springboot项目--后端问题记录

springboot项目后端记录

  • 前言
  • 一、包
  • 二、处理技巧
    • [1. 全局异常处理](#1. 全局异常处理)
    • [2. 拦截器(用于请求转发前的验证)](#2. 拦截器(用于请求转发前的验证))
    • [3. sql表中下划线列匹配实体类小驼峰](#3. sql表中下划线列匹配实体类小驼峰)
    • [4. 在mvc框架中,让实体类属性不传到前端(JsonIgnore注解)](#4. 在mvc框架中,让实体类属性不传到前端(JsonIgnore注解))
    • [5. ThreadLocal处理taken数据重复解析](#5. ThreadLocal处理taken数据重复解析)
    • [6. 实体类日期属性转json时格式处理](#6. 实体类日期属性转json时格式处理)
    • [7. Validated包中分组校验实体类参数(不同方法对校验要求不同)](#7. Validated包中分组校验实体类参数(不同方法对校验要求不同))
    • [8. Validated自定义校验注解](#8. Validated自定义校验注解)
    • [9. 当前端传过来的参数不是必须的时候](#9. 当前端传过来的参数不是必须的时候)
    • [10. 数据库分页查询](#10. 数据库分页查询)
    • [11. springBoot中整合mybatis时mapper.xml文件存放位置](#11. springBoot中整合mybatis时mapper.xml文件存放位置)
    • [12. 文件上传](#12. 文件上传)
    • [13. 阿里oss对象存储服务存储文件](#13. 阿里oss对象存储服务存储文件)
    • [14. 使用redis实现taken主动失效](#14. 使用redis实现taken主动失效)
      • 实现步骤
        • [① 添加redis的依赖](#① 添加redis的依赖)
        • [② 在用户登陆的时候向redis中添加taken](#② 在用户登陆的时候向redis中添加taken)
        • [③ 在拦截器中加入taken比对](#③ 在拦截器中加入taken比对)
        • [④ 修改密码后删除redis中的taken](#④ 修改密码后删除redis中的taken)
  • 三、关于项目环境方面的东西
    • [1. 项目部署](#1. 项目部署)
    • [2. 属性配置方法](#2. 属性配置方法)
      • [(1) 项目内配置文件](#(1) 项目内配置文件)
      • [(2) jar包统一目录的application.yml配置文件](#(2) jar包统一目录的application.yml配置文件)
      • [(3) 环境变量](#(3) 环境变量)
      • [(4) 命令行配置](#(4) 命令行配置)
      • 优先级
    • [3. 多环境管理](#3. 多环境管理)
      • [(1) 单文件中的环境管理](#(1) 单文件中的环境管理)
      • [(2) 多文件的环境](#(2) 多文件的环境)
      • [(3) 环境分组](#(3) 环境分组)
  • 参考

前言

用于记录springboot学习中没见过的注解技巧


一、包

1. lombok--自动生成勾子方法

作用

作用在类之上,自动生成类的 getter setter toString equal等方法

依赖

xml 复制代码
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.34</version>
          <scope>provided</scope>
      </dependency>

使用

Data注解自动实现getter setter toString equal等方法

Constructor类型注解用于自动实现构造函数

这些代码会出现在编译以后的class文件中,但是不会出现在源文件中

java 复制代码
package com.xduzmh.pojo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//统一响应结果
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {
    private Integer code;//业务状态码  0-成功  1-失败
    private String message;//提示信息
    private T data;//响应数据

    //快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    //快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

    public static Result error(String message) {
        return new Result(1, message, null);
    }
}

2. Validated--自动校验

自动校验中的分组校验和自定义校验注解放在了第二章技巧里面
validated常用注解

作用

接收的是当个参数时,用于完成参数的格式校验, 如用户名长度格式要求

依赖

xml 复制代码
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>

使用

一般参数校验

在类之上先加注解@Validated,告诉框架需要对这个类进行校验。

在需要校验的参数前面加@Patten注解,注解的参数regexp里面写匹配的正则表达式

java 复制代码
@Validated
@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
	private UserService userService;

//	注册
	@PostMapping("/register")
	public Result register(@Pattern(regexp = "^\\s{5,16}$") String username, @Pattern(regexp = "^\\s{5,16}$") String password){
		//判断是否存在
		User user = userService.findByUserName(username);
		if (null != user){
			return Result.error("用户存在");
		}else {
//			注册(需要对密码进行加密)
			String md5String = Md5Util.getMD5String(username);
			userService.register(username, md5String);
			return Result.success();
		}
	}

也可以在需要被校验的参数前面加注解@Validated

java 复制代码
@PostMapping("/register")
	public Result register(@Validated @Pattern(regexp = "^\\S{5,16}$") String username,@Validated @Pattern(regexp = "^\\S{5,16}$") String password){
		//判断是否存在
		User user = userService.findByUserName(username);
		if (null != user){
			return Result.error("用户被占用");
		}else {
//			注册(需要对密码进行加密)
			String md5String = Md5Util.getMD5String(password);
			userService.register(username, md5String);
			return Result.success();
		}
	}
实体参数校验

需要校验的参数由一个实体类封装,这时需要对实体类的属性添加限制注解。在需要校验的实体参数前加@Validated注解

validated常用注解

实体类

java 复制代码
//Data注解来自lombok,可以自动生成类的 getter setter toString equal等方法
@Data
public class User {
    @NotNull
    private Integer id;//主键ID
    private String username;//用户名
    @JsonIgnore
    private String password;//密码
    @NotNull
    @NotEmpty
    @Pattern(regexp = "^\\S{1,10}$")
    private String nickname;//昵称
    @Email
    private String email;//邮箱
    private String userPic;//用户头像地址
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

参数校验使用的方法,加注解@Validated到对应参数前面

java 复制代码
	@PutMapping("/update")
	public Result update(@RequestBody @Validated User user){

		userService.update(user);
		return Result.success("更新成功");
	}

结论

  • 当参数是普通参数时Validated注解可以放在参数之前或者类之上都可以
  • 参数是封装实体类的时候,Validated注解只能放在参数前
  • 用在实体属性上的那些注解(@NotNull,@Email等)也可以直接用在参数前面

3. JWT(json web taken) 令牌生成

什么是taken

taken本质上是一个字符串

  • 作用
    • 给客户的身份验证,客户在登陆后获得taken,如何以后的每一次访问都需要带上taken,防止未登陆的用户可以访问系统内资源
  • 要求
    • 承载业务数据, 减少后续请求查询数据库的次数
    • 防篡改, 保证信息的合法性和有效性

JWT包

JWT包内实现了JWT标准的taken封装和拆解。jwt令牌主要有三个部分组成。这几个部分原本的数据是json格式的,通过算个转换为了Base64(即常用的64个字符,包括大小写字母,数字等)。

  • 头包含加密算法类型。
  • 中间这段是承载的信息,一般是客户端的id和用户名等非敏感信息(这样可以较少对承载的信息的查询)
  • 最后一段数组签名是为了防止数据被篡改,对头部和载荷进行加密计算得来
依赖
xml 复制代码
      <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<!--      用于Taken令牌生成-->
      <dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>4.4.0</version>
      </dependency>
使用
获取taken
java 复制代码
	public void jwtTest(){
		Map<String, Object> user = new HashMap<>();
		user.put("username", "zhangsan");
		user.put("ID", 1);
		String jwt = JWT.create().withClaim("user", user)
				.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
				.sign(Algorithm.HMAC256("zhangsan"));
		System.out.println(jwt);
	}
校验


加密的密钥不能错,校验失败会抛出异常,使用时通过捕捉异常进行判断

java 复制代码
	@Test
	public void jwtDecodeJwt(){
		String taken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
				".eyJ1c2VyIjp7IklEIjoxLCJ1c2VybmFtZSI6InpoYW5nc2FuIn0sImV4cCI6MTcyNTU5MTA5MX0" +
				".ihpqakbm6OTg5dVcVPmqGea7izW2Fa4TQyUlqcFH9GI";
		JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("zhangsan")).build();

		//校验失败会抛出异常,所以在使用时需要对异常进行处理
		DecodedJWT decodedJWT = jwtVerifier.verify(taken);
		Map<String, Claim> claims = decodedJWT.getClaims();
		System.out.println(claims.get("user"));
	}
封装的工具类
java 复制代码
package com.xduzmh.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtil {

    private static final String KEY = "itheima";
	
	//接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
                .sign(Algorithm.HMAC256(KEY));
    }

	//接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }

}
xml 复制代码
使用

二、处理技巧

1. 全局异常处理

@RestControllerAdvice作用及原理

作用

用于捕捉异常,比如在使用validation校验失败时会产生异常,这时就需要进行全局异常的捕捉

代码
java 复制代码
package com.xduzmh.exception;

import com.xduzmh.pojo.Result;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(Exception.class)
	public Result exceptionHandler(Exception e){
		e.printStackTrace();
		return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
	}

}

StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败"主要是处理没有错误信息的情况。

2. 拦截器(用于请求转发前的验证)

作用

在指定接收到请求时会先传给拦截器,如果符合要求则通过请求,不符合则拦截。

可以用来对taken进行校验,拦截taken不符的请求

代码

实现拦截器对象

需要实现HandlerInterceptor 接口的preHandle方法,返回true放行,返回false拦截,请求参数在request对象中,返回值可以在response设置

一般情况下会创建interceptors包,所有拦截器都放在这个包下面
记得在类上加Component注解,把对象注入容器

java 复制代码
package com.xduzmh.interceptors;

import com.xduzmh.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Map;

@Component
public class LoginInterceptor implements HandlerInterceptor {

	@Override
	public boolean preHandle(HttpServletRequest request,
							 HttpServletResponse response,
							 Object handler) throws Exception {
		String taken = request.getHeader("Authorization");
		try {
			Map<String, Object> claims = JwtUtil.parseToken(taken);
		} catch (Exception e) {
			response.setStatus(401);
			return false;
		}
		return true;
	}
}

注册拦截器

拦截器需要在web配置类中添加, web配置了需要实现接口WebMvcConfigurer下的addInterceptors方法来添加拦截器。
可以用excludePathPatterns("/user/register","/user/login")来放行方法

配置类放在config包下

记得在类上加Configuration注解,声明是配置类

java 复制代码
package com.xduzmh.config;


import com.xduzmh.interceptors.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Autowired
	private LoginInterceptor loginInterceptor;

	@Override
	public void addInterceptors(InterceptorRegistry registry){
		registry.addInterceptor(loginInterceptor)
				.excludePathPatterns("/user/register","/user/login");
	}

}

3. sql表中下划线列匹配实体类小驼峰

在实体类中属性是小驼峰命名,表中是下划线命名,这时需要进行转换

只需要在spring配置文件中开启mybatis转换即可

yml 复制代码
mybatis:
  configuration:
    map-underscore-to-camel-case: true

4. 在mvc框架中,让实体类属性不传到前端(JsonIgnore注解)

在实体类中可能存在一些私密信息,传到前端就会暴露隐私,如密码等字段,这种情况下springboot提供了对应的注解,只需要在实体类对应属性之上加入注解@JsonIgnore,就可以在对象转json时忽略这个属性

java 复制代码
@Data
public class User {
    private Integer id;//主键ID
    private String username;//用户名
    @JsonIgnore
    private String password;//密码
    private String nickname;//昵称
    private String email;//邮箱
    private String userPic;//用户头像地址
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

5. ThreadLocal处理taken数据重复解析

Tomcat服务器会为每个用户开辟单独的线程,所有可以用ThreadLocal来存储需要频繁使用的数据,如用户名等属性

如前所说的,用户名username可以从taken中解析出来,但是每次用都要进行解析就十分的麻烦且费事,我们就可以把username放在一个全局的ThreadLocal中,需要的时候直接get()。

ThreadLocal存数据的时候可以在拦截器里面存,因为需要taken的请求都会过经过拦截器。直接在拦截器里面添加即可。

操作ThreadLocal的工具类
java 复制代码
package com.xduzmh.utils;

import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocal 工具类
 */
@SuppressWarnings("all")
public class ThreadLocalUtil {
    //提供ThreadLocal对象,
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    //根据键获取值
    public static <T> T get(){
        return (T) THREAD_LOCAL.get();
    }
	
    //存储键值对
    public static void set(Object value){
        THREAD_LOCAL.set(value);
    }


    //清除ThreadLocal 防止内存泄漏
    public static void remove(){
        THREAD_LOCAL.remove();
    }
}
使用

在拦截器中向ThreadLocal增加数据,把taken解析出来的数就存到ThreadLocal中

java 复制代码
@Component
	public boolean preHandle(HttpServletRequest request,
							 HttpServletResponse response,
							 Object handler) throws Exception {
		String taken = request.getHeader("Authorization");
		try {
			Map<String, Object> claims = JwtUtil.parseToken(taken);
			ThreadLocalUtil.set(claims);
		} catch (Exception e) {
			response.setStatus(401);
			return false;
		}
		return true;
	}

数据获取

java 复制代码
	@GetMapping("/userInfo")
	public Result<User> userInfo(){

		Map<String, Object> claims = ThreadLocalUtil.get();
		String username = (String) claims.get("username");
		User user = userService.findByUserName(username);
		return Result.success(user);
	}

ThreadLocal数据移除时机

ThreadLocal是全局的,如果只添加而不移除就会造成内存泄漏

移除的时机是这次请求结束的时候,请求结束的处理需要在拦截器类里面实现对应的afterCompletion接口方法

java 复制代码
@Component
public class LoginInterceptor implements HandlerInterceptor {

   @Override
   public boolean preHandle(HttpServletRequest request,
   						 HttpServletResponse response,
   						 Object handler) throws Exception {
   	String taken = request.getHeader("Authorization");
   	try {
   		Map<String, Object> claims = JwtUtil.parseToken(taken);
   		ThreadLocalUtil.set(claims);
   	} catch (Exception e) {
   		response.setStatus(401);
   		return false;
   	}
   	return true;
   }

   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   	ThreadLocalUtil.remove();
   }
}

6. 实体类日期属性转json时格式处理

只需要在实体类对应的日期属性上加JsonFormat注解,并指定格式即可

java 复制代码
public class Category {
    private Integer id;//主键ID
    @NotEmpty
    private String categoryName;//分类名称
    @NotEmpty
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

加和不加的区别

7. Validated包中分组校验实体类参数(不同方法对校验要求不同)

这里的主要场景是用于不同方法对同一个属性参数的要求可能不同,如ID字段,有的方法没有要求,有的方法要求非空,这时就不能同时满足两个方法,这时就需要对校验进行分组

实现

①定义组接口

组接口是实现在实体类里面的public接口

java 复制代码
package com.xduzmh.pojo;

import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;

import java.time.LocalDateTime;

//Data注解来自lombok,可以自动生成类的 getter setter toString equal等方法
@Data
public class Category {

    public interface Add {

    }
    public interface Update {

    }

    @NotNull
    private Integer id;//主键ID
    @NotEmpty
    private String categoryName;//分类名称
    @NotEmpty
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime updateTime;//更新时间
}
②给注解添加分组

如果 id 属性的@NotNull属于Update 组的就只需要指定一下@NotNull的group属性=Update .class。

这样,只有在指定Validated注解使用Update 这个组时,id才有NotNull的限制

java 复制代码
package com.xduzmh.pojo;

import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;

import java.time.LocalDateTime;

//Data注解来自lombok,可以自动生成类的 getter setter toString equal等方法
@Data
public class Category {

    public interface Add {

    }
    public interface Update {

    }

    @NotNull(groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty(groups = {Update.class, Add.class})
    private String categoryName;//分类名称
    @NotEmpty(groups = {Update.class, Add.class})
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime updateTime;//更新时间
}
③使用时在Vilidated注解中赋值分组
java 复制代码
//	增加
	@PostMapping
	public Result add(@RequestBody @Validated(Category.Add.class) Category category){
		categoryService.add(category);
		return Result.success();
	}
	
//	更新分类名和别名
	@PutMapping
	public Result update(@RequestBody @Validated(Category.Update.class) Category category){
		categoryService.update(category);
		return Result.success();
	}
默认分组的情况

上面的写法中,如果不指定组,那sping会默认分配给Default组。

分组是可以继承的,所有可以通过继承的方式优化代码

java 复制代码
@Data
public class Category {

    public interface Add extends Default {

    }
    public interface Update extends Default{

    }
    @NotNull(groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty
    private String categoryName;//分类名称
    @NotEmpty
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime updateTime;//更新时间
}

对于上面的代码,相当于
@NotEmpty private String categoryName;//分类名称 @NotEmpty private String categoryAlias;//分类别名

属于默认分组,也就相当于这两个以及包含在了Add组和Update组里面了。

当Validated注解没有指定参数时,使用的是默认分组Default

8. Validated自定义校验注解

注解一般防止anno包下,校验方法实现类放在validation包下

创建注解

java 复制代码
package com.xduzmh.anno;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

@Target({ElementType.FIELD}) //元注解,声明注解作用位置
@Retention(RetentionPolicy.RUNTIME) //元注解,声明注解保留时间
@Constraint(validatedBy = StateValidation.class)  //声明校验处理方法的类
public @interface State {

	String message() default "{状态只能是\"草稿\"和\"已发布\"}"; //校验失败的提示信息

	Class<?>[] groups() default {}; 	//注解的分组信息

	Class<? extends Payload>[] payload() default {}; //注解的附加数据,一般不用
}

创建校验方法类

校验类需要实现接口ConstraintValidator<注解, 需要校验数据类型>的isValid方法(true校验通过,false校验失败)

java 复制代码
package com.xduzmh.validation;

import com.xduzmh.anno.State;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class StateValidation implements ConstraintValidator<State, String> {
	@Override
	public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
		if(s == null){
			return false;
		}
		if (s.equals("已发布") || s.equals("草稿")){
			return true;
		}
		return false;
	}
}

9. 当前端传过来的参数不是必须的时候

当前端传过来的参数不是必须的时候, 可以在参数前指定注解`@RequestParam(required = false)

java 复制代码
	@GetMapping
	public Result<PageBean<Article>> list(
			Integer pageNum,
			Integer pageSize,
			@RequestParam(required = false) Integer categoryId,
			@RequestParam(required = false) String state
	){
		PageBean<Article> res = articleService.list(pageNum, pageSize, categoryId, state);
		return Result.success(res);
	}

10. 数据库分页查询

mybatis的分页查询需要使用到插件pagehelper

依赖

xml 复制代码
      <dependency>
          <groupId>com.github.pagehelper</groupId>
          <artifactId>pagehelper-spring-boot-starter</artifactId>
          <version>2.1.0</version>
      </dependency>

使用方法

java 复制代码
PageHelper.startPage(pageNum, pageSize);

参数pageNum表示要查询第几页
pageSize表示每页存几条数据

这样就算开启了分页插件


java 复制代码
List<Article> as = articleMapper.list(categoryId, state,userId);
		Page<Article> page = (Page<Article>) as;

		pageBean.setTotal(page.getTotal());
		pageBean.setItems(page.getResult());

调用数据集查询后得到的结果要强转成Page类,这样才能获取到查询结果的一些信息,如总共查询到几条数据等

具体结果表示

11. springBoot中整合mybatis时mapper.xml文件存放位置

mapper.xml存放位置

动图SQL参看mybatis框架

12. 文件上传

从前端上传的文件会被springBoot保存在MultipartFile类的对象中

对象方法

java 复制代码
String getOriginalFilename(); //获取原始文件名
void transferTo(File dest); //将接收的文件转存到磁盘文件中
long getSize(); //获取文件的大小,单位:字节
byte[] getBytes(); //获取文件内容的字节数组
InputStream getInputStream(); //获取接收到的文件内容的输入流

实现

java 复制代码
package com.xduzmh.controller;

import com.github.pagehelper.util.MSUtils;
import com.xduzmh.pojo.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

@RestController

public class FileUploadContoller {

	@PostMapping("/upload")
	public Result<String> upload(MultipartFile file) throws IOException {

		//获取文件后缀名
		String filenamesuffix = file.getOriginalFilename().
				substring(file.getOriginalFilename().lastIndexOf("."));
		//通过uuid生成随机字符串当做文件名,防止同名图片覆盖
		String filename = UUID.randomUUID().toString();

		System.out.println(filenamesuffix + filename);
		//把文件存在本地磁盘. 如果这里有oss服务,可以调用服务把图片存在oss服务器上
		file.transferTo(new File("C:\\Users\\Administrator\\Desktop\\pic\\"
				+ filename + filenamesuffix));

		return Result.success("图片的url");
	}
}

13. 阿里oss对象存储服务存储文件

使用第三方服务的思路

  • 准备工作
    • 开通服务,获取相关数据(密钥等数据)
  • 参照官方SDK编写入门程序
    • 看官方案例
  • 集成使用
    • 参考官方案例写自己的代码

14. 使用redis实现taken主动失效

为什么要让令牌主动失效

  • 当你泄露密码时,别人用你的密码登陆了服务获得了一个taken
  • 你知道情况后立即修改了密码
  • 你虽然修改了密码,但是别人已经登陆了,有了taken,就可以用这个taken去访问你的资源

实现步骤

  1. 在用户登陆的时候向redis中添加taken
  2. 在每次访问资源时都会经过的拦截器中加入taken的校验
    • 如果当前redis中存在taken,则通过,否则说明修改了密码
  3. 修改密码时删除redis中的taken
① 添加redis的依赖
xml 复制代码
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.2.4</version>
</dependency>

在使用时调用stringRedisTemplate对象中的方法获得对redis的操作

Springboot会自动帮我们注入一个StringRedisTemplate对象

② 在用户登陆的时候向redis中添加taken
java 复制代码
//	登录
	@PostMapping("/login")
	public Result login(@Pattern(regexp = "^\\S{5,16}$") String username,
						@Pattern(regexp = "^\\S{5,16}$") String password){
		User user = userService.findByUserName(username);
		//用户不存在
		if (null == user){
			return Result.error("用户名错误");
		}

		//	存在的情况下验证密码是否正确,正确的话赋予令牌
		if(user.getPassword().equals(Md5Util.getMD5String(password))){
			//密码一致登录成功, 授予登录成功的客户端taken
			Map<String, Object> claims = new HashMap<>();
			claims.put("id", user.getId());
			claims.put("username", user.getUsername());
			String taken = JwtUtil.genToken(claims);

			//添加到redis
			stringRedisTemplate.opsForValue().set(taken,taken,12, TimeUnit.HOURS);
			
			return Result.success(taken);
		}
		//密码错误
		return Result.error("密码错误");
	}
③ 在拦截器中加入taken比对
④ 修改密码后删除redis中的taken
java 复制代码
//	更新密码
	@PatchMapping("/updatePwd")
	public Result updatePwd(@RequestBody Map<String, String> params, @RequestHeader("Authorization") String taken){
		String oldPwd = params.get("old_pwd");
		String newPwd = params.get("new_pwd");
		String rePwd = params.get("re_pwd");

		//非空判断
		if(!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)){
			return Result.error("密码不能为空");
		}

		// 原密码比对
		Map<String, Object> map = ThreadLocalUtil.get();
		String username = (String) map.get("username");
		User loginuser = userService.findByUserName(username);
		if (!loginuser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
			return Result.error("密码不正确");
		}
		// 对比两次密码是否一致
		if(!newPwd.equals(rePwd)){
			return Result.error("两次输入密码不一致");
		}
		//修改密码
		userService.updatePwd(newPwd);
		

	
		//删除redis中旧的taken
		stringRedisTemplate.opsForValue().getOperations().delete(taken);
		return Result.success("密码修改成功");
	}

三、关于项目环境方面的东西

1. 项目部署

项目部署需要把项目打包成jar包,放在服务器上,通过java -jar 包名运行jar包

在项目打包时需要配置springboot的项目打包插件

xml 复制代码
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

然后执行maven的package指令,打包好的jar包在target目录下,把jar包放在服务器下通过java -jar 包名运行

当然运行的环境必须要有JRE且版本必须是java17及其以上

2. 属性配置方法

属性就是一些写在配置文件里面的参数如数据库的连接信息等

yml 复制代码
spring:
 datasource:
   driver-class-name: com.mysql.cj.jdbc.Driver

(1) 项目内配置文件

这些信息一般都是写在spingboot的配置文件中

yml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456
mybatis:
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: /mapper/*.xml

(2) jar包统一目录的application.yml配置文件

在jar包同一目录下添加application.yml配置文件,写法和项目内的配置文件是一样的

(3) 环境变量

直接把要配置的参数加到环境变量中

变量名就是属性名,中间用 . 隔开

(4) 命令行配置

这种配置方法是在执行java命令时把属性当作参数传进去
java -jar 包名 --属性名=属性值

优先级

上面四种方法优先级逐渐增加

命令行 > 环境变量 > 外部配置文件 > 内部配置文件
使了一下这些配置能不能兼容

测试方法在内部配置文件配置了web访问路径是 /aaa。在执行命令行是没有管web访问路径,发现访问路径没有发生改变。

这说明各个配置方式不兼容
初学者,出现错误希望告知笔者,谢谢

3. 多环境管理

在项目开发过程中往往会有不同的开发阶段(开发,测试,发布),不同的开发阶段往往使用了不同的配置环境。这就需要进行环境的频繁切换。频繁的修改配置文件中的很多属性,这就很麻烦。

springboot提供了多环境管理机制

(1) 单文件中的环境管理

在单个文件中的环境管理,各个环境是通过---分隔开的。

公共的配置往往写在最开始,便于查找修改,如何才是各个环境的配置

环境名称的指定通过属性spring.config.activate.on-profile:环境名称指定

在选择生效环境时只需要配置spring.profiles active: 环境名称

yml 复制代码
# 公共部分

mybatis:
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: /mapper/*.xml
server:
  servlet:
    context-path: /a
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456
  profiles:
    active: test

# 开发环境
---
spring:
  config:
    activate:
      on-profile: dev

server:
  port: 8081

# 测试环境
---
spring:
  config:
    activate:
      on-profile: test
server:
  port: 8082

# 发布环境
---
spring:
  config:
    activate:
      on-profile: pro
server:
  port: 8083

(2) 多文件的环境

在单文件管理中,如果很多个环境放在一个文件中就会很难管理,所以springboot也提供了多文件的环境管理。

只需要在命名文件为application-环境名.yml
注意,文件名一定要是application-环境名.yml,环境名不区分大小写,但是一定要对,一定要和配置文件里面配置的环境名字一样

application的内容

yml 复制代码
# 公共部分

mybatis:
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: /mapper/*.xml
server:
  servlet:
    context-path: /a
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456
  profiles:
    active: test

application-dev.yml内容

yml 复制代码
# 开发环境
---
spring:
  config:
    activate:
      on-profile: dev

server:
  port: 8081

(3) 环境分组

在多文件配置的情况下又会出现某个环境的配置很多的情况下,这时又需要对环境的配置进行划分
application.xml配置

下面展示一下dev环境的内容

application.yml

yml 复制代码
spring:
  profiles:
    active: test
    group:
      "dev": devDB, devServer, devSelf
      "test" : testServer,testDB

application-devDB.yml

yml 复制代码
mybatis:
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: /mapper/*.xml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://locahost:3306/big_event
    username: root
    password: 123456

application-devServer.yml

yml 复制代码
server:
  port: 8081

application-devSelf.yml是空文件什么都没写

参考

@RestControllerAdvice作用及原理validated常用注解
mapper.xml存放位置
mybatis框架

相关推荐
XINGTECODE9 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码14 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶15 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺19 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序27 分钟前
vue3 封装request请求
java·前端·typescript·vue
凡人的AI工具箱42 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
陈王卜44 分钟前
django+boostrap实现发布博客权限控制
java·前端·django
小码的头发丝、1 小时前
Spring Boot 注解
java·spring boot
先天牛马圣体1 小时前
如何提升大型AI模型的智能水平
后端
午觉千万别睡过1 小时前
RuoYI分页不准确问题解决
spring boot