跳至主要內容

SpringBoot项目合集

sixkey大约 71 分钟后端SpringBoot项目实战

SpringBoot项目合集

1、搭建SpringBoot项目

导入依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>sixkey</groupId>
  <artifactId>swx</artifactId>
  <version>1.0-SNAPSHOT</version>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.1</version>
  </parent>

  <dependencies>
      
    <!-- Mvc依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
      
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
      
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
      
    <!--mysql数据库驱动依赖-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
      
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<!--一定要引入一个jdbc依赖,不然会和springboot默认的冲突-->    
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
      <version>2.7.3</version>
    </dependency>
    
    <!--druid连接池-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.22</version>
    </dependency>
    
    <!--简化配置-->
    <!-- 引入lombok -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    
    <!-- 引入fastjson -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>2.0.7</version>
    </dependency>

配置文件

server:
  port: 8200
spring:
  application:
    name: swx
  datasource:
    url: jdbc:mysql://localhost:3306/sixkey?characterEncoding=UTF-8&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: '020708'
    type: com.alibaba.druid.pool.DruidDataSource
    initial-size: 10
    max-active: 150
    min-idle: 10
    max-wait: 5000
    pool-prepared-statements: false
    validation-query: SELECT 1
    validation-query-timeout: 500
    test-on-borrow: false
    test-on-return: false
    test-while-idle: true
    time-between-eviction-runs-millis: 60000
    min-evictable-idle-time-millis: 30000
    filters: stat,wall,log4j
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

2、集成swagger增强版

添加依赖

<!-- swagger-->
    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <!--knife4j-spring-boot-starter-->
      <artifactId>knife4j-spring-boot-starter</artifactId>
      <version>3.0.3</version>
    </dependency>

添加配置文件

#knife4j
knife4j:
  # 开启增强配置
  enable: true
  basic:
    enable: true
    # Basic认证用户名
    username: admin
    # Basic认证密码
    password: admin

添加配置类

package sixkey.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * ClassName:Knife4jConfig
 * Package:sixkey.config
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/19 - 22:26
 * @Version:v1.0
 */
@Configuration
@EnableSwagger2
@EnableKnife4j
public class Knife4jConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .useDefaultResponseMessages(false)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("sixkey.controller"))
                .paths(PathSelectors.any())
                .build();

    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .description("文学接口文档")
                .contact(new Contact("文学", "http:://8.130.108.52", "2073958890@qq.com"))
                .version("v1.1.0")
                .title("文学接口文档")
                .build();
    }
}

美化启动类

@Slf4j
@SpringBootApplication
@ServletComponentScan
public class Application {
    public static void main(String[] args) throws UnknownHostException {
        ConfigurableApplicationContext application = SpringApplication.run(Application.class, args);
        log.info("==============项目启动成功==============");
        //获取配置信息
        ConfigurableEnvironment environment = application.getEnvironment();
        //获取port
        String port = environment.getProperty("server.port");
        //获取ip主机
        String ip = InetAddress.getLocalHost().getHostAddress();
        System.out.println("\n----------------------------------------------------------\n\t" +
                "sixkey is running! Access URLs:\n\t" +
                "Knife4j-ui: \thttp://" + ip + ":" + port + "/doc.html\n\t" +
                "----------------------------------------------------------");
    }
}

控制台输出格式

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.1)
2023-05-19 22:40:32.367  INFO 16360 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2023-05-19 22:40:32.463  INFO 16360 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8200 (http) with context path ''
2023-05-19 22:40:32.530  INFO 16360 --- [           main] sixkey.Application                       : Started Application in 2.473 seconds (JVM running for 3.058)
2023-05-19 22:40:32.531  INFO 16360 --- [           main] sixkey.Application                       : ==============项目启动成功==============

----------------------------------------------------------
	sixkey is running! Access URLs:
	Knife4j-ui: 	http://192.168.134.131:8200/doc.html   //直接点即可访问接口文档
	----------------------------------------------------------

controller类编写

@Api(tags = "系统用户管理接口")
@RestController
@RequestMapping("/users")
public class UserController {

    @ApiOperation(value = "用户列表", httpMethod = "GET", notes = "用户列表")
    @GetMapping("/test")
    public String test(){
        return "hello swx~";
    }
}

浏览器访问展示

p94anw4.jpgopen in new window
p94anw4.jpg

3、集成Mybatis-Plus

添加依赖

<!--mybatis-plus依赖,已经包括了mybatis的依赖了-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>
        <!--mybatis-plus的模板引擎依赖,使用mybatyis-plus必须要加这个依赖-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
		</dependency>

配置文件

#==============================mybatis-plus配置==============================
mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.shiyi.entity
  global-config:
    # 数据库相关配置
    db-config:
      #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: AUTO
      #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
      field-strategy: not_empty
      #驼峰下划线转换
      column-underline: true
      db-type: mysql
      db-type: mysql
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
    #刷新mapper 调试神器
    refresh: true
  # 原生配置
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
  logging:
    level: warn

公共字段自动填充

  • 编写MyMetaObjectHandler 类
  • 实现MetaObjectHandler接口
package sixkey.config.MybatisPlusConfig;

/**
 * ClassName:MyMetaObjectHandler
 * Package:sixkey.config.MybatisPlusConfig
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/19 - 23:15
 * @Version:v1.0
 */

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Date;

/**
 * 公共字段自动填充配置
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入时自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
    }



    /**
     * 修改时自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
    }
}

实体类

package sixkey.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import javax.annotation.Tainted;
import java.io.Serializable;
import java.util.Date;

/**
 * ClassName:User
 * Package:sixkey.entity
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/19 - 23:20
 * @Version:v1.0
 */

/**
 * 系统用户类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user") //对应数据库的表
@Accessors(chain = true) //可以链式编写
public class User implements Serializable {
    @TableId // 主键
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String sex;
    @TableLogic
    private Integer deleted; //逻辑删除
    @TableField(fill = FieldFill.INSERT) //插入时自动填充
    private Date createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE) //修改时自动填充
    private Date updateTime;
}

mapper类

/**
 * 系统用户
 */
public interface UserMapper extends BaseMapper<User> {
}

service类

/**
 * 系统用户类
 */
public interface IUserService extends IService<User> {
}

serviceImpl类

/**
 * 系统用户类
 */
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

具体的CRUD由具体功能要求去实现吧~

4、统一响应基础类

响应枚举类

package sixkey.utils;

/**
 * ClassName:ResCodeEnum
 * Package:sixkey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/20 - 0:11
 * @Version:v1.0
 */

/**
 * 响应码枚举类
 */
public enum ResCodeEnum {
    
    SUCCESS(200,"操作成功"),
    FAIL(201,"操作失败"),
    NEED_LOGIN(401,"需要登录后操作"),
    SYSTEM_ERROR(500,"系统错误"),
    USERNAME_EXIST(501, "用户名已存在"),
    PHONENUMBER_EXIST(502, "手机号已存在"),
    EMAIL_EXIST(503, "邮箱已存在"),
    REQUIRE_USERNAME(504, "必需填写用户名"),
    CONTENT_NOT_NULL(506, "评论内容不能为空"),
    FILE_TYPE_ERROR(507, "文件类型错误"),
    USERNAME_NOT_NULL(508, "用户名不能为空"),
    NICKNAME_NOT_NULL(509, "昵称不能为空"),
    PASSWORD_NOT_NULL(510, "密码不能为空"),
    EMAIL_NOT_NULL(511, "邮箱不能为空"),
    NICKNAME_EXIST(512, "昵称已存在"),
    LOGIN_ERROR(505, "用户名或密码错误");


    int code;
    String message;

    ResCodeEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode(){
        return code;
    }

    public String getMessage(){
        return message;
    }
}

统一响应类

package sixkey.utils;

import java.io.Serializable;

/**
 * ClassName:ResResult
 * Package:sixkey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/20 - 0:21
 * @Version:v1.0
 */

/**
 * 统一结果响应类
 */
public class ResResult<T> implements Serializable {
    private Integer code;
    private String message;
    private T data;

    //当操作成功,无返回数据时
    public ResResult(){
        this.code = ResCodeEnum.SUCCESS.code;
        this.message = ResCodeEnum.SUCCESS.getMessage();
    }

    //返回码code和数据data
    public ResResult(Integer code, T data){
        this.code = code;
        this.data = data;
    }

    //返回码code和消息message
    public ResResult(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    //返回码code、消息message、数据集data
    public ResResult(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static ResResult errorResult(int code, String msg) {
        ResResult result = new ResResult();
        return result.error(code, msg);
    }

    public static ResResult okResult() {
        ResResult result = new ResResult();
        return result;
    }

    public static ResResult okResult(int code, String msg) {
        ResResult result = new ResResult();
        return result.ok(code, null, msg);
    }

    public static ResResult okResult(Object data) {
        ResResult result = setAppHttpCodeEnum(ResCodeEnum.SUCCESS, ResCodeEnum.SUCCESS.getMessage());
        if (data != null) {
            result.setData(data);
        }
        return result;
    }

    public static ResResult errorResult(ResCodeEnum enums) {
        return setAppHttpCodeEnum(enums, enums.getMessage());
    }

    public static ResResult errorResult(ResCodeEnum enums, String msg) {
        return setAppHttpCodeEnum(enums, msg);
    }

    public static ResResult setAppHttpCodeEnum(ResCodeEnum enums) {
        return okResult(enums.getCode(), enums.getMessage());
    }

    private static ResResult setAppHttpCodeEnum(ResCodeEnum enums, String msg) {
        return okResult(enums.getCode(), msg);
    }

    public ResResult<?> error(Integer code, String msg) {
        this.code = code;
        this.message = msg;
        return this;
    }

    public ResResult<?> ok(Integer code, T data) {
        this.code = code;
        this.data = data;
        return this;
    }

    public ResResult<?> ok(Integer code, T data, String msg) {
        this.code = code;
        this.data = data;
        this.message = msg;
        return this;
    }

    public ResResult<?> ok(T data) {
        this.data = data;
        return this;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

  • 成功

ResResult.okResult();

  • 失败

ResResult.errorResult(ResCodeEnum.SYSTEM_ERROR) , 参数传入我们定义的响应枚举类

实战

package sixkey.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import sixkey.domain.vo.UserVo;
import sixkey.service.IUserService;
import sixkey.utils.ResResult;

/**
 * ClassName:controller
 * Package:sixkey
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/19 - 22:09
 * @Version:v1.0
 */
@Slf4j
@Api(tags = "系统用户管理接口")
@RestController
@RequestMapping("/users")
//lombok提供的自动注入注解,但是被注入属性要加final修饰
@RequiredArgsConstructor
public class UserController {

    //已经通过@RequiredArgsConstructor此注解自动注入值
    private final IUserService userService;

    /**
     * 添加用户
     * @param userVo
     * @return
     */
    @ApiOperation(value = "添加用户", httpMethod = "POST",response = ResResult.class, notes = "添加用户")
    @PostMapping("/add")
    public ResResult addUser(@RequestBody UserVo userVo){
        log.info("==== {}用户请求进来了 =====",userVo.getUsername());
        userService.addUser(userVo);
        return ResResult.okResult();
    }
}

接口测试展示

p94dTPA.jpgopen in new window
p94dTPA.jpg

5、测试报错

报了 java.lang.IllegalArgumentException: argument type mismatch 数据类型不匹配

**原因:**数据库中的create_time 和 update_time设置了时间戳来填充,而mybatisplus自动填充字段那里是用了系统时间

修改数据库

p94wZa4.jpgopen in new window
p94wZa4.jpg

**代码修改:**设置为当前时间

package sixkey.config.MybatisPlusConfig;

/**
 * ClassName:MyMetaObjectHandler
 * Package:sixkey.config.MybatisPlusConfig
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/19 - 23:15
 * @Version:v1.0
 */

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Date;

/**
 * 公共字段自动填充配置
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入时自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
    }



    /**
     * 修改时自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
    }
}

6、统一异常处理

自定义一个系统异常去继承Exception异常类

/**
 * 系统异常处理
 */
public class SystemException extends Exception{
    private int code;
    private String message;
    
    public SystemException(ResCodeEnum resCodeEnum){
        super(resCodeEnum.getMessage());
        this.code = resCodeEnum.getCode();
        this.message = resCodeEnum.getMessage();
    }

    public int getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

自定义全局异常处理类

package sixkey.Exception.handler;

/**
 * ClassName:Handler
 * Package:sixkey.Exception
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/20 - 16:40
 * @Version:v1.0
 */

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import sixkey.Exception.SystemException;
import sixkey.utils.ResCodeEnum;
import sixkey.utils.ResResult;

/**
 * 统一异常处理
 */
@RestControllerAdvice //统一异常处理注解
@Slf4j
public class GlobalExceptionHandler {
    
    //此注解可以捕捉到SystemException抛出的异常
    @ExceptionHandler(SystemException.class)
    public ResResult systemExceptionHandler(SystemException systemException){
        log.info("出现了异常:",systemException);
        return ResResult.errorResult(systemException.getCode(),systemException.getMessage());
    }
    
    //对未知异常进行一个兜底
    @ExceptionHandler(Exception.class)
    public ResResult exceptionHandler(Exception exception){
        log.info("出现了异常:",exception);
        return ResResult.errorResult(ResCodeEnum.SYSTEM_ERROR.getCode(),exception.getMessage());
    }
}

7、SpringBoot参数校验

导入依赖

<!--参数校验-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

使用

package sixkey.domain.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
import java.util.UUID;

/**
 * ClassName:User
 * Package:sixkey.entity
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/19 - 23:20
 * @Version:v1.0
 */

/**
 * 系统用户类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user") //对应数据库的表
@Accessors(chain = true) //可以链式编写
public class User implements Serializable {

    @TableId // 主键
    private Integer id;

    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;

    @JsonIgnore //忽略前端传过来的值
    private String salt = UUID.randomUUID().toString().replaceAll("-","");

    @NotNull(message = "年龄不能为空")
    private Integer age;

    @NotBlank(message = "性别不能为空")
    private String sex;

    @TableLogic
    @JsonIgnore
    private Integer deleted; //逻辑删除

    @TableField(fill = FieldFill.INSERT) //插入时自动填充
    private Date createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE) //修改时自动填充
    private Date updateTime;
}

注意:字符串和int类型的属性所使用的注解不同,否则校验不匹配。

controller添加注解 并在参数前添加注解@Valid

//参数校验
@Validated
public class UserController {

    //已经通过@RequiredArgsConstructor此注解自动注入值
    private final IUserService userService;

    /**
     * 用户注册
     * @param user
     * @return
     */
    @ApiOperation(value = "用户注册", httpMethod = "POST",response = ResResult.class, notes = "用户注册")
    @PostMapping("/registry")
    public ResResult RegisterUser(@RequestBody @Valid User user){
        log.info("==== {}用户请求进来了 =====",user.getUsername());
        userService.RegisterUser(user);
        return ResResult.okResult();
    }

统一异常返回,这样前端好看

//参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResResult bindException(MethodArgumentNotValidException e){
        log.info("出现了异常:",e);
        return ResResult.errorResult(ResCodeEnum.SYSTEM_ERROR.getCode(),e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
    }
p95ABgP.jpgopen in new window
p95ABgP.jpg

8、集成Sa-Token

导入依赖

<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.34.0</version>
</dependency>

添加配置文件

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: swxToken
  # token有效期,单位s 默认30, -1代表永不过期 
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (true时允许一起登录,false时新登录挤掉旧登录) 
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (true时所有登录共用一个token,false时每次登录新建一个token) 
  is-share: true
  # token风格
  token-style: uuid
  # 是否输出操作日志 
  is-log: true

登录认证

完成数据库比对后,添加核心代码

//3.登录 StpUtil.login(user.getId());

/**
     * 用户登录
     * @param loginUserVo
     */
    @Override
    public void doLogin(LoginUserVo loginUserVo) throws UserNameNotFoundException, PasswordErrorException {
        //1.根据用户名查询是否有此用户
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername,loginUserVo.getUsername());
        User user = baseMapper.selectOne(wrapper);
        if(user == null){
            //用户名不存在
            throw new UserNameNotFoundException(ResCodeEnum.USERNAME_NOTFOUND);
        }

        //2.密码是否正确
        String salt = user.getSalt();
        String password = loginUserVo.getPassword();
        String MD5Password = DigestUtils.md5DigestAsHex((password + salt).getBytes());
        if(!MD5Password.equals(user.getPassword())){
            throw new PasswordErrorException(ResCodeEnum.LOGIN_ERROR);
        }

        //3.登录
        StpUtil.login(user.getId());

    }
}

判断当前用户是否是登录状态

StpUtil.isLogin();

退出登录

StpUtil.logout();

查看用户Token信息

StpUtil.getTokenInfo()

// 查询登录状态,浏览器访问: http://localhost:8081/users/isLogin
    @ApiOperation(value = "查询登录状态",httpMethod = "GET",response = String.class,notes = "查询登录状态")
    @GetMapping("/isLogin")
    public String isLogin() {
        return "当前会话是否登录:" + StpUtil.isLogin();
    }

    // 查询登录状态,浏览器访问: http://localhost:8081/users/isLogin
    @ApiOperation(value = "退出登录",httpMethod = "GET",response = ResResult.class,notes = "退出登录")
    @GetMapping("/loginOut")
    public ResResult loginOut() {
        StpUtil.logout();
        return ResResult.okResult();
    }

    // 查询Token信息  ---- http://localhost:8081/acc/tokenInfo
    @ApiOperation(value = "查询Token信息",httpMethod = "GET",response = ResResult.class,notes = "查询Token信息")
    @GetMapping("/tokenInfo")
    public ResResult tokenInfo() {
        return ResResult.okResult(StpUtil.getTokenInfo());
    }

权限认证:基于注解鉴权

  • @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
  • @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
  • @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
  • @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
  • @SaCheckBasic: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
  • @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
  • @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。

Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态 因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中

注册拦截器

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册 Sa-Token 拦截器,打开注解式鉴权功能
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }
}

获取当前账号权限码集合

/**
 * 自定义权限验证接口扩展
 */
@Component    // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {

    private final MenuMapper menuMapper;

    private final RoleMapper roleMapper;


    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return menuMapper.getMenuByUserId(loginId);
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return roleMapper.getRoleByUserId(loginId);
    }
}

sql查询语句

查询操作权限路径

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="sixkey.mapper.MenuMapper">

    <select id="getMenuByUserId" resultType="java.lang.String">
        select auth_url from sys_menu
        where id in(select menu_id from sys_role_menu
        where role_id = (select role_id from sys_user_role where user_id=#{loginId}))
    </select>
</mapper>

查询操作角色

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="sixkey.mapper.RoleMapper">

    <select id="getRoleByUserId" resultType="java.lang.String">
        SELECT role_name
        from sys_role
        WHERE id = (SELECT role_id from sys_user_role WHERE user_id = #{loginId});
    </select>
</mapper>

controller层添加注解

/**
     * 修改用户
     * @param user
     * @return
     */
    @SaCheckLogin
    @SaCheckRole("admin")
    @SaCheckPermission("system/user/update")
    @ApiOperation(value = "修改用户",httpMethod = "PUT",response = ResResult.class,notes = "修改用户")
    @PutMapping("/update")
    public ResResult updateUser(@RequestBody @Valid User user){
        // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
        StpUtil.checkLogin();
        log.info("修改用户请求进来了.............");
        userService.updateUser(user);
        return ResResult.okResult();
    }

疑问?为什么加个简单的注解就可以实现鉴权呢?

​ 因为我们在上面做了获取当前账号权限码集合,只要实现了StpInterface接口并做了查询,那么Sa-Token就帮我们做了一切。

9、短信验证登录

前期准备工作

登录阿里云官网—–>找到产品里面的短信服务,点进去,然后看到如下画面

p9ofYee.jpgopen in new window
p9ofYee.jpg

一路走

p9ofsOS.jpgopen in new window
p9ofsOS.jpg

添加签名

p9of5lV.jpgopen in new window
p9of5lV.jpg
p9ofoOU.jpgopen in new window
p9ofoOU.jpg

添加模板

p9ofBSP.jpgopen in new window
p9ofBSP.jpg
p9ofNod.jpgopen in new window
p9ofNod.jpg

获取AccessKey Id 和密码去

p9ofdJI.jpgopen in new window
p9ofdJI.jpg

不再截图

点进去AccessKey管理,点开始使用子用户AccessKey,然后创建用户,登录名随便填,访问方式填:OpenAPI 调用访问

点进去刚刚创建的用户里面即可得到 id和密码

java核心代码及工具类

导入maven依赖

<dependency>
      <groupId>com.aliyun</groupId>
      <artifactId>aliyun-java-sdk-core</artifactId>
      <version>4.5.16</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-dysmsapi -->
    <dependency>
      <groupId>com.aliyun</groupId>
      <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
      <version>2.1.0</version>
    </dependency>

两个工具类

/**
 * 短信发送工具类
 */
public class SMSUtils {

	/**
	 * 发送短信
	 * @param signName 签名
	 * @param templateCode 模板
	 * @param phoneNumbers 手机号
	 * @param param 参数
	 */
	public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
		DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");//填短信服务id和密码
		IAcsClient client = new DefaultAcsClient(profile);

		SendSmsRequest request = new SendSmsRequest();
		request.setSysRegionId("cn-hangzhou");
		request.setPhoneNumbers(phoneNumbers);
        //签名
		request.setSignName(signName);
        //模板code
		request.setTemplateCode(templateCode);
		request.setTemplateParam("{\"code\":\""+param+"\"}");
		try {
			SendSmsResponse response = client.getAcsResponse(request);
			System.out.println("短信发送成功");
		}catch (ClientException e) {
			e.printStackTrace();
		}
	}

}
/**
 * 随机生成验证码工具类
 */
public class ValidateCodeUtils {
    /**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code;
    }

    /**
     * 随机生成指定长度字符串验证码
     * @param length 长度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

具体业务代码实现

	@PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpServletRequest request){
        //获取手机号
        String phone = user.getPhone();
        if(!StringUtils.isEmpty(phone)){
            //生成随机的4位验证码
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            //调用阿里云提供的短信服务API完成发送短信
            //SMSUtils.sendMessage("","",phone,code);
            //需要将生成的验证码保存到Session
            request.getSession().setAttribute(phone,code);
            return R.success("短信发送成功!");
        }
        return R.error("短信发送失败!");
    }

10、邮箱验证码注册

导入依赖

<!--邮件发送-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mail</artifactId>
      <version>2.6.1</version>
    </dependency>

获取username 、 password

p9Oo04P.jpgopen in new window
p9Oo04P.jpg

配置文件

spring:
#发送邮件配置相关
# 配置邮件服务器的地址 smtp.qq.com
  mail:
    host: smtp.qq.com
    # 配置邮件服务器的端口(465或587)
    port: 465
    username: 2073958890@qq.com
    password: ievhywdhgrdvcddd
    default-encoding: utf-8
# SSL 连接配置
    properties:
      mail:
        smtp:
          socketFactory:
            class: javax.net.ssl.SSLSocketFactory
# 开启 debug,这样方便开发者查看邮件发送日志
        debug: true

注意:大坑

一定要配置SSL连接,不然代码对了也依然收不到邮件!!!

核心代码

//依赖注入
private final JavaMailSender javaMailSender;

@Transactional(rollbackFor = Exception.class)
    protected void sendEmail(String toEmail,String code) throws SystemException {
        try {
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
            //获取配置文件中的邮箱用户
            helper.setFrom(emailConfig.getUsername());
            helper.setTo(toEmail);

            EmailModelDto emailModelDto = redisComponent.getEmailModelDto();
            helper.setSubject(emailModelDto.getEmailTitle());
            helper.setText(String.format(emailModelDto.getEmailContent(),code));
            helper.setSentDate(new Date());
            javaMailSender.send(mimeMessage);
        }catch (Exception e){
            log.info("邮箱验证码发送失败!");
            throw new SystemException(ResCodeEnum.EMAIL_SEND_fail);
        }
    }

**
 * 邮箱格式配置类
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class EmailModelDto implements Serializable {
    private String emailTitle = "邮箱验证码";
    private String emailContent = "您好,您的邮箱验证码是:%s,15分钟有效!";
    //初始化用户使用空间,5M
    private Integer initUseSpace = 5;

    public String getEmailTitle() {
        return emailTitle;
    }

    public void setEmailTitle(String emailTitle) {
        this.emailTitle = emailTitle;
    }

    public String getEmailContent() {
        return emailContent;
    }

    public void setEmailContent(String emailContent) {
        this.emailContent = emailContent;
    }

    public Integer getInitUseSpace() {
        return initUseSpace;
    }

    public void setInitUseSpace(Integer initUseSpace) {
        this.initUseSpace = initUseSpace;
    }
}
/**
 * 初始化邮箱内容,并缓存到redis中
 */
@Component("RedisComponent")
public class RedisComponent {

    @Autowired
    private RedisUtils redisUtils;

    public EmailModelDto getEmailModelDto(){
        EmailModelDto emailModelDto = (EmailModelDto) redisUtils.get(Constants.REDIS_KEY_EMAIL_SETTING);
        if(emailModelDto == null){
            emailModelDto = new EmailModelDto();
            //保存到redis中
            redisUtils.set(Constants.REDIS_KEY_EMAIL_SETTING,emailModelDto);
        }
        return emailModelDto;
    }
}

邮箱注册结合到了redis,将邮箱内容缓存到了redis中

导入依赖

<!--redis -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
      <version>2.6.1</version>
    </dependency>

配置文件

#redis配置
  redis:
    host: localhost
    #password:
    port: 6379
    database: 0
    connect-timeout: 1800000

注意(坑):使用redis一定要将key 和 value 进行序列化,redis默认的序列化机制会乱码!!!

/**
 * redis序列化
 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化

        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

redis基本操作

/**
 * 将邮箱格式及内容保存到redis中
 */
@Slf4j
@Component("RedisUtils")
public class RedisUtils<V> {

    @Resource
    private RedisTemplate<String,V> redisTemplate;

    public V get(String key){
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    public boolean set(String key, V value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        }catch (Exception e){
            log.info("设置redisKey:{},value:{}失败!",key,value);
            return false;
        }
    }

    public boolean setTime(String key, V value, Long time){
        try {
            if(time > 0){
                redisTemplate.opsForValue().set(key,value,time,TimeUnit.SECONDS);
                return true;
            }else {
                set(key,value);
            }
            return true;
        }catch (Exception e){
            log.info("设置redisKey:{},value:{}失败!",key,value);
            return false;
        }
    }
}

总结:使用redis缓存时key 和 value要设置自己的序列化;邮箱注册时要进行SSL连接!!!

11、java生成验证码图片

工具类

package sixkey.utils;

/**
 * ClassName:CreateImageCode
 * Package:sixkey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/5/28 - 10:38
 * @Version:v1.0
 */

import org.springframework.web.bind.annotation.PathVariable;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.TileObserver;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;

/**
 * 生成验证码图片
 */
public class CreateImageCode {
    //图片宽带
    private int width = 160;
    //图片的高度
    private int height = 40;
    //验证码字符个数
    private int codeCount = 4;
    //验证码干扰线数
    private int lineCount = 20;
    //验证码
    private String code = null;
    //验证码图片Buffer
    private BufferedImage bufferedImage = null;
    Random random = new Random();

    public CreateImageCode(){
        creatImage();
    }

    public CreateImageCode(int width,int height){
        this.width = width;
        this.height = height;
        creatImage();
    }

    public CreateImageCode(int width,int height,int codeCount){
        this.width = width;
        this.height = height;
        this.codeCount = codeCount;
        creatImage();
    }

    public CreateImageCode(int width,int height,int codeCount,int lineCount){
        this.width = width;
        this.height = height;
        this.codeCount = codeCount;
        this.lineCount = lineCount;
        creatImage();
    }

    //生成图片
    private void creatImage(){
        //字体宽度
        int fontWidth = width / codeCount;
        //字体高度
        int fontHeight = height -5;
        int codeY = height - 8;

        //图像buffer
        bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
        Graphics g = bufferedImage.getGraphics();
        //设置背景颜色
        g.setColor(getRandColor(200,250));
        g.fillRect(0,0,width,height);
        //设置字体
        Font font = new Font("Fixedsys", Font.BOLD,fontHeight);
        g.setFont(font);

        //设置干扰线
        for(int i = 0; i< lineCount; i++){
            int xs = random.nextInt(width);
            int ys = random.nextInt(height);
            int xe = xs + random.nextInt(width);
            int ye = ys + random.nextInt(height);
            g.setColor(getRandColor(1,255));
            g.drawLine(xs,ys,xe,ye);
        }

        //添加噪点
        float yawpRate = 0.01f;
        int area = (int) (yawpRate * width * height);
        for(int i = 0; i < area; i++){
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            bufferedImage.setRGB(x,y,random.nextInt(255));
        }

        //得到随机字符
        String str1 = randomStr(codeCount);
        this.code = str1;
        for(int i = 0; i < codeCount; i++){
            String strRand = str1.substring(i,i + 1);
            g.setColor(getRandColor(1,255));
            g.drawString(strRand,i * fontWidth + 3, codeY);
        }
    }

    //得到随机字符
    private String randomStr(int n){
        String str1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
        String str2 = "";
        int len = str1.length() - 1;
        double r;
        for(int i = 0 ; i < n; i++){
            r = (Math.random()) * len;
            str2 = str2 + str1.charAt((int) r);
        }
        return str2;
    }

    //得到随机颜色
    private Color getRandColor(int fc,int bc){
        if(fc > 255) fc = 255;
        if(bc > 255) bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r,g,b);
    }

    //产生随机字体
    private Font getFont(int size){
        Random random = new Random();
        Font font[] = new Font[5];
        font[0] = new Font("Ravie",Font.PLAIN,size);
        font[1] = new Font("Antique Olive Compact",Font.PLAIN,size);
        font[2] = new Font("Fixedsys",Font.PLAIN,size);
        font[3] = new Font("Wide Latin",Font.PLAIN,size);
        font[4] = new Font("Gill Sans Ultra Bold",Font.PLAIN,size);
        return font[random.nextInt(5)];
    }

    public void write(OutputStream sos) throws IOException {
        ImageIO.write(bufferedImage,"png",sos);
        sos.close();
    }

    public BufferedImage getBuffImg(){
        return bufferedImage;
    }

    public String getCode(){
        return code.toLowerCase();
    }

}

使用

CreateImageCode vCode = new CreateImageCode(130, 38, 5, 10);

        //设置返回值格式
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setContentType("image/jpeg");
        String code = vCode.getCode();
        //判断登录方式
        if (type == null || type == Constants.ZERO) {
            //type=0是登录时发送的验证码
            session.setAttribute(Constants.CHECK_CODE_KEY,code);
        }else {
            //前端做了默认说明:type = 1 是注册时发送的验证码
            session.setAttribute(Constants.CHECK_CODE_KEY_EMAIL,code);
        }
        vCode.write(response.getOutputStream());

12、配置请求过滤器(Filter)

前后端未分离版

实现Filter类

/**
 * 过滤器注解urlPatterns = "/*"表示过滤掉所有请求
 */
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    //路径匹配器,支持通配符
    private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //1.获取当前的请求url
        String requestURI = request.getRequestURI();
        //以下是不需要处理的请求url,直接放行
        String urls[] = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };
        //2.判断本次请求是否需要处理
        boolean check = check(urls, requestURI);
        //3.如果不需要处理则直接放行
        if(check){
            log.info("本次请求不需要处理:{}",request.getRequestURI());
            filterChain.doFilter(request,response);
            return;
        }
        //4.查看登录状态,如果已登录则放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,id为:"+request.getSession().getAttribute("employee"));
            filterChain.doFilter(request,response);
            return;
        }
        log.info("用户未登录!");
        //5.如果未登录则返回未登录结果,通过输出流方式响应给前端
        //如果未登录,只要返回NOTLOGIN字符串,页面跳转由前端js来控制
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    /**
     * 判断本次请求是否需要处理封装方法
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls,String requestURI){
        for (String url : urls) {
            //如果match为true,说明当前请求路径为不需要处理的请求,则说明路径匹配上
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}

在启动类加@ServletComponentScan

@Slf4j//lombok提供的日志
@SpringBootApplication//启动类
@ServletComponentScan//开启过滤器
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目启动成功!");
    }
}

13、Mybatis-Plus分页查询

MybatisPlusInterceptorMybatisPlusInterceptor是一系列的实现InnerInterceptor的拦截器链,也可以理解为一个集合。可以包括如下的一些拦截器:

自动分页: PaginationInnerInterceptor(最常用) 多租户: TenantLineInnerInterceptor 动态表名: DynamicTableNameInnerInterceptor 乐观锁: OptimisticLockerInnerInterceptor sql性能规范: IllegalSQLInnerInterceptor 防止全表更新与删除: BlockAttackInnerInterceptor

分页插件配置类

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusPageConfig {
    @Bean//交给spring管理
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

分页查询

/**
     * 员工信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        log.info("page={},pageSize={},name={}",page,pageSize,name);
        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);
        //构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        //添加过滤条件(模糊查询)
        queryWrapper.like(!StringUtils.isEmpty(name),Employee::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //执行查询
        employeeService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

分页前端代码

<el-pagination
        class="pageList"
        :page-sizes="[10, 20, 30, 40]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="counts"
        :current-page.sync="page"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      ></el-pagination>

new Vue({
        el: '#member-app',
        data() {
          return {
             input: '',
             counts: 0,
             page: 1,
             pageSize: 10,
			}
		methods:{
			handleSizeChange (val) {
            	this.pageSize = val
            	this.init()
          		},
          	 handleCurrentChange (val) {
            	this.page = val
            	this.init()
          		}
			}

14、ThreadLocal使用

使用前景

因为自动填充公共字段时需要获取当前登录用户的Id,所以引入了ThreadLocal。

在学习ThreadLocal之前,我们需要确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,要确保以下所执行到的方法都是同一个线程:

1LoginCheckFilter的doFilter方法
2EmployeeController的update方法
3MyMetaObjectHandler的updateFill方法
可以使用以下代码查看当前线程Id
long id = ThreadLocal.currentThread().getId();

什么是ThreadLocal?

由JDK所提供。ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal为每一个线程提供单独的一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法:

public void set(T value) 设置当前线程的线程局部变量的值
public T get()           返回当前线程所对应的线程局部变量的值

具体使用:

我们可以在LoginCheckFilter的doFilter方法中获取当前登录的用户Id,并调用ThreadLocal的set方法设置当前线程的线程局部变量的值 ( 用户Id ) ,然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获取当前的线程所对应的线程局部变量的值 ( 用户Id )。

代码实现:

/**
 * 基于ThreadLocal封装工具类,用来保存和获取当前登录的用户Id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

在过滤器方法里面一旦用户登录就设置用户Id

//4.查看登录状态,如果已登录则放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,id为:"+request.getSession().getAttribute("employee"));
            Long empId = (Long) request.getSession().getAttribute("employee");
            //同一个线程保存用户Id
            BaseContext.setCurrentId(empId);
            filterChain.doFilter(request,response);
            return;
        }

15、文件上传、下载介绍

前端Form表单要求:

method="post" 采用post方式提交数据
enctype="multipart/form-data" 采用multipart格式上传文件
type="file" 使用input的file控件上传

后端代码实现:

properties文件配置:

#文件上传位置
reggie.path=D:/img/
/**
 * 文件上传、下载功能
 */
@RestController
@RequestMapping("/common")
public class CommonController {

    //获取配置文件中的路径
    @Value("${reggie.path}")
    private String basePath;

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file) throws IOException {
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会被删除。
        //获取原始文件名
        String originalFilename = file.getOriginalFilename();//abc.jpg
        //截取后缀.jpg
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

        //使用UUID重新生成文件名,防止上传同名文件被覆盖
        String finalFileName = UUID.randomUUID().toString() + suffix;//skfjd.jpg

        //创建一个目录对象
        File dir = new File(basePath);
        if(!dir.exists()){
            //此目录不存在则创建
            dir.mkdirs();
        }

        //将临时文件转存到指定位置
        file.transferTo(new File(basePath + finalFileName));

        return R.success(finalFileName);
    }
}

文件下载具体实现

	/**
     * 文件下载
     * @param name
     * @param response
     */
    @GetMapping("/download")
    public void downLoad(String name, HttpServletResponse response){
        //通过输入流读取指定文件中的File
        FileInputStream fileInputStream = null;
        ServletOutputStream outputStream = null;
        //设置下载格式
        response.setContentType("image/jpeg");
        try {
            fileInputStream = new FileInputStream(basePath + name);
            outputStream = response.getOutputStream();

            //读写文件
            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1 ){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        //关闭流
        try {
            fileInputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        try {
            outputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

16、路径分析

1.@PathVariable使用场景
path: http://localhost:8081/page/12
2.@RequestParam使用场景
path: http://localhost:8081/page?ids=12346545
3.@RequestBody使用场景
path: http://localhost:8081/page
data{
	id:12,
	name:'jack',
	phone:123465789
}

17、Maven聚合工程

​ 父工程 guigu-oa-parent 依赖版本

​ 子模块 common(公共模块)

​ common-util 核心工具类

​ service-util 业务模块工具类

​ 子模块 model 实体类

​ 子模块 service-oa 业务模块

项目依赖及配置

配置

#端口配置
server.port=8200

#配置数据源
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/guigu-oa?serverTimezone=GMT%2B8&useSSL=false&nullCatalogMeansCurrent=true&characterEncoding=utf-8
spring.datasource.druid.username=root
spring.datasource.druid.password=020708

#配置mybatis-plus
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath:com/guigu/mapper/xml/*.xml

#设置date格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#redis配置
spring.redis.host=82.157.234.124
spring.redis.port=6379
spring.redis.password=123456
spring.redis.database=0
spring.redis.timeout=1800000

#activiti配置
#    false:默认,数据库表不变,但是如果版本不对或者缺失表会抛出异常(生产使用)
#    true:表不存在,自动创建(开发使用)
#    create_drop: 启动时创建,关闭时删除表(测试使用)
#    drop_create: 启动时删除表,在创建表 (不需要手动关闭引擎)
spring.activiti.database-schema-update=true
#监测历史表是否存在,activities7默认不开启历史表
spring.activiti.db-history-used=true
#none:不保存任何历史数据,流程中这是最高效的
#activity:只保存流程实例和流程行为
#audit:除了activity,还保存全部的流程任务以及其属性,audit为history默认值
#full:除了audit、还保存其他全部流程相关的细节数据,包括一些流程参数
spring.activiti.history-level=full
#校验流程文件,默认校验resources下的process 文件夹的流程文件
spring.activiti.check-process-definitions=true

#微信推送配置
wechat.mpAppId=wx88a3b565c0b59c37
wechat.mpAppSecret=6ebce2277427da473f269d309f880874
# 授权回调获取用户信息接口地址
wechat.userInfoUrl=http://sixkey3.viphk.91tunnel.com/admin/wechat/userInfo

父项目pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.guigu</groupId>
  <artifactId>guigu-oa-parent</artifactId>
  <!--打包方式-->
  <packaging>pom</packaging>

  <version>1.0-SNAPSHOT</version>
  <modules>
    <module>common</module>
      <module>model</module>
    <module>service-oa</module>
  </modules>
  <!--父依赖-->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.6.RELEASE</version>
  </parent>
  <!--版本控制-->
  <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <java.version>1.8</java.version>
    <mybatis-plus.version>3.4.1</mybatis-plus.version>
    <mysql.version>8.0.30</mysql.version>
    <knife4j.version>3.0.3</knife4j.version>
    <jwt.version>0.9.1</jwt.version>
    <fastjson.version>2.0.21</fastjson.version>
  </properties>

  <!--配置dependencyManagement锁定依赖的版本-->
  <dependencyManagement>
    <dependencies>
      <!--mybatis-plus 持久层-->
      <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>${mybatis-plus.version}</version>
      </dependency>
      <!--mysql-->
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
      </dependency>
      <!--knife4j-->
      <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-spring-boot-starter</artifactId>
        <version>${knife4j.version}</version>
      </dependency>
      <!--jjwt-->
      <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>${jwt.version}</version>
      </dependency>
      <!--fastjson-->
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.19.1</version>
        <configuration>
          <skipTests>true</skipTests>    <!--默认关掉单元测试 -->
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

common pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>guigu-oa-parent</artifactId>
        <groupId>org.guigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.guigu</groupId>
    <artifactId>common</artifactId>
    <!--打包方式-->
    <packaging>pom</packaging>
    <modules>
        <module>service-util</module>
        <module>common-util</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

工具类common-util pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>common</artifactId>
        <groupId>com.guigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.atguigu</groupId>
    <artifactId>common-util</artifactId>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!--<scope>provided </scope>-->
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>
</project>

工具类service-util pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>common</artifactId>
        <groupId>com.guigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>service-util</artifactId>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>common-util</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.guigu</groupId>
            <artifactId>model</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--接口文档生成依赖-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>

    </dependencies>
</project>

实体类model pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>guigu-oa-parent</artifactId>
        <groupId>org.guigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.guigu</groupId>
    <artifactId>model</artifactId>
    <packaging>jar</packaging>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--lombok用来简化实体类-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <scope>provided </scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <scope>provided </scope>
        </dependency>
    </dependencies>
</project>

启动类service-oa pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>guigu-oa-parent</artifactId>
        <groupId>org.guigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.guigu</groupId>
    <artifactId>service-oa</artifactId>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.guigu</groupId>
            <artifactId>model</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.guigu</groupId>
            <artifactId>service-util</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>

        <!--引入activiti的springboot启动器 -->
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-spring-boot-starter</artifactId>
            <version>7.1.0.M6</version>
            <exclusions>
                <exclusion>
                    <artifactId>mybatis</artifactId>
                    <groupId>org.mybatis</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-mp</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <!-- 指定该Main Class为全局的唯一入口 这里是启动类的地址 -->
                    <mainClass>com.guigu.ServiceOaApplication</mainClass>
                    <layout>ZIP</layout>
                </configuration>
                <executions>
                    <execution>
                        <!--可以把依赖的包都打包到生成的Jar包中-->
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.yml</include>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes> <include>**/*.yml</include>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                    <include>**/*.png</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
</project>

18、随机生成验证码

package com.itheima.utils;

import java.util.Random;

/**
 * 随机生成验证码工具类
 */
public class ValidateCodeUtils {
    /**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code;
    }

    /**
     * 随机生成指定长度字符串验证码
     * @param length 长度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

19、整合RabbitMQ死信队列

7.4. 整合 springboot

7.4.1导入依赖

<dependencies>
 <!--RabbitMQ 依赖-->
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-amqp</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>fastjson</artifactId>
 <version>1.2.47</version>
 </dependency>
 <dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 </dependency>
 <!--swagger-->
 <dependency>
 <groupId>io.springfox</groupId>
 <artifactId>springfox-swagger2</artifactId>
 <version>2.9.2</version>
 </dependency>
 <dependency>
 <groupId>io.springfox</groupId>
 <artifactId>springfox-swagger-ui</artifactId>
 <version>2.9.2</version>
 </dependency>
 <!--RabbitMQ 测试依赖-->
 <dependency>
 <groupId>org.springframework.amqp</groupId>
 <artifactId>spring-rabbit-test</artifactId>
 <scope>test</scope>
 </dependency>
</dependencies>

7.4.2. 修改配置文件

spring:
  rabbitmq:
    host: 82.157.234.124
    port: 5672
    username: admin
    password: 123

7.4.3. 添加 Swagger 配置类

/**
 * swagger2配置类
 */
@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .useDefaultResponseMessages(false)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sixkey.controller"))
                .paths(PathSelectors.any())
                .build();

    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .description("RabbitMQ接口文档")
                .version("v1.1.0")
                .title("RabbitMQ接口文档")
                .build();
    }
}

7.5. 队列 TTL

7.5.1. 代码架构图

创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

pC9fQR1.jpgopen in new window
pC9fQR1.jpg
**7.5.2. 配置文件类代码 **
/**
 * TTL 队列 配置类
 */
@Configuration
public class TtlQueueConfig {
    //普通交换机名称
    public static final String X_EXCHANGE = "X";
    //普通队列名称
    public static final String QUEUE_A = "QA";
    //普通队列名称
    public static final String QUEUE_B = "QB";
    //死信交换机名称
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    //死信队列名称
    public static final String DEAD_LETTER_QUEUE = "QD";
    // 声明普通交换机 xExchange
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }
    // 声明死信交换机 xExchange
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }
    //声明普通队列 A ttl 为 10s 并绑定到对应的死信交换机
    @Bean("queueA")
    public Queue queueA(){
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列的 TTL
        args.put("x-message-ttl", 10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
    }
    // 声明队列 A 绑定 X 交换机
    @Bean
    public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }
    //声明普通队列 B ttl 为 40s 并绑定到对应的死信交换机
    @Bean("queueB")
    public Queue queueB(){
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列的 TTL
        args.put("x-message-ttl", 40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
    }
    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
    }
    //声明死信队列 QD
    @Bean("queueD")
    public Queue queueD(){
        return new Queue(DEAD_LETTER_QUEUE);
    }
    //声明死信队列 QD 绑定关系
    @Bean
    public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
                                        @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

生产者端

@Autowired
private RabbitTemplate rabbitTemplate;

@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable String message){
        log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
        rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
}

消费者端

/**
 * 延迟队列  消费者
 */
@Slf4j
@Component
public class DeadLetterQueueConsumer {
    //监听死信队列
    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
    }
}

发起一个请求 http://localhost:8080/ttl/sendMsg/嘻嘻嘻

pC94g8s.jpgopen in new window
pC94g8s.jpg

第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息, 然后被消费掉,这样一个延时队列就打造完成了。

不过,如果这样使用的话,岂不是**每增加一个新的时间需求,就要新增一个队列,**这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然 后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?

7.6. 延时队列优化

7.6.1. 代码架构图

在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间

pC95QRs.jpgopen in new window
pC95QRs.jpg
**7.6.2. 配置文件类代码 **
//死信队列优化,引入队列C
	public static final String QUEUE_C = "QC";
//优化延迟队列,引入队列C
    //声明队列 C 死信交换机
    @Bean("queueC")
    public Queue queueC(){
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //没有声明 TTL 属性
        return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
    }
    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }
7.6.3. 消息生产者代码
//优化延迟队列
    @GetMapping("sendExpirationMsg/{message}/{ttlTime}")
    public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
        rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
            //设置发送消息延迟时间
            correlationData.getMessageProperties().setExpiration(ttlTime);
            return correlationData;
        });
        log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
    }

发起请求

​ 延迟20s

  • http://localhost:8080/ttl/sendExpirationMsg/你好 1/20000

延迟2s

  • http://localhost:8080/ttl/sendExpirationMsg/你好 2/2000

效果图

pC9oKvn.jpgopen in new window
pC9oKvn.jpg

缺点:需要排队

看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消 息可能并不会按时“死亡“,因为 **RabbitMQ 只会检查第一个消息是否过期,**如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

7.7. Rabbitmq 插件弥补延迟队列需要排队缺点

弥补缺点

上文中提到的问题,确实是一个问题,如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间 及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题。

7.7.1. 安装延时队列插件

在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载 rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。 进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ

①、将下载好的插件上传到rabbitmq安装目录下

②、将插件复制到插件文件夹下

​ cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins

③、安装插件

​ rabbitmq-plugins enable rabbitmq_delayed_message_exchange

④、重启rabbitmq

​ systemctl restart rabbitmq–server

查看是否生效

pC9TVqx.jpgopen in new window
pC9TVqx.jpg
7.7.2. 代码架构图

在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:

pC9T8sI.jpgopen in new window
pC9T8sI.jpg
pC9T3QA.jpgopen in new window
pC9T3QA.jpg
7.7.3. 配置文件类代码

在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才 投递到目标队列中。

/**
 * 基于插件的 延迟队列配置:队列、交换机、routingKey
 */
@Configuration
public class DelayedQueueConfig {
    //延迟队列名称
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    //延迟交换机名称
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    //延迟routingKey名称
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
    
    //声明延迟队列
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }
    
    //自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机的类型
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
                args);
    }
    
    //将队列、交换机、routingKey进行绑定
    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
                                       @Qualifier("delayedExchange") CustomExchange
                                               delayedExchange) {
        return
                BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}
7.7.4. 消息生产者代码
//发送消息,基于插件的延迟队列
    @GetMapping("sendDelayMsg/{message}/{delayTime}")
    public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
        //设置交换机名称,routingKey名称,发送的消息
        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, message,
                correlationData ->{
                    correlationData.getMessageProperties().setDelay(delayTime);
                    return correlationData;
                });
        log.info(" 当 前 时 间 : {}, 发送一条延迟 {} 毫秒的信息给队列 delayed.queue:{}", new
                Date(),delayTime, message);
    }
7.7.5. 消息消费者代码
/**
 * 基于插件的 延迟队列  消费者
 */
@Slf4j
@Component
public class DelayedQueueConsumer {
    //监听死信队列
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveDelayedQueue(Message message){
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到延时队列的消息:{}", new Date(), msg);
    }
}

发起请求:

​ 延迟20s

  • http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000

延迟2s

  • http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000
pC9HmCD.jpgopen in new window
pC9HmCD.jpg

解决

第二个消息被先消费掉了,符合预期

7.8. 总结

延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正 确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为 单个节点挂掉导致延时队列不可用或者消息丢失。

当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景

20、集成阿里云OSS图片上传

1、需要一些配置,首先登录阿里云,开通对象存储oss

2、需要如下几个值:

aliyun:
 oss:
   file:
     endpoint: oss-xxxxx.aliyuncs.com
     keyid: LTAI5tGSrn4xxxxxx
     keysecret: sXW8K7HPD9uJxxxxxx
     bucketname: xxxx

以上这些值都是从阿里云获取的。

注意:创建用户后记得打开权限:

导入依赖

<!--   阿里云oss依赖     -->
    <dependency>
      <groupId>com.aliyun.oss</groupId>
      <artifactId>aliyun-sdk-oss</artifactId>
      <version>3.10.2</version>
    </dependency>
    <!--  日期工具栏依赖  -->
    <dependency>
      <groupId>joda-time</groupId>
      <artifactId>joda-time</artifactId>
      <version>2.9.9</version>
    </dependency>

配置文件

# 阿里云OSS
# 不同服务器地址不同
aliyun:
  oss:
    file:
      endpoint: oss-xxx.aliyuncs.com
      keyid: LTAI5tGSxxxxxx
      keysecret: sXW8K7Hxxxxx
      bucketname: sixxxx

编写一个配置类,属性注入

/**
 * 阿里云oss图片上传文件配置
 */
@Data
@Component
public class OssConfig implements InitializingBean {

    // 读取配置文件中的内容
    @Value("${aliyun.oss.file.endpoint}")
    private String endpoint;
    @Value("${aliyun.oss.file.keyid}")
    private String keyId;
    @Value("${aliyun.oss.file.keysecret}")
    private String keySecret;
    @Value("${aliyun.oss.file.bucketname}")
    private String bucketName;

    // 在spring生命周期中 实例化->生成对象->属性填充后会进行afterPropertiesSet方法
    // 定义空开常用变量
    public static String END_POINT;
    public static String KEY_ID;
    public static String KEY_SECRET;
    public static String BUCKET_NAME;

    @Override
    public void afterPropertiesSet() throws Exception {
        END_POINT = endpoint;
        KEY_ID = keyId;
        KEY_SECRET = keySecret;
        BUCKET_NAME = bucketName;
    }
}

Controller层

/**
 * 阿里云OSS图片上传
 */
@Api("阿里云OSS图片上传")
@RestController
@RequiredArgsConstructor
public class FileUploadController {
    private final IFileUploadService fileUploadService;

    @ApiOperation(value = "图片上传", httpMethod = "POST",response = ResResult.class, notes = "图片上传")
    @PostMapping("/upload")
    public ResResult uploadFile(MultipartFile file){
        String url = fileUploadService.uploadFileAvatar(file);
        System.out.println("========url=========="  + url);
        return ResResult.okResult(url);
    }
    
    @ApiOperation(value = "删除图片", httpMethod = "POST",response = ResResult.class, notes = "删除图片")
    @PostMapping("/delete")
    public ResResult deleteFile(@RequestParam("filePath") String filePath){
        fileUploadService.deleteFile(filePath);
        return ResResult.okResult();
    }
}

实现层

/**
 * ClassName:FileUploadServiceImpl
 * Package:sixkey.service.impl
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/5 - 23:59
 * @Version:v1.0
 */
/**
 * 阿里云OSS图片上传
 */
@Service
public class FileUploadServiceImpl implements IFileUploadService {

    // 从工具类中获取对象
    private String endpoint = OssConfig.END_POINT;
    private String accessKeyId = OssConfig.KEY_ID;
    private String accessKeySecret = OssConfig.KEY_SECRET;
    private String bucketName = OssConfig.BUCKET_NAME;

    // 上传头像到oss
    @Override
    public String uploadFileAvatar(MultipartFile file) {
        try {
            // 创建OSS实例
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
            // 获取上传文件的输入流
            InputStream inputStream = file.getInputStream();
            // 获取文件原始名称
            String filename = file.getOriginalFilename();
            // 完善1、 --> 在文件名中添加唯一值
            String uuid = UUID.randomUUID().toString().replace("-", "");
            filename = uuid + filename;
            // 完善2、 --> 把文件按照日期分类
            String datePath = new DateTime().toString("yyyy/MM/dd");
            // 拼接时间 yyyy/MM/dd/filename
            filename = datePath + "/" + filename;
            //System.out.println(filename);
            // 调用oss方法实现上传
            // 1、bucketName 2、上传到oss文件路径和文件名称 3、文件的输入流
            ossClient.putObject(bucketName, filename, inputStream);
            // 获取url地址(根据阿里云oss中的图片实例拼接字符串) 拼接url字符串
            // https://edu-leader.oss-cn-beijing.aliyuncs.com/%E4%BB%96.jpg
            String url = "https://"+bucketName+"."+endpoint+"/"+filename;
            // 关闭oss
            ossClient.shutdown();
            return url;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public void deleteFile(String filePath) {
        String https = "https://";
        String replaceFilePath = https + bucketName + "." + endpoint + "/";
        // 创建OSS实例
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        // 根据BucketName,filePath删除文件:filePath只能是文件路径,
        //比如我图片路径为:2023/6/6/xxxxxxxxxxxxxxxxxxxxxxxxx.jpg , filePath = 2023/6/6/xxxxxxxxxxxxxxxxxxxxxxxxx.jpg 
        ossClient.deleteObject(bucketName, filePath.replace(replaceFilePath,""));
        ossClient.shutdown();
    }
}

特别注意

这里是通过apifox测试的

这里的参数名file一定要和Controller层的参数名file一致,类型也要是file类型,不然后端一直报null异常

完整测试

判断是否成功阿里云页面

21、权限管理

1、根据用户查询角色、查询所有角色

①、数据库设计

用户表

pCkpD29.jpgopen in new window
pCkpD29.jpg

角色表

pCkprvR.jpgopen in new window
pCkprvR.jpg

用户角色表

pCkpB8J.jpgopen in new window
pCkpB8J.jpg

②、根据用户获取角色数据、所有角色数据

controller层

/**
     * url: `${api_name}/toAssign/${adminId}`,
     * method: 'get'
     */
    @ApiOperation(value = "根据用户获取角色数据")
    @GetMapping("/toAssign/{adminId}")
    public Result toAssign(@PathVariable("adminId") Long adminId){
        //map集合中包含两部分:1、所有角色列表;2、用户已有的角色列表
        Map<String,Object> map = roleService.findRoleByUserId(adminId);
        return Result.ok(map);
    }

Impl层

@Override
    public Map<String, Object> findRoleByUserId(Long adminId) {
        /*
           整体思路:
           1、第一去角色表查询到所有角色数据。
           2、第二通过用户id查询用户角色表获取此用户所对应的所有数据,因为一个用户可能同时拥有多个角色,
              所以返回的类型自然是一个集合list来封装。
           3、第三通过第二步查询到的用户角色数据,通过stream()流获取特定数据:角色id,因为有多个,所以封装在集合中。
           4、将第一步和第三步查询到的角色id做匹配,如果匹配成功,则说明此角色已经分配给此用户,
              将此角色添加到一个新的集合中(因为有多个角色)。
           5、最后将所有角色和已分配给用户的角色这两部分数据封装到map中进行返回。
         */
        //查询所有的角色:条件为Null
        List<Role> allRoleList = baseMapper.selectList(null);
        //构造查询条件:通过用户id查询用户角色关系表的所有数据
        LambdaQueryWrapper<AdminRole> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(AdminRole::getAdminId,adminId);
        //一个用户id可能会有多个角色id的值,所有将返回一个集合List
        List<AdminRole> adminRoleList = adminRoleService.list(wrapper);
        //通过查询得到的adminRoleList获取角色id
        List<Long> roleList = adminRoleList.stream()
                                            .map(item -> item.getRoleId())
                                            .collect(Collectors.toList());
        //通过查询得到的roleList查看roleList是否包含在所有角色id allRoleList中
        //如果存在,此角色id就是已经为用户分配过的角色,最后封装在一个新的list中放到map中进行返回
        List<Role> assignedRoleList = new ArrayList<>();
        for (Role role : allRoleList) {
            //判断roleList是否在allRoleList中
            if(roleList.contains(role.getId())){
                //如果存在,此角色id就是已经为用户分配过的角色,最后将角色封装在一个新的角色list中
                assignedRoleList.add(role);
            }
        }
        //封装到map,进行返回
        Map<String,Object> map = new HashMap<>();
        //所有角色
        map.put("allRolesList",allRoleList);
        //已分配的角色
        map.put("assignRoles",assignedRoleList);
        return map;
    }

2、根据用户分配角色

controller层

/**
     * url: `${api_name}/doAssign`,
     * method: 'post',
     * params: {
     *      adminId,
     *      roleId
     *   }
     * @param adminId
     * @param roleId
     * @return
     */
    @ApiOperation(value = "根据用户分配角色")
    @PostMapping("/doAssign")
    public Result doAssign(@RequestParam Long adminId,@RequestParam Long[] roleId) {
        roleService.saveUserRoleRealtionShip(adminId,roleId);
        return Result.ok();
    }

Impl层

/**
     * 根据用户分配角色
     * @param adminId
     * @param roleIds
     */
    @Override
    public void saveUserRoleRealtionShip(Long adminId, Long[] roleIds) {
        //分配之前将原来在用户角色表中分配过的角色数据删除,通过用户id
        LambdaQueryWrapper<AdminRole> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(AdminRole::getAdminId,adminId);
        adminRoleService.remove(wrapper);

        //重新分配
        //遍历角色id(roleIds),得到每个角色id,拿着每个角色id + adminId添加到用户角色表中。
        /*for (Long roleId : roleIds) {
            AdminRole adminRole = new AdminRole();
            adminRole.setRoleId(roleId);
            adminRole.setAdminId(adminId);
            adminRoleService.save(adminRole);
        }*/
        //以上方法每添加一个就要重新save一次
        //优化
        List<AdminRole> adminRoles = new ArrayList<>();
        for (Long roleId : roleIds) {
            AdminRole adminRole = new AdminRole();
            adminRole.setRoleId(roleId);
            adminRole.setAdminId(adminId);
            adminRoles.add(adminRole);
        }
        //批量添加
        adminRoleService.saveBatch(adminRoles);
    }

3、构建菜单树

表结构

操作shequ-acl数据库中的permission表

重点字段说明:

type:菜单类型,分为:目录、菜单与按钮

​ 目录:一个分类(可理解为一级菜单)、目录下级节点可以为目录与菜单

​ 菜单:一个具体页面,菜单的下级节点只能是按钮

​ 按钮:页面上的功能

​ to_code:对应路由里面的路由地址path

​ code:对应菜单的功能权限标识

4.2.2 示例数据
4.2.3 页面效果

构建菜单树返回

controller层

@ApiOperation(value = "获取菜单列表")
    @GetMapping
    public Result index() {
        List<Permission> list = permissionService.queryAllMenu();
        return Result.ok(list);
    }

impl层

/**
     * 获取菜单列表
     * @return
     */
    @Override
    public List<Permission> queryAllMenu() {
        //查询所有菜单
        List<Permission> allPermissionList = baseMapper.selectList(null);
        //获取菜单树
        List<Permission> result = PermissionUtils.buildPermissions(allPermissionList);
        return result;
    }

构建菜单树工具类

//构建菜单树
    public static List<Permission> buildPermissions(List<Permission> allPermissionList) {
        //封装最终返回list集合
        List<Permission> treeList = new ArrayList<>();
        //遍历所有菜单,得到第一层数据(父菜单),Pid = 0
        for (Permission permission : allPermissionList) {
            //判断是否是父菜单
            if(permission.getPid() == 0){
                //设置层级为第一层
                permission.setLevel(1);
                //从第一层开始往下找
                treeList.add(findChildren(permission,allPermissionList));
            }
        }
        return treeList;
    }

    //递归找子菜单
    //permission:上层节点,从这里往下面找子节点
    //allPermissionList: 所有菜单数据(包括子菜单和父菜单)
    private static Permission findChildren(Permission permission, List<Permission> allPermissionList) {
        //设置菜单
        permission.setChildren(new ArrayList<Permission>());
        //遍历所有菜单
        //判断当前id和Pid是否相等,封装,递归往下找
        for (Permission item : allPermissionList) {
            //判断当前id和Pid是否相等
            if(permission.getId().longValue() == item.getPid().longValue()){
                //设置层级
                int level = permission.getLevel() + 1;
                item.setLevel(level);
                if(permission.getChildren() == null){
                    permission.setChildren(new ArrayList<>());
                }
                //封装下一层数据
                permission.getChildren().add(findChildren(item,allPermissionList));
            }
        }
        return permission;
    }

递归删除菜单

cotroller层

@ApiOperation(value = "递归删除菜单")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        permissionService.removeChildById(id);
        return Result.ok();
    }

impl层

/**
     * 递归删除子菜单
     * @param id
     */
    @Override
    public void removeChildById(Long id) {
        //idList中有要删除的所有菜单id
        List<Long> idList = new ArrayList<>();

        //根据当前id,获取到当前菜单下的所有子菜单
        //如果子菜单下还有子菜单,都要获取到
        //递归
        this.getAllPermissionId(id,idList);
        //设置当前菜单,因为当前菜单也要删除
        idList.add(id);
        //调用批量删除方法进行删除
        baseMapper.deleteBatchIds(idList);
    }

工具类

//重点:递归查找当前菜单下面的所有子菜单
    //第一个参数是当前菜单id
    //第二个参数最终封装list集合,包含所有菜单id
    private void getAllPermissionId(Long id, List<Long> idList) {

        LambdaQueryWrapper<Permission> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Permission::getPid,id);
        //获取当前id下的所有子菜单
        List<Permission> permissionsList = baseMapper.selectList(wrapper);
        //获取当前id下的所有子菜单id封装到idList中
        permissionsList.stream().forEach(item ->{
            //封装菜单id到idList中
           idList.add(item.getId());
           //递归
            this.getAllPermissionId(item.getId(),idList);
        });
    }

22、代码生成工具类

1、导入依赖

<dependencies>
        <!--代码生成时一直报slf4j错误,最后加上这个依赖就可以用了,此包不一定要引入,除非报错-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.2</version>
            <type>jar</type>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>
    </dependencies>

2、工具类

package com.sixKey;


import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

public class CodeGet {

    public static void main(String[] args) {

        // 1、创建代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 2、全局配置
        // 全局配置
        //工程路径:D:\SpringBoot_Vue_project\sixKey-yx-parent
        //common 和 generator是模块名
        GlobalConfig gc = new GlobalConfig();
        gc.setOutputDir("D:\\SpringBoot_Vue_project\\sixKey-yx-parent\\common\\generator"+"/src/main/java");

        gc.setServiceName("%sService");	//去掉Service接口的首字母I
        gc.setAuthor("sixKey");
        gc.setOpen(false);
        mpg.setGlobalConfig(gc);

        // 3、数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/shequ-sys?serverTimezone=GMT%2B8&useSSL=false");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("020708");
        dsc.setDbType(DbType.MYSQL);
        mpg.setDataSource(dsc);

        // 4、包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.sixKey");
        //设置最后要在哪个包下生成这些代码,也可以不设置
        //pc.setModuleName("sys"); //包名
        pc.setController("controller");
        pc.setService("service");
        pc.setMapper("mapper");
        mpg.setPackageInfo(pc);

        // 5、策略配置
        StrategyConfig strategy = new StrategyConfig();
        //配置数据中要生成的表
        strategy.setInclude("region","ware","region_ware");

        strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略

        strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
        strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作

        strategy.setRestControllerStyle(true); //restful api风格控制器
        strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符

        mpg.setStrategy(strategy);

        // 6、执行
        mpg.execute();
        System.out.println("========代码生成成功!========");
    }
}

23、整合微信登录

思路

获取openId 和 密钥

配置文件

wx:
  open:
    # 小程序微信公众平台appId
    app_id: wx15d273ce67xxxxxx
    # 小程序微信公众平台api秘钥
    app_secret: f7b0f0b4eb4b78dc4baxxxxx

工具类

package com.sixKey.utils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class HttpClientUtils {

    public static final int connTimeout=10000;
    public static final int readTimeout=10000;
    public static final String charset="UTF-8";
    private static HttpClient client = null;

    static {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(128);
        cm.setDefaultMaxPerRoute(128);
        client = HttpClients.custom().setConnectionManager(cm).build();
    }

    public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String get(String url) throws Exception {
        return get(url, charset, null, null);
    }

    public static String get(String url, String charset) throws Exception {
        return get(url, charset, connTimeout, readTimeout);
    }

    /**
     * 发送一个 Post 请求, 使用指定的字符集编码.
     *
     * @param url
     * @param body RequestBody
     * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
     * @param charset 编码
     * @param connTimeout 建立链接超时时间,毫秒.
     * @param readTimeout 响应超时时间,毫秒.
     * @return ResponseBody, 使用指定的字符集编码.
     * @throws ConnectTimeoutException 建立链接超时异常
     * @throws SocketTimeoutException  响应超时
     * @throws Exception
     */
    public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
            throws ConnectTimeoutException, SocketTimeoutException, Exception {
        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        String result = "";
        try {
            if (StringUtils.isNotBlank(body)) {
                HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
                post.setEntity(entity);
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());

            HttpResponse res;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }

    /**
     * 提交form表单
     *
     * @param url
     * @param params
     * @param connTimeout
     * @param readTimeout
     * @return
     * @throws ConnectTimeoutException
     * @throws SocketTimeoutException
     * @throws Exception
     */
    public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        try {
            if (params != null && !params.isEmpty()) {
                List<NameValuePair> formParams = new ArrayList<NameValuePair>();
                Set<Entry<String, String>> entrySet = params.entrySet();
                for (Entry<String, String> entry : entrySet) {
                    formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                }
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
                post.setEntity(entity);
            }

            if (headers != null && !headers.isEmpty()) {
                for (Entry<String, String> entry : headers.entrySet()) {
                    post.addHeader(entry.getKey(), entry.getValue());
                }
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());
            HttpResponse res = null;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null
                    && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
    }

    /**
     * 发送一个 GET 请求
     *
     * @param url
     * @param charset
     * @param connTimeout  建立链接超时时间,毫秒.
     * @param readTimeout  响应超时时间,毫秒.
     * @return
     * @throws ConnectTimeoutException   建立链接超时
     * @throws SocketTimeoutException   响应超时
     * @throws Exception
     */
    public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
            throws ConnectTimeoutException,SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpGet get = new HttpGet(url);
        String result = "";
        try {
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            get.setConfig(customReqConf.build());

            HttpResponse res = null;

            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(get);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(get);
            }
            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            get.releaseConnection();
            if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }
    /**
     * 从 response 里获取 charset
     *
     * @param ressponse
     * @return
     */
    @SuppressWarnings("unused")
    private static String getCharsetFromResponse(HttpResponse ressponse) {
        // Content-Type:text/html; charset=GBK
        if (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
            String contentType = ressponse.getEntity().getContentType().getValue();
            if (contentType.contains("charset=")) {
                return contentType.substring(contentType.indexOf("charset=") + 8);
            }
        }
        return null;
    }
    /**
     * 创建 SSL连接
     * @return
     * @throws GeneralSecurityException
     */
    private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
        try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
                    return true;
                }
            }).build();
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {

                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }
                @Override
                public void verify(String host, SSLSocket ssl)
                        throws IOException {
                }
                @Override
                public void verify(String host, X509Certificate cert)
                        throws SSLException {
                }
                @Override
                public void verify(String host, String[] cns,
                                   String[] subjectAlts) throws SSLException {
                }
            });
            return HttpClients.custom().setSSLSocketFactory(sslsf).build();
        } catch (GeneralSecurityException e) {
            throw e;
        }
    }
}

package com.sixKey.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * ClassName:ConstantPropertiesUtil
 * Package:com.sixKey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/10 - 19:42
 * @Version:v1.0
 */

/**
 * 获取配置文件中的值
 */
@Component
public class ConstantPropertiesUtil implements InitializingBean {
    @Value("${wx.open.app_id}")
    private String appId;

    @Value("${wx.open.app_secret}")
    private String appSecret;

    public static String WX_OPEN_APP_ID;
    public static String WX_OPEN_APP_SECRET;

    @Override
    public void afterPropertiesSet() throws Exception {
        WX_OPEN_APP_ID = appId;
        WX_OPEN_APP_SECRET = appSecret;
    }
}

实战

package com.sixKey.controller;

import com.alibaba.fastjson.JSONObject;
import com.sixKey.constant.RedisConst;
import com.sixKey.enums.UserType;
import com.sixKey.exception.ServiceException;
import com.sixKey.model.user.User;
import com.sixKey.result.Result;
import com.sixKey.result.ResultCodeEnum;
import com.sixKey.service.UserService;
import com.sixKey.utils.ConstantPropertiesUtil;
import com.sixKey.utils.HttpClientUtils;
import com.sixKey.utils.JwtHelper;
import com.sixKey.vo.user.LeaderAddressVo;
import com.sixKey.vo.user.UserLoginVo;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * ClassName:WeiXinApiController
 * Package:com.sixKey.controller
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/10 - 21:07
 * @Version:v1.0
 */
@Slf4j
@RestController
@RequestMapping("/api/user/weixin")
@RequiredArgsConstructor
public class WeiXinApiController {

    private final UserService userService;

    private final RedisTemplate redisTemplate;

    /**
     * 微信授权登录借接口
     * @param code
     * @return
     */
    @ApiOperation(value = "微信登录获取openid(小程序)")
    @GetMapping("/wxLogin/{code}")
    public Result callback(@PathVariable String code){
        //1、得到微信返回的code临时票据值
        log.info("微信返回的code临时票据值:{}",code);
        if (StringUtils.isEmpty(code)) {
            //如果code为空,则报非法回调请求异常
            throw new ServiceException(ResultCodeEnum.ILLEGAL_CALLBACK_REQUEST_ERROR);
        }
        //2、拿着code + 小程序id + 小程序密钥,请求微信接口服务
        //// 使用HttpClient工具请求微信接口服务
        String wxOpenAppId = ConstantPropertiesUtil.WX_OPEN_APP_ID;
        String wxOpenAppSecret = ConstantPropertiesUtil.WX_OPEN_APP_SECRET;
        //向微信发送get请求
        //拼接请求地址加参数
        //%s即为占位符
        StringBuffer baseAccessTokenUrl = new StringBuffer()
                .append("https://api.weixin.qq.com/sns/jscode2session")
                .append("?appid=%s")
                .append("&secret=%s")
                .append("&js_code=%s")
                .append("&grant_type=authorization_code");
        //给占位符设置值
        String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
                ConstantPropertiesUtil.WX_OPEN_APP_ID,
                ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
                code);

        //使用以上url路径通过HttpClient发送请求
        String result = null;
        try {
            result = HttpClientUtils.get(accessTokenUrl);
        }catch (Exception e){
            throw new ServiceException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
        }
        //3、请求微信接口服务后会返回两个值 session_key 和 openId
        //// openId是每个微信用户的唯一标识
        JSONObject resultJson = JSONObject.parseObject(result);
        //如果成功,则errcode为null
        if(resultJson.getString("errcode") != null){
            throw new ServiceException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
        }
        log.info("使用code换取的access_token结果 = {}",resultJson);
        String accessToken = resultJson.getString("session_key");
        String openId = resultJson.getString("openid");
        //4、添加微信用户信息到数据库里面
        //// 查询user表
        //// 判断是否是第一次使用微信授权登录:如何判断?通过openId查询是否有数据可查
        User user = userService.getByOpenid(openId);
        //如果是第一次登录,需要设置一些值
        if(null == user){
            user = new User();
            user.setOpenId(openId);
            user.setNickName(openId);
            user.setPhotoUrl("");
            user.setUserType(UserType.USER);
            user.setIsNew(0);
            userService.save(user);
        }
        //5、根据userId查询提货点和团长信息
        //// 提货点:查询user表得到userId,通过userId查询user_deliver表得到leaderId
        //// 团长:通过leaderId查询leader表获取leader信息
        LeaderAddressVo leaderAddressVo = userService.getLeaderAddressVoByUserId(user.getId());
        //6、使用JWT工具根据userId和userName生成token字符串
        String token = JwtHelper.createToken(user.getId(), user.getNickName());
        //7、获取当前登录用户信息,放到redis中,设置有效时间
        UserLoginVo userLoginVo = userService.getUserLoginVo(user.getId());
        redisTemplate.opsForValue().set(RedisConst.USER_LOGIN_KEY_PREFIX + user.getId(),
                                        userLoginVo,
                                        RedisConst.USERKEY_TIMEOUT,
                                        TimeUnit.DAYS);
        //8、需要返回的数据封装到map中进行返回
        Map<String,Object> map = new HashMap<>();
        map.put("user",user);
        map.put("leaderAddressVo",leaderAddressVo);
        map.put("token",token);
        return Result.ok(map);
    }
}

24、SpringBoot整合Aop实现日志记录、权限校验、参数校验

实例1、实现日志记录

1、自定义注解
package sixkey.annotation;

import java.lang.annotation.*;

/**
 * ClassName:LogAnnotation
 * Package:sixkey.annotation
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 10:39
 * @Version:v1.0
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {

    String model() default "";

    String operation() default "";
}

2、Aspect切面
package sixkey.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.HttpRequestHandlerServlet;
import sixkey.annotation.LogAnnotation;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

import static org.springframework.web.context.request.RequestContextHolder.resetRequestAttributes;

/**
 * ClassName:LogAspect
 * Package:sixkey.aop
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 10:40
 * @Version:v1.0
 */
@Component
@Aspect
@Slf4j
public class LogAspect {
    //切点
    @Pointcut("@annotation(sixkey.annotation.LogAnnotation)")
    public void pointcut(){};

    //环绕通知
    @Around("pointcut()")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        //记录方法执行时间
        long begin = System.currentTimeMillis();
        //方法开始执行
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        long time = end - begin;
        //开始记录日志
        recordLog(joinPoint,time);
        return result;
    }

    private void recordLog(ProceedingJoinPoint joinPoint,long time){
        //获取方法有日志注解的方法名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
        log.info("==================开始记录日志=================");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String uri = request.getRequestURI();
        String remoteAddr = request.getRemoteAddr();
        String requestMethod = request.getMethod();
        log.info("===============请求URI=============" + uri);
        log.info("===============请求远程地址=============" + remoteAddr);
        log.info("===============请求方法=============" + requestMethod);
        //获取注解的参数
        log.info("model:{}",annotation.model());
        log.info("operation:{}",annotation.operation());
        log.info("方法执行时间:" + time + "ms");
    }
}
3、使用
@PostMapping("/upload")
@LogAnnotation(model="文件",operation="文件上传")
public ResResult uploadFile(MultipartFile file){
   String url = fileUploadService.uploadFileAvatar(file);
        return ResResult.okResult(url);
}

实例2

所有的POST请求被调用前在控制台输出被调用的提示

1、Aspect切面
package sixkey.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * ClassName:PostAspect
 * Package:sixkey.aop
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 13:44
 * @Version:v1.0
 */
@Component
@Aspect
public class PostAspect {

    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void pointcut(){};

    //前置通知,在目标方法被执行前执行
    @Before("pointcut()")
    public void postAdvice(){
        System.out.println("post请求的advice被触发...........");
    }
}

2、使用
    @PostMapping("/upload")
    public ResResult uploadFile(MultipartFile file){
        String url = fileUploadService.uploadFileAvatar(file);
        return ResResult.okResult(url);
    }

实例3、参数校验、权限校验

1、自定义注解
package sixkey.annotation;

import java.lang.annotation.*;

/**
 * ClassName:PermissionAnnotation
 * Package:sixkey.annotation
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 14:01
 * @Version:v1.0
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation {
    String value() default "";
}

2、Aspect切面类
package sixkey.aop;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import sixkey.utils.ResCodeEnum;
import sixkey.utils.ResResult;

/**
 * ClassName:PermissionAdvice
 * Package:sixkey.aop
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 14:11
 * @Version:v1.0
 */
@Component
@Aspect
@Order(0) //数值越小优先级越高
public class PermissionAdvice {

    //切点
    @Pointcut("@annotation(sixkey.annotation.PermissionAnnotation)")
    public void pointcut(){};

    @Around("pointcut()")
    public Object permissionCheckFirst(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("==========参数校验============");
        //获取目标方法上的参数
        Object[] args = joinPoint.getArgs();
        String ids = args[0].toString();
        long id = Long.parseLong(ids);
        String username = args[1].toString();
        System.out.println("============id值===========" + id);
        System.out.println("============username值===========" + username);
        //校验逻辑
        if(id < 0){
            return ResResult.errorResult(ResCodeEnum.USERID_ERR);
        }
        if(StringUtils.isEmpty(username)){
            return ResResult.errorResult(ResCodeEnum.USERNAME_NOT_NULL);
        }
        if(!username.equals("admin")){
            return ResResult.errorResult(ResCodeEnum.NOT_ADMIN);
        }
        return joinPoint.proceed();
    }
}

3、使用
    @GetMapping("/getById/{id}/{username}")
    @PermissionAnnotation(value = "校验id是否合以及用户是否是管理员") //aop权限校验
    public ResResult getById(@PathVariable("id") Long id,@PathVariable("username") String username){
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getId,id);
        wrapper.eq(User::getUsername,username);
        User user = userService.getOne(wrapper);
        return ResResult.okResult(user);
    }

实例4、用户操作权限校验

1、自定义注解
package sixkey.annotation;

import java.lang.annotation.*;

/**
 * ClassName:UserPermissionAnnotation
 * Package:sixkey.annotation
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 17:28
 * @Version:v1.0
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserPermissionAnnotation {
    String value() default "";
}

2、Aspect切面类
package sixkey.aop;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import sixkey.annotation.UserPermissionAnnotation;
import sixkey.domain.entity.Permiss;
import sixkey.service.IPermissService;
import sixkey.service.IUserPermissService;
import sixkey.utils.ResCodeEnum;
import sixkey.utils.ResResult;

import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;

/**
 * ClassName:UserPermissionAdvice
 * Package:sixkey.aop
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 17:30
 * @Version:v1.0
 */
@Component
@Aspect
@Order(0)
public class UserPermissionAdvice {

    @Autowired
    private IPermissService permissService;
    @Autowired
    private IUserPermissService userPermissService;

    //切点
    @Pointcut("@annotation(sixkey.annotation.UserPermissionAnnotation)")
    public void pointcut(){};

    @Around("pointcut()")
    public Object UserPermissionAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("=============用户权限校验开始===========");
        Object[] args = joinPoint.getArgs();
        //获取登录用户的id
        String userIds = args[0].toString();
        long userId = Long.parseLong(userIds);
        //通过UserId查询权限用户对应的一个或多个权限id
        List<Integer> permissionIdList = userPermissService.getPermissIdByUserId(userId);
        //查询权限表,获取用户对应的权限
        List<Permiss> permisses = permissService.listByIds(permissionIdList);
        //获取到用户对应的权限的值
        List<String> permissions = permisses.stream().map(item -> item.getPermission()).collect(Collectors.toList());
        //获取注解对应的权限值
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //获取方法上的注解
        UserPermissionAnnotation annotation = method.getAnnotation(UserPermissionAnnotation.class);
        //获取注解的内容
        String value = annotation.value();
        boolean flag = false;
        for (String permission : permissions) {
            if(permission.equals(value)){
                flag = true;
            }
        }
        //抛出无权限异常
        if(!flag){
            return ResResult.errorResult(ResCodeEnum.NOT_PERMISSION);
        }
        return joinPoint.proceed();
    }
}

3、使用
package sixkey.controller;

/**
 * ClassName:UserPermissionController
 * Package:sixkey.controller
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/6/22 - 17:12
 * @Version:v1.0
 */

import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import sixkey.Exception.UserNameExistException;
import sixkey.annotation.PermissionAnnotation;
import sixkey.annotation.UserPermissionAnnotation;
import sixkey.domain.entity.User;
import sixkey.domain.vo.UserVo;
import sixkey.service.IUserService;
import sixkey.utils.ResResult;

import javax.validation.Valid;
import java.util.List;

/**
 * 用户权限校验
 */
@Api(tags = "用户权限校验")
@RestController
@RequestMapping("/user/permission")
@RequiredArgsConstructor
public class UserPermissionController {

    private final IUserService userService;

    //增加用户
    @ApiOperation(value = "添加用户")
    @PostMapping("/add/{userId}")
    @UserPermissionAnnotation(value = "user:add")
    public ResResult add(@PathVariable("userId") Long userId, @RequestBody User user) throws UserNameExistException {
        userService.RegisterUser(user);
        return ResResult.okResult();
    }

    //修改用户
    @ApiOperation(value = "修改用户",httpMethod = "PUT",response = ResResult.class,notes = "修改用户")
    @PutMapping("/update/{userId}")
    @UserPermissionAnnotation(value = "user:update")
    public ResResult updateUser(@PathVariable("userId") Long userId,@RequestBody User user){
        userService.updateUser(user);
        return ResResult.okResult();
    }

    //获取用户
    @ApiOperation(value = "获取用户")
    @GetMapping("/getById/{userId}")
    @UserPermissionAnnotation(value = "user:get")
    public ResResult getById(@PathVariable("userId") Long userId){
        List<User> userList = userService.list();
        return ResResult.okResult(userList);
    }

    //删除用户
    @ApiOperation(value = "删除用户",httpMethod = "DELETE",response = ResResult.class,notes = "删除用户")
    @DeleteMapping("/delete/{userId}/{id}")
    @UserPermissionAnnotation(value = "user:delete")
    public ResResult updateUser(@PathVariable("userId") Long userId,@PathVariable("id") Long id){
        userService.removeById(id);
        return ResResult.okResult();
    }
}

25、使用selenium爬虫获取数据

下载浏览器驱动

http://chromedriver.storage.googleapis.com/index.html

版本下载浏览器版本对应的即可

将软件解压在一个无中文路径下最好。

导入依赖

<dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>4.10.0</version>
</dependency>
package sixkey.utils;

/**
 * 爬取数据脚本
 */
@Component
public class ItScriptWorkerUtils {

    public List<Content> execute(){
        //设置浏览器驱动
        System.setProperty("webdriver.chrome.driver","D:\\chromeDriver\\chromedriver.exe");
        ChromeOptions chromeOptions = new ChromeOptions();
        //默认只允许本地连接,解决403报错问题
        chromeOptions.addArguments("--remote-allow-origins=*");
        //初始化web测试驱动
        ChromeDriver chromeDriver = new ChromeDriver(chromeOptions);
        //打开入口页(要爬取数据网页的url)
        String url = "http://www.itpub.net/";
        chromeDriver.get(url);
        try {
            //线程休眠5s,确保已经打开网页
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //开始爬取数据(获取网页前十页最新消息)
        int count = 1;
        List<Content> contents = new ArrayList<>();
        while (count <= 10){
            //通过css选择器获取html
            List<WebElement> elements = chromeDriver.findElements(By.cssSelector("div.right-box h4 a"));
            elements.forEach(item ->{
                Content content = new Content();
                String title = item.getText();
                String src = item.getAttribute("href");
                content.setTitle(title);
                content.setUrl(src);
                contents.add(content);
            });

            //遇到下一页按钮时自动点击事件
            List<WebElement> pages = chromeDriver.findElements(By.cssSelector("div.page a"));
            WebElement nextPage = pages.get(pages.size() - 1);
            //触发下一页按钮单机事件
            nextPage.click();
            count++;
            try {
                //线程休眠5s,确保已经下一页
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //关闭驱动
        chromeDriver.close();
        return contents;
    }
}

26、优雅配置接口响应和统一异常处理

Spirng 提供了 ResponseBodyAdvice 接口,支持在消息转换器执行转换之前,对接口的返回结果进行处理,再结合 @ControllerAdvice 注解即可轻松支持上述功能。

1、接口响应和统一

准备三个类

ResponseCodeEnums.java:响应类型枚举类

package sixkey.utils;

/**
 * ClassName:ResponseCodeEnums
 * Package:sixkey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/7/12 - 13:20
 * @Version:v1.0
 */

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;

/**
 * 响应类型枚举类
 */
@Getter
@AllArgsConstructor
public enum ResponseCodeEnums {

    SUCCESS(200, "success"),
    FAIL(500, "failed"),

    HTTP_STATUS_200(200, "ok"),
    HTTP_STATUS_400(400, "request error"),
    HTTP_STATUS_401(401, "no authentication"),
    HTTP_STATUS_403(403, "no authorities"),
    HTTP_STATUS_500(500, "server error");

    private final int code;
    private final String message;
}

ResponseInfo.java:响应数据封装类

package sixkey.utils;

/**
 * ClassName:ResponseInfo
 * Package:sixkey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/7/12 - 13:26
 * @Version:v1.0
 */

import lombok.Data;

/**
 * 响应类
 */
@Data
public class ResponseInfo<T> {
    /**
     * 状态码
     */
    protected int code;

    /**
     * 响应信息
     */
    protected String message;

    /**
     * 返回数据
     */
    private T data;

    public static <T> ResponseInfo<T> success() {
        return new ResponseInfo<>();
    }

    public static <T> ResponseInfo<T> success(T data) {
        return new ResponseInfo<>(data);
    }

    public static <T> ResponseInfo<T> fail(String message) {
        return new ResponseInfo<>(ResponseCodeEnums.FAIL.getCode(), message);
    }

    public ResponseInfo() {
        this.code = ResponseCodeEnums.SUCCESS.getCode();
        this.message = ResponseCodeEnums.SUCCESS.getMessage();
    }

    public ResponseInfo(ResponseCodeEnums statusEnums) {
        this.code = statusEnums.getCode();
        this.message = statusEnums.getMessage();
    }

    /**
     * 若没有数据返回,可以人为指定状态码和提示信息
     */
    public ResponseInfo(int code, String msg) {
        this.code = code;
        this.message = msg;
    }

    /**
     * 有数据返回时,状态码为200,默认提示信息为“操作成功!”
     */
    public ResponseInfo(T data) {
        this.data = data;
        this.code = ResponseCodeEnums.SUCCESS.getCode();
        this.message = ResponseCodeEnums.SUCCESS.getMessage();
    }

    /**
     * 有数据返回,状态码为 200,人为指定提示信息
     */
    public ResponseInfo(T data, String msg) {
        this.data = data;
        this.code = ResponseCodeEnums.SUCCESS.getCode();
        this.message = msg;
    }
}

ResponseResultHandler:响应信息增强类

package sixkey.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * ClassName:ResponseResultHandler
 * Package:sixkey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/7/12 - 14:21
 * @Version:v1.0
 */

/**
 * 响应信息增强类
 */
@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        log.info("supports:{}", returnType.getDeclaringClass().getName());
        /**
         * 排除swagger-ui请求返回数据增强
         */
        return !returnType.getDeclaringClass().getName().contains("springfox");
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 如果是ResponseInfo数据类型就直接返回
        if (body instanceof ResponseInfo) {
            return body;
        }
        // 如果是空,则返回成功
        else if (body == null) {
            return ResponseInfo.success();
        }
        // 如果是异常类型就直接返回
        else if (body instanceof Exception) {
            return body;
        }
        // 如果是String类型则直接返回String类型
        else if (body instanceof String) {
            return body;
        }
        // 返回封装后的数据
        else {
            return ResponseInfo.success(body);
        }
    }
}

测试类

@RestController
@RequiredArgsConstructor
public class TestController {

    private final IUserService userService;

    @GetMapping("/test")
    public void test(){}

    //根据id获取用户
    @ApiOperation(value = "根据id获取用户")
    @GetMapping("/getById/{id}")
    public ResponseInfo getById(@PathVariable("id") Long id){
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getId,id);
        User user = userService.getOne(wrapper);
        return ResponseInfo.success(user);
    }
}

响应展示

2、统一异常处理

@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {

  /**
   * 参数格式异常处理
   */
  @ExceptionHandler({IllegalArgumentException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ResponseInfo<String> badRequestException(IllegalArgumentException ex) {
    log.error("参数格式不合法:{}", ex.getMessage());
    return new ResponseInfo<>(HttpStatus.BAD_REQUEST.value() + "", "参数格式不符!");
  }

  /**
   * 权限不足异常处理
   */
  @ExceptionHandler({AccessDeniedException.class})
  @ResponseStatus(HttpStatus.FORBIDDEN)
  public ResponseInfo<String> badRequestException(AccessDeniedException ex) {
    return new ResponseInfo<>(HttpStatus.FORBIDDEN.value() + "", ex.getMessage());
  }

  /**
   * 参数缺失异常处理
   */
  @ExceptionHandler({MissingServletRequestParameterException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ResponseInfo<String> badRequestException(Exception ex) {
    return new ResponseInfo<>(HttpStatus.BAD_REQUEST.value() + "", "缺少必填参数!");
  }

  /**
   * 空指针异常
   */
  @ExceptionHandler(NullPointerException.class)
  @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseInfo<String> handleTypeMismatchException(NullPointerException ex) {
    log.error("空指针异常,{}", ex.getMessage());
    return ResponseInfo.fail("空指针异常");
  }

  @ExceptionHandler(Exception.class)
  @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseInfo<String> handleUnexpectedServer(Exception ex) {
    log.error("系统异常:", ex);
    return ResponseInfo.fail("系统发生异常,请联系管理员");
  }

  /**
   * 系统异常处理
   */
  @ExceptionHandler(Throwable.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseInfo<String> exception(Throwable throwable) {
    log.error("系统异常", throwable);
    return new ResponseInfo<>(HttpStatus.INTERNAL_SERVER_ERROR.value() + "系统异常,请联系管理员!");
  }
}

测试

@GetMapping("/test")
    public void test(){
        int i = 1/0;
    }

展示

自定义异常类

只需继承RuntimeException即可

/**
 * 自定义异常类
 */
public class TestException extends RuntimeException{
}

在全局异常处理类中设置

@RestControllerAdvice
public class ExceptionHandlerAdvice {
    @ExceptionHandler(TestException.class)
    public ResponseInfo<String> exception(TestException e){
        return ResponseInfo.fail("测试系统异常4");
    }
}

在需要抛出异常的地方正常抛出即可

@GetMapping("/test")
    public void test() throws SystemException {
        int i = 1 / 1;
        if(i == 1){
            throw new TestException();
        }
    }

27、SpringBoot日志使用

1、获取日志对象

import org.slf4j.Logger;  //要导这个包


@RestController
@RequiredArgsConstructor
public class TestController {
    //获取日志对象(当前类的日志)
    private static final Logger log = LoggerFactory.getLogger(TestController.class);

    private final IUserService userService;

    @GetMapping("/test")
    public void test() throws SystemException {
        log.trace("我是trace");
        log.debug("我是debug");
        log.info("我是info");
        log.warn("我是warn");
        log.error("我是error");
        }
    }

有几个注意点,第一个是日志对象Logger的包是 slf4j,Spring Boot 中内置了日志框架 Slf4j,可以让开发人员使用一致的API来调用不同的日志记录框架;第二个,一般每个类都有自己的日志文件,getLogger方法参数推荐为当前这个类,并且这个日志文件不能轻易修改,所以是private static final

可以看到只打印了三个日志,这是因为日志有级别:

日志级别的种类:

SpringBoot日志的级别用于控制输出日志的详细程度。

每种不同的日志等级对应一组不同的日志信息,级别越高,输出的日志信息就越详细。各种日志级别的含义如下:

  • trace:微量,少许的意思,级别最低;
  • debug:需要调试时候的关键信息打印;
  • info:普通的打印信息(默认⽇志级别);
  • warn:警告,不影响使用,但需要注意的问题;
  • error:错误信息,级别较⾼的错误⽇志信息;
  • fatal:致命的,因为代码异常导致程序退出执⾏的事件。   SpringBoot中日志级别从低到高依次为:trace < debug < info < warn < error < fatal,默认的级别是info。

日志的输出规则:当前的级别以及比当前级别高的日志才能输出(不会输出fatal),上面的 5 种不同的方法分别对应不同的日志级别,注意,没有fatal()方法,因为当出现fatal级别的时候程序就会被终止。

这也就是上面写了5个方法,只打印了3个的原因。

2、自定义日志的级别:

logging:
  level:
    root: error #日志等级是error ; root表示根路径

现在可以看到:只打印了error以及error以上的日志

以上这是对根路径设置级别,还可以分别对不同的目录进行同时设置。

logging:
  level:
    sixkey:
      controller: trace #对控制层的日志设置了trace级别的日志,两者不会冲突。

可以看到打印了所有的日志

3、日志的持久化

配置文件

logging:
  file:
    path: D:\\logging\\

不用自己创建这个路径,会自动创建。

可以看到已经自动创建了日志文件

文件内容

它会默认在目录中创建一个文件,文件里面装了你自己写的日志内容。那么问题来了,我多次访问接口方法,它会覆盖掉这里面的内容吗?

答案是不会的,它会在后面追加。

4、配置文件名

可以进一步精确到文件名。

logging:
  file:
    name: D:\logging\mySpring.log

现在还有一个问题,如果程序运行时间周期很长(比如生产环境),那么日志一定是非常多的,用一个文件来存储显然不现实,那么如何让它自动地分成多个文件呢?

可以设置日志文件存储大小的最大容量

logging:
  file:
    name: D:\logging\mySpring.log
  logback:
    rollingpolicy:
      max-file-size: 1KB #设置日志大小的最大容量(一般不会这么小,这里用于演示)

这个配置项用于指定一个日志文件的最大大小,支持的单位包括 KB、MB、GB 等。,当日志文件达到指定大小后,将自动创建一个新的日志文件来继续记录日志信息,我以上面的代码为例,我经过多次请求后:

更多的属性请看官方文档常见应用程序属性(spring.io)

5、借助lombok输出日志

只需加上一个注解即可

@Slf4j
@RestController
@RequiredArgsConstructor
public class TestController {
    //不再需要获取日志对象(当前类的日志)
    //private static final Logger log = LoggerFactory.getLogger(TestController.class);

    private final IUserService userService;

    @GetMapping("/test")
    public void test() throws SystemException {
        log.trace("我是trace");
        log.debug("我是debug");
        log.info("我是info");
        log.warn("我是warn");
        log.error("我是error");
    }
}

日志输出路径配置文件那些都不变。

28、SpringBoot自定义注解 + 异步 + 观察者模式实现日志保存

1、前期准备

导入依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.16</version>
</dependency>

<!--jdbc-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

yml配置

server:
  port: 8200
spring:
  application:
    name: swx
  datasource:
    url: jdbc:mysql://localhost:3306/sixkey?characterEncoding=UTF-8&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: '020708'
    type: com.alibaba.druid.pool.DruidDataSource

数据库表设计,这里只为了演示,具体可自行设计

CREATE TABLE `sys_log`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '模块标题',
  `business_type` int(2) NULL DEFAULT 0 COMMENT '业务类型(0其它 1新增 2修改 3删除)',
  `method` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '方法名称',
  `request_method` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求方式',
  `oper_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '操作人员',
  `oper_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求URL',
  `oper_ip` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '主机地址',
  `oper_time` datetime(0) NULL DEFAULT NULL COMMENT '操作时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1585197503834284034 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志记录' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

实体类

/**
 * <p>
 * 操作日志记录
 * </p>
 *
 * @author sixkey
 * @since 2023-07-13
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_log")
@ApiModel(value="Log对象", description="操作日志记录")
public class Log implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "日志主键")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "模块标题")
    private String title;

    @ApiModelProperty(value = "业务类型(0其它 1新增 2修改 3删除)")
    private Integer businessType;

    @ApiModelProperty(value = "方法名称")
    private String method;

    @ApiModelProperty(value = "请求方式")
    private String requestMethod;

    @ApiModelProperty(value = "操作人员")
    private String operName;

    @ApiModelProperty(value = "请求URL")
    private String operUrl;

    @ApiModelProperty(value = "主机地址")
    private String operIp;

    @ApiModelProperty(value = "操作时间")
    private Date operTime;
}

2、主要功能

大体思路: 先手写一个注解--->切面来进行获取要保存的数据--->一个发布者来发布要保存的数据--->一个监听者监听后保存(异步)

①、编写注解
/**
 * ClassName:Log
 * Package:sixkey.annotation
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/7/13 - 1:41
 * @Version:v1.0
 */
@Target(ElementType.METHOD) //注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) //修饰注解的生命周期
public @interface Log {

    String value() default "";

    /**
     * 模块
     */
    String title() default "测试模块";

    /**
     * 功能
     */
    BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}

②、编写业务类型枚举类
package sixkey.Enums;

/**
 * ClassName:BusinessTypeEnum
 * Package:sixkey.Enums
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/7/13 - 1:44
 * @Version:v1.0
 */

/**
 * 业务类型枚举
 */
public enum BusinessTypeEnum {

    /**
     * 其它
     */
    OTHER(0,"其它"),

    /**
     * 新增
     */
    INSERT(1,"新增"),

    /**
     * 修改
     */
    UPDATE(2,"修改"),

    /**
     * 删除
     */
    DELETE(3,"删除");

    private Integer code;

    private String message;

    BusinessTypeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

③、编写切面
/**
 * ClassName:SysLogAspect
 * Package:sixkey.aop
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/7/13 - 1:49
 * @Version:v1.0
 */
@Slf4j
@Aspect
@Component
public class SysLogAspect {

    @Pointcut("@annotation(sixkey.annotation.Log)")
    public void pointcut(){}

    @Autowired
    private EventPublistener eventPublistener;

    /**
     * 在切点之后切入
     */
    @After("pointcut()")
    public void deAfter(JoinPoint joinPoint){
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        //获取方法上的注解
        Log logAnnotation = method.getAnnotation(Log.class);

        //获取系统基本信息

         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String requestMethod = request.getMethod();
        String url = request.getRequestURI();
        String ip = IpUtils.getIpAddr(request);
        sixkey.domain.entity.Log sysLog = new sixkey.domain.entity.Log();
        sysLog.setBusinessType(logAnnotation.businessType().getCode());
        sysLog.setTitle(logAnnotation.title());
        sysLog.setRequestMethod(requestMethod);
        sysLog.setOperIp(ip);
        sysLog.setOperUrl(url);
        //从登录中token获取登录人员信息即可
        sysLog.setOperName("我是测试人员");
        sysLog.setOperTime(LocalDateTime.now());
        //发布消息
        eventPublistener.publishEvent(sysLog);
        log.info("========日志发送成功,内容为:{}==========",sysLog);
    }
}

④、Ip工具类
/**
 * ClassName:IpUtils
 * Package:sixkey.utils
 * Description
 *
 * @Author:@wenxueshi
 * @Create:2023/7/13 - 2:08
 * @Version:v1.0
 */

/**
 * 获取系统信息工具类
 */
public class IpUtils {
    /**
     * 获取客户端IP
     *
     * @param request 请求对象
     * @return IP地址
     */
    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
    }


    /**
     * 从多级反向代理中获得第一个非unknown IP地址
     *
     * @param ip 获得的IP地址
     * @return 第一个非unknown IP地址
     */
    public static String getMultistageReverseProxyIp(String ip) {
        // 多级反向代理检测
        if (ip != null && ip.indexOf(",") > 0) {
            final String[] ips = ip.trim().split(",");
            for (String subIp : ips) {
                if (false == isUnknown(subIp)) {
                    ip = subIp;
                    break;
                }
            }
        }
        return ip;
    }

    /**
     * 检测给定字符串是否为未知,多用于检测HTTP请求相关
     *
     * @param checkString 被检测的字符串
     * @return 是否未知
     */
    public static boolean isUnknown(String checkString) {
        return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
    }
}

⑤、事件发布

事件发布是由ApplicationContext对象进行发布的,直接注入使用即可! 使用观察者模式的目的:为了业务逻辑之间的解耦,提高可扩展性。 这种模式在spring和springboot底层是经常出现的,大家可以去看看。 发布者只需要关注发布消息,监听者只需要监听自己需要的,不管谁发的,符合自己监听条件即可

/**
 * 事件发布者
 */
@Component
public class EventPublistener {

    @Autowired
    private ApplicationContext applicationContext;

    //发布事件
    public void publishEvent(Log log){
        applicationContext.publishEvent(log);
    }
}
⑥、监听者

@Async:单独开启一个新线程去保存,提高效率! @EventListener:监听

/**
 * 事件监听者
 */
@Slf4j
@Component
public class MyEventListener {

    @Autowired
    private ILogService logService;

    @Async //开启线程池异步
    @EventListener //开启监听
    public void saveLog(Log sysLog){
        log.info("=========即将异步保存到数据库中=========");
        logService.save(sysLog);
    }
}
⑦、设置线程池

上面会存在一个问题,并发上来后就会出现线程突然打满,导致OOM。 所以在阿里开发规范说到: 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方 式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。 3)ScheduledThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

编写自定义线程池PoolConfig :

/**
 * 线程池配置
 */
@Configuration
public class PoolConfig {
    
    @Bean
    public ThreadPoolExecutor asyncExecutor(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
                10,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(30),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        return threadPoolExecutor;
    }
}

上面就是直接使用**@Async("asyncExecutor")**就可以直接引用了!!

启动类上添加:@EnableAsync,不然@Async不会生效的!!

⑧、测试
	@Log(title = "测试玩呢",businessType = BusinessTypeEnum.INSERT)
    @GetMapping("/test")
    public void test() throws SystemException {
        log.info("测试日志保存到数据库是否成功");
    }

数据库展示