LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

SpringBoot趣味实战课

2024/10/21 后端

Swagger + Mariadb + Hibernate 实现极简CRUD

application.yaml
spring:
  application:
    name: Pluminary
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MariaDB103Dialect
  springdoc:
    api-docs:
      path: /v3/api-docs
    swagger-ui:
      path: /swagger-ui.html

  server:
    port: 8080
    servlet:
      context-path: /springboot
      session:
        timeout: 60
  debug: true
com/pcy/Swagger/SwaggerConfig.java
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**") //这里是扫描包
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
com/pcy/service/UserRepository.java //【这个是持久化接口 实现CRUD】
package com.pcy.service;

import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Integer> {
}
com/pcy/controller/UserController.java
package com.pcy.controller;

import com.pcy.dao.User;
import com.pcy.service.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功获取用户信息"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @GetMapping("/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }

    @Operation(summary = "创建用户", description = "创建一个新的用户")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功创建用户"),
            @ApiResponse(responseCode = "400", description = "无效的输入")
    })
    @PostMapping
    public User create(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "更新用户", description = "更新用户信息")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功更新用户信息"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @PutMapping
    public User update(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "删除用户", description = "根据ID删除用户")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功删除用户"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @DeleteMapping("/{id}")
    public void delete(@PathVariable int id) {
        userRepository.deleteById(id);
    }
}
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pcy</groupId>
    <artifactId>Pluminary</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Pluminary</name>
    <description>Pluminary</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <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>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>mysql</groupId>-->
<!--            <artifactId>mysql-connector-java</artifactId>-->
<!--            <version>8.0.33</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>2.7.4</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

增加分页、排序

com/pcy/controller/UserController.java
@Operation(summary = "获取用户列表", description = "获取用户列表")
    @GetMapping
    public Page<User> list(@RequestParam(defaultValue = "id") String property,
 @RequestParam(defaultValue = "ASC")Sort.Direction direction,
 @RequestParam(defaultValue = "0") Integer page,
 @RequestParam(defaultValue = "10") Integer pageSize) {
    Pageable pageable = PageRequest.of(page, pageSize, direction, property);
        return userRepository.findAll(pageable);
    }

根据姓名查用户

com/pcy/controller/UserController.java    
    @Operation(summary = "根据姓名查用户",description = "根据姓名查用户")
    @GetMapping("/name")
    public List<User> getByName(String name){
        return userRepository.findByNameContaining(name);
    }
com/pcy/service/UserRepository.java
package com.pcy.service;

import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserRepository extends JpaRepository<User,Integer> {
    List<User> findByNameContaining(String name);
}

根据生日查用户、删除User表

com/pcy/controller/UserController.java
@Operation(summary = "根据生日获取用户信息①",description = "根据生日获取用户信息①")
    @GetMapping("/birthdayOne")
    public List<User> getBirthDayOne(LocalDate birthDay){
        return userRepository.findByBirthDay(birthDay);
    }

    @Operation(summary = "根据生日获取用户信息②",description = "根据生日获取用户信息②")
    @GetMapping("/birthdayTwo")
    public List<User> getBirthDayTwo(LocalDate birthDay){
        return userRepository.findByBirthDayNative(birthDay);
    }

    @Operation(summary = "删除User",description = "删除User")
    @GetMapping("/delete")
    public void delete(){
        userRepository.delete();
    }
com/pcy/service/UserRepository.java
@Query("SELECT u FROM User u WHERE u.birthday=?1")
    List<User> findByBirthDay(LocalDate birthDay);

    @Query(value = "SELECT * FROM user WHERE birth_day =:birthDay",nativeQuery = true)
    List<User> findByBirthDayNative(LocalDate birthDay);

    @Modifying
    @Transactional
    @Query(value = "DELETE FROM User")
    int delete();

增加审计

com/pcy/MallApplication.java //【增加@EnableJpaAuditing】
package com.pcy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}
com/pcy/dao/BaseEntity.java //【没有必要为每个实体类都编写 直接封装导一个类 User去继承】
package com.pcy.dao;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Data
@MappedSuperclass
//该注解用于监听实体类,在save、update之后的状态
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    @CreatedBy
    @Column(updatable = false)
    private String creator;

    @LastModifiedBy
    private String modifier;

    @CreatedDate
    @Column(updatable = false) //不可修改的
    private LocalDateTime createTime;

    @LastModifiedDate
    private LocalDateTime updateTime;
}
com/pcy/dao/User.java //【增加@EqualsAndHashCode 与 extends BaseEntity】
@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
public class User extends BaseEntity{
    @Id
//    @Schema(description = "用户ID")
//    @NotBlank(message = "Id不能为空")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    ...
}
com/pcy/service/impl/AuditorAwareImpl.java
package com.pcy.service.impl;

import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // 添加一个随机数
        return Optional.of("管理员"+(int)(Math.random()));
    }
}

引入Mybatis-Plus + FreeMarker

pom.xml
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.4.2</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
//根据你提供的实体类BaseEntity和User,我为你设计了一个基于MyBatis-Plus 3.5.x版本的代码生成器MysqlGenerator,它将自动生成与这些实体类相关的代码,如Mapper、Service、Controller等。以下是生成器的代码示例
【仅供查看学习 实际代码爆红无法导入】
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.builder.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Property;
import com.baomidou.mybatisplus.generator.keywords.MySqlKeyWordsHandler;

import java.util.Collections;

public class MysqlGenerator {

    // 项目路径
    private static final String PROJECT_PATH = System.getProperty("user.dir");
    // 输出路径
    private static final String OUTPUT_DIR = PROJECT_PATH + "/src/main/java";
    // 作者
    private static final String AUTHOR = "YourName";
    // 包名
    private static final String BASE_PACKAGE = "com.pcy";
    // 数据源配置
    private static final String DATABASE_URL = "jdbc:mysql://localhost:3306/your_database";
    private static final String DATABASE_USERNAME = "root";
    private static final String DATABASE_PASSWORD = "password";
    private static final String DATABASE_DRIVER = "com.mysql.cj.jdbc.Driver";

    public static void main(String[] args) {
        // 1. 全局配置
        GlobalConfig.Builder globalConfig = new GlobalConfig.Builder()
            .outputDir(OUTPUT_DIR)
            .author(AUTHOR)
            .enableSwagger()
            .fileOverride()
            .disableOpenDir(); // 不自动打开输出目录

        // 2. 数据源配置
        DataSourceConfig.Builder dataSourceConfig = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
            .dbQuery(new MySqlQuery())
            .schema("public")
            .dbType(DbType.MYSQL)
            .keyWordsHandler(new MySqlKeyWordsHandler())
            .driverName(DATABASE_DRIVER);

        // 3. 包配置
        PackageConfig.Builder packageConfig = new PackageConfig.Builder()
            .parent(BASE_PACKAGE)
            .entity("dao")
            .mapper("mapper")
            .service("service")
            .controller("controller");

        // 4. 策略配置
        StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder()
            .addInclude("user") // 生成指定表
            .addTablePrefix("t_") // 去掉表前缀
            .entityBuilder()
                .superClass(BaseEntity.class)
                .enableLombok()
                .addSuperEntityColumns("id", "creator", "modifier", "create_time", "update_time")
                .logicDeleteColumnName("deleted")
                .addTableFills(new Property("create_time", FieldFill.INSERT))
                .addTableFills(new Property("update_time", FieldFill.INSERT_UPDATE))
                .enableActiveRecord()
                .naming(NamingStrategy.underline_to_camel)
                .columnNaming(NamingStrategy.underline_to_camel)
            .controllerBuilder()
                .enableRestStyle()
                .enableHyphenStyle()
            .serviceBuilder()
                .formatServiceFileName("%sService")
                .formatServiceImplFileName("%sServiceImpl")
            .mapperBuilder()
                .enableBaseResultMap()
                .enableBaseColumnList();

        // 5. 模板配置
        TemplateConfig.Builder templateConfig = new TemplateConfig.Builder();

        // 6. 自定义配置
        InjectionConfig.Builder injectionConfig = new InjectionConfig.Builder()
            .beforeOutputFile((tableInfo, objectMap) -> objectMap.put("parent", BASE_PACKAGE));

        // 7. 整合配置
        AutoGenerator autoGenerator = new AutoGenerator(dataSourceConfig.build())
            .global(globalConfig.build())
            .packageInfo(packageConfig.build())
            .strategy(strategyConfig.build())
            .template(templateConfig.build())
            .injection(injectionConfig.build())
            .templateEngine(new FreemarkerTemplateEngine()); // 选择模板引擎

        // 8. 执行
        autoGenerator.execute();
    }
}
/*
关键配置说明:
GlobalConfig:设置代码生成的全局配置,包括作者、输出目录、是否覆盖已有文件等。
DataSourceConfig:配置数据库连接信息,使用MySQL数据库。
PackageConfig:指定生成的代码所在的包路径。
StrategyConfig:配置生成策略,包括实体类的继承关系、使用Lombok、Rest风格的控制器等。
TemplateConfig:模板配置,可定制生成的模板。
InjectionConfig:自定义配置,用于在生成文件前注入自定义的变量或逻辑。
AutoGenerator:整合所有配置并执行代码生成。

生成的文件包括:
实体类:根据数据库表生成实体类,并继承BaseEntity。
Mapper接口:生成Mapper接口用于数据库操作。
Service接口和实现类:生成Service接口及其实现类。
Controller类:生成Rest风格的控制器类。

使用方法:
修改数据库连接信息(DATABASE_URL、DATABASE_USERNAME、DATABASE_PASSWORD)。
配置需要生成代码的表名(addInclude("user"))。
运行MysqlGenerator.java的main方法,代码将会生成在指定的输出目录中。
*
//【以下都是自动生成的代码】
com/pcy/mapper/UserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
com/pcy/service/UserService.java
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.User;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 用户表 服务类
 * </p>
 */
public interface UserService extends IService<User> {
// 在Spring中使用事务
    @Transactional(propagation = Propagation.REQUIRED)
    void addWithRequired(User user);

    @Transactional(propagation = Propagation.REQUIRED)
    void addWithRequiredAndException(User user);

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void addWithRequiredNew(User user);

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void addWithRequiredNewAndException(User user);

    @Transactional(propagation = Propagation.NESTED)
    void addWithNested(User user);

    @Transactional(propagation = Propagation.NESTED)
    void addWithNestedAndException(User user);
}
com/pcy/service/impl/UserServiceImpl.java
package com.pcy.service.impl;


import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private UserMapper mapper;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequired(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequiredAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNew(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNewAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNested(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNestedAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }
}
resources/mapper/UserMapper.xml
<?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="com.pcy.mapper.UserMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.pcy.entity.User">
    <result column="id" property="id" />
    <result column="creator" property="creator" />
    <result column="modifier" property="modifier" />
    <result column="create_time" property="createTime" />
    <result column="update_time" property="updateTime" />
        <result column="name" property="name" />
        <result column="email" property="email" />
        <result column="birth_day" property="birthDay" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id,
        creator,
        modifier,
        create_time,
        update_time,
        name, email, birth_day
    </sql>

</mapper>
//【提问:爆bug  "Could not autowire. No beans of 'UserMapper' type found"】 深度解析
1. @MapperScan 注解的原理 //启动类里面的 @MapperScan("com.pcy.mapper")
@MapperScan 是 MyBatis-Spring 提供的一个注解,用于指定要扫描的 Mapper 接口所在的包路径。它的作用是告诉 Spring 框架应该在哪些包路径下寻找 Mapper 接口,并将它们注册为 Spring 的 Bean。
扫描 Mapper 接口:Spring Boot 在启动时,会扫描你指定的包路径下的所有接口,并检测这些接口是否包含 MyBatis 的 Mapper 注解或者继承了 BaseMapper 等相关接口。
注册为 Bean:一旦找到这些接口,Spring 会自动为这些接口生成一个实现类,并将它们注册为 Spring 容器中的 Bean,这样你就可以通过 @Autowired 注入这些 Mapper。

2. @Mapper 注解的原理
@Mapper 是 MyBatis 提供的一个注解,用于标记一个接口为 MyBatis 的 Mapper 接口。被标记为 @Mapper 的接口会被 MyBatis-Spring 扫描到,并且 MyBatis 会为该接口生成一个实现类,负责执行 SQL 语句。
当你在 UserMapper 接口上添加 @Mapper 注解时,即使没有使用 @MapperScan,MyBatis 也会知道这个接口是一个 Mapper 接口,并将其注册为一个 Bean。这使得你可以在 UserServiceImpl 中通过 @Autowired 注入它。

3. 为什么使用 @MapperScan 和 @Mapper 不会报错
自动注册 Bean:@MapperScan 会自动扫描指定包路径下的所有 Mapper 接口,并将它们注册为 Spring 容器中的 Bean。这意味着在 UserServiceImpl 中,当你使用 @Autowired 注入 UserMapper 时,Spring 可以找到对应的 Bean,从而避免 Could not autowire 错误。
手动注册 Bean:当你在 Mapper 接口上直接使用 @Mapper 注解时,Spring 也会将该接口注册为一个 Bean,这样你同样可以通过 @Autowired 进行注入,而不会出现 Bean 找不到的问题。
//【提问:MysqlGenerator 逆向生成那些包的原理】
MyBatis-Plus 提供的 MyBatis-Plus Generator 是一个非常强大的代码生成工具,可以通过数据库表结构生成对应的 Java 代码,包括实体类、Mapper 接口、Mapper XML 文件、Service 类、Controller 类等。这个过程通常被称为“逆向工程”或“代码生成”。
1. MyBatis-Plus Generator 的工作原理
 1.1 读取数据库表结构
数据源配置:首先,MyBatis-Plus Generator 通过配置的数据源连接到指定的数据库。它会读取数据库中的表结构信息,包括表名、字段名、数据类型、主键、外键、索引等信息。
> DataSourceConfig dsc = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
    .driverName(DATABASE_DRIVER)
    .build();

元数据解析:MyBatis-Plus Generator 通过 JDBC 获取数据库的元数据 (Metadata),并解析每个表的结构,将其转换为可以用于代码生成的数据结构。

 1.2 生成代码
代码生成器:AutoGenerator 是核心的代码生成器类。它根据从数据库中获取的表结构信息,生成相应的 Java 类文件。
> AutoGenerator generator = new AutoGenerator(dsc);

模板引擎:MyBatis-Plus Generator 使用模板引擎(例如 Freemarker)来渲染代码模板。通过模板和解析后的元数据,生成代码文件。每个生成的 Java 类文件都对应着一个模板文件,模板文件中包含了如何生成特定类型文件的逻辑。
> generator.templateEngine(new FreemarkerTemplateEngine());

 1.3 生成的包和文件
实体类 (entity):根据表结构生成对应的 Java 实体类。每个实体类与数据库表一一对应,包含表中字段的定义。
> strategyConfig.entityBuilder().enableLombok().naming(NamingStrategy.underline_to_camel);

Mapper 接口 (mapper):生成的 Mapper 接口用于与数据库交互,执行基本的增删改查操作。Mapper 接口通常继承自 BaseMapper,提供基本的 CRUD 操作。
> strategyConfig.mapperBuilder().enableBaseResultMap().enableBaseColumnList();
Mapper XML 文件 (mapper.xml):生成的 Mapper XML 文件包含了 Mapper 接口中对应的方法的 SQL 语句。这些 XML 文件用于定义复杂的查询、更新语句等。

Service 接口和实现类 (service, service.impl):Service 层是业务逻辑层。生成的 Service 接口提供了业务操作的定义,Service 实现类则实现这些业务操作。
> strategyConfig.serviceBuilder().formatServiceFileName("%sService");

Controller 类 (controller):生成的 Controller 类用于处理 HTTP 请求,调用 Service 层的方法进行业务处理,然后返回结果。Controller 通常与前端交互,处理用户请求。
> strategyConfig.controllerBuilder().enableRestStyle().enableHyphenStyle();


2. MyBatis-Plus Generator 如何生成这些包和文件
 2.1 代码生成策略 (StrategyConfig)
StrategyConfig 类用于配置代码生成的策略,如生成哪些表,生成哪些类,类的命名规则,是否使用 Lombok 等。
StrategyConfig strategyConfig = new StrategyConfig.Builder()
    .addInclude("user") // 生成指定表
    .entityBuilder().enableLombok() // 实体类配置
    .mapperBuilder().enableBaseResultMap() // Mapper 配置
    .serviceBuilder().formatServiceFileName("%sService") // Service 配置
    .controllerBuilder().enableRestStyle() // Controller 配置
    .build();

 2.2 模板文件
MyBatis-Plus Generator 使用的模板文件可以自定义,通常位于 resources/templates 目录下。每个模板文件对应一个需要生成的 Java 文件类型,例如 entity.java.ftl 对应实体类,mapper.java.ftl 对应 Mapper 接口。
     
模板文件中可以使用变量和逻辑来决定生成的代码内容。例如,${className} 会被替换为实际的类名,<#if useLombok> @Data </#if> 会根据条件生成代码。
     
 2.3 文件输出配置 (InjectionConfig 和 FileOutConfig)
通过 InjectionConfig 和 FileOutConfig,可以控制生成文件的路径、名称、以及自定义生成的文件内容。例如,可以指定某个表的实体类生成到特定的包下,或者将 XML 文件输出到特定的路径。
InjectionConfig cfg = new InjectionConfig.Builder()
    .beforeOutputFile((tableInfo, objectMap) -> {
        // 自定义处理逻辑
    })
    .build();

用MyBatis Plus的分页

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pcy</groupId>
    <artifactId>Pluminary</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Pluminary</name>
    <description>Pluminary</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <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>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>mysql</groupId>-->
<!--            <artifactId>mysql-connector-java</artifactId>-->
<!--            <version>8.0.33</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
            <exclusions>
                <exclusion>
                    <artifactId>mybatis-spring</artifactId>
                    <groupId>org.mybatis</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.5</version> <!-- 版本对齐 -->
        </dependency>

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

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
//【添加MyBatis-Plus的分页插件】
com/pcy/utils/MyBatisPlusConfig.java
package com.pcy.utils;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//@Configuration 用于定义配置类,被注解的类内部包含有一个或多个被@Bean注解的方法
// 用于构建bean定义,初始化Spring容器
@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MARIADB));
        return interceptor;
    }
}
com/pcy/controller/UserController.java //【增加listPage】
package com.pcy.controller;

import com.pcy.entity.User;
import com.pcy.service.UserRepository;
import com.pcy.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.List;

@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @GetMapping("/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }

    @Operation(summary = "创建用户", description = "创建一个新的用户")
    @PostMapping
    public User create(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "更新用户", description = "更新用户信息")
    @PutMapping
    public User update(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "删除用户", description = "根据ID删除用户")
    @DeleteMapping("/{id}")
    public void delete(@PathVariable int id) {
        userRepository.deleteById(id);
    }

    @Operation(summary = "获取用户列表", description = "获取用户列表")
    @GetMapping("/list")
    public org.springframework.data.domain.Page<User> list(@RequestParam(defaultValue = "id") String property,
                                                           @RequestParam(defaultValue = "ASC") Sort.Direction direction,
                                                           @RequestParam(defaultValue = "0") Integer page,
                                                           @RequestParam(defaultValue = "10") Integer pageSize) {
        Pageable pageable = PageRequest.of(page, pageSize, direction, property);
        return userRepository.findAll(pageable);
    }

    @Operation(summary = "根据生日获取用户信息①", description = "根据生日获取用户信息①")
    @GetMapping("/birthdayOne")
    public List<User> getBirthDayOne(@RequestParam LocalDate birthDay) {
        return userRepository.findByBirthDay(birthDay);
    }

    @Operation(summary = "根据生日获取用户信息②", description = "根据生日获取用户信息②")
    @GetMapping("/birthdayTwo")
    public List<User> getBirthDayTwo(@RequestParam LocalDate birthDay) {
        return userRepository.findByBirthDayNative(birthDay);
    }

    @Operation(summary = "删除所有用户", description = "删除所有用户")
    @DeleteMapping("/deleteAll")
    public void deleteAll() {
        userRepository.deleteAll();
    }

    @Operation(summary = "分页查询用户列表", description = "分页查询用户列表")
    @GetMapping("/page")
    public Page<User> listPage(@RequestParam(defaultValue = "1") Integer page,
                               @RequestParam(defaultValue = "10") Integer pageSize) {
        return userService.page(new Page<>(page, pageSize));
    }
}

高级SQL语句(Lambda)

wrapper.lambda().like(user -> user.getName(), "p");
/*
Lambda 表达式:

user -> user.getName() 是一个 Lambda 表达式。
user 是 User 类的一个实例,作为 Lambda 表达式的输入参数。
user.getName() 是对 user 对象的 getName() 方法的调用,返回 name 字段的值。
作用:

这行代码告诉 MyBatis-Plus:在生成的 SQL 查询中,查找 name 字段值中包含 "p" 的所有记录。
wrapper.lambda() 返回一个 LambdaQueryWrapper<User> 对象,支持使用 Lambda 表达式进行条件构建。
.like() 方法添加了一个 LIKE 条件,表示在 SQL 查询中进行模糊匹配。
*/


wrapper.lambda().like(User::getName, "p");
/*
方法引用:

User::getName 是一种方法引用,它引用了 User 类的 getName() 方法。
方法引用是对 Lambda 表达式的一种简写。它表示将某个方法作为函数式接口的实现。
作用:

这行代码与第一行代码的作用相同,都是在生成的 SQL 查询中查找 name 字段值中包含 "p" 的所有记录。
User::getName 告诉 MyBatis-Plus:使用 User 类中的 getName() 方法来获取要参与条件判断的字段。
*/
com/pcy/controller/UserController.java
@Operation(summary = "自定义查询", description = "自定义查询")
    @GetMapping("/Dingyi")
    public List<User> getWrapper() { //类型List<User> 可以返回数据库列表
        QueryWrapper<User> wrapper = new QueryWrapper<>();
//        wrapper.eq("name", "潘春尧");
//        wrapper.lambda().ge(User::getBirthDay, LocalDate.parse("2011-01-01"));
//        wrapper.between(User::getBirthDay, "2011-01-01", "2011-12-31");
        wrapper.lambda().like(User::getName, "string");

//      wrapper.lambda().like(user -> user.getName(), "p");
//        wrapper.select("name,count(*)").groupBy("name");
//        return (QueryWrapper<User>) userMapper.selectList(wrapper);
//        wrapper.in(CollectionUtils.isNotEmpty(nameList), User::getName, nameList);
        return userMapper.selectList(wrapper);
    }

自动填充、填充实现策略

com/pcy/utils/MyMetaObjectHandler.java
package com.pcy.utils;

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

import java.time.LocalDateTime;

public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "creator", this::getCurrentUser, String.class);
        this.strictInsertFill(metaObject, "modifier", this::getCurrentUser, String.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
        this.strictUpdateFill(metaObject, "modifier", this::getCurrentUser, String.class);
    }
    
    // 模拟获取当前用户
    private String getCurrentUser(){
        return "管理员" + (int) (Math.random() * 10);
    }
}
// 这是自动填充的原理
default MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
        if (metaObject.getValue(fieldName) == null) {
            Object obj = fieldVal.get();
            if (Objects.nonNull(obj)) {
                metaObject.setValue(fieldName, obj);
            }
        }
        return this;
    }

强大的Druid

pom.xml
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
// Druid和MariaDB是两种不同类型的数据库系统
1、类型和用途:
Druid:Druid是一种分布式的实时分析数据库,主要用于处理高吞吐量的时间序列数据或事件数据。它专为快速查询和分析大规模数据而设计,常用于数据仓库、在线分析处理(OLAP)以及实时数据分析等场景。
MariaDB:MariaDB是一种关系型数据库管理系统(RDBMS),它是MySQL的一个分支,广泛用于常规的事务处理、数据存储和管理。MariaDB通常用于传统的OLTP(在线事务处理)场景,如web应用、内容管理系统等。

2、适用场景:
Druid:适合用于实时数据分析、日志分析、时间序列分析、用户行为分析等需要快速响应的场景。
MariaDB:适合传统的数据库应用,如电子商务系统、内容管理系统、ERP、CRM等需要强事务处理能力的场景。
    
总结来说,Druid和MariaDB各自适用于不同的数据处理需求,Druid更侧重于实时分析和大规模数据处理,而MariaDB更侧重于事务处理和关系型数据管理

Spring Data JPA与MyBatis-Plus的区别并且简单举例说明

Spring Data JPA: //【实现接口】

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // Other fields, getters, and setters
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
}
Spring Data JPA: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/jpa/users")
public class UserJpaController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
                             .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        return userRepository.save(user);
    }
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User user = userRepository.findById(id)
                                  .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));

        user.setName(userDetails.getName());
        // Update other fields here
        return userRepository.save(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        User user = userRepository.findById(id)
                                  .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));

        userRepository.delete(user);
    }

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        return userRepository.findByName(name);
    }
}


MyBatis Plus: //【实现接口】

@TableName("user")
public class User {
    private Long id;
    private String name;
    // Other fields, getters, and setters
}

public interface UserMapper extends BaseMapper<User> {
    // Custom SQL
    @Select("SELECT * FROM user WHERE name = #{name}")
    List<User> selectByName(@Param("name") String name);
}
MyBatis-Plus: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping
    public List<User> getAllUsers() {
        return userMapper.selectList(null);
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userMapper.selectById(id);
    }

    @PostMapping
    public void createUser(@RequestBody User user) {
        userMapper.insert(user);
    }

    @PutMapping("/{id}")
    public void updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new ResourceNotFoundException("User not found with id: " + id);
        }

        user.setName(userDetails.getName());
        // Update other fields here
        userMapper.updateById(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new ResourceNotFoundException("User not found with id: " + id);
        }

        userMapper.deleteById(id);
    }

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        return userMapper.selectByName(name);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// 构建查询条件的包装类,它使用 Lambda 表达式避免了手写字符串可能导致的字段错误。
// 这种方式非常适合需要根据多个条件动态生成SQL查询的场景,使用LambdaQueryWrapper不仅能提高代码的可读性,还能减少由于硬编码字符串导致的错误。
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        // 使用 LambdaQueryWrapper 构建模糊查询条件
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(User::getName, name); // 类似于 SQL 中的 "WHERE name LIKE '%name%'"

        // 执行查询并返回结果
        return userMapper.selectList(queryWrapper);
    }
    // 其他CRUD方法与前面的示例相同
}

Junit

经过单元测试,观察日志输出,就会发现没有进行数据库查询,对数据库的交互逻辑不是Service层的单元测试需要关心的事情,而是Dao层的单元测试需要考虑的。Service层的单元测试是假定Dao层全部正确的基础上写的,我们只需要关注Service层是正确即可。
pom.xml
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
com/pcy/service/impl/UserServiceImpl.java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    @Autowired
    private UserMapper mapper;

    public User getById(int id) {
        logger.info("id为:",id);
        return mapper.selectById(id);
    }
......
}
这是测试Service
test/java  com/pcy/service/impl/UserServiceImplTest.java //【用Mock改造 + log4j】
// 检查 UserServiceImpl 是否在测试中被 @MockBean 或其他方式替换为Mock对象。如果使用了Mock对象,测试时不会真正访问数据库,而是使用模拟数据。
package com.pcy.service.impl;

import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class UserServiceImplTest {
    @InjectMocks
    UserServiceImpl userService;

    @Mock
    UserMapper userMapper;

    @Test
    @DisplayName("Test Service getById")
    void getById() {
        // 模拟userMapper的selectById方法返回一个User对象
        User mockUser = new User().setId(1).setName("qwe").setEmail("1234@qq.com");
        Mockito.when(userMapper.selectById(1)).thenReturn(mockUser);

        // 调用userService的getById方法,并验证返回结果
        User user = userService.getById(1);

        System.out.println(user);
        Assertions.assertEquals("qwe", user.getName());
    }
}
=====================================================================
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-08-12T21:18:07.805+08:00  INFO 31512 --- [Pluminary] [           main] com.pcy.service.impl.UserServiceImpl     : id为:
User(id=1, name=qwe, age=0, email=1234@qq.com, birthDay=null)
com/pcy/entity/User.java
//你的 User 类同时使用了 Lombok 注解 (@Data, @Accessors(chain = true)) 和手动定义的 getter/setter 方法。由于 Lombok 已经生成了这些方法,手动定义的 getter/setter 方法会覆盖 Lombok 自动生成的方法,这可能导致链式调用的 setEmail 和其他类似方法无法正确解析。
package com.pcy.entity;

import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.time.LocalDate;

@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
@Accessors(chain = true) // 允许链式调用
public class User extends BaseEntity{
    @Id
//    @Schema(description = "用户ID")
//    @NotBlank(message = "Id不能为空")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

//    @Schema(description = "用字")
//    @NotBlank(message = "名字不能为空")
    @Column(nullable = false, columnDefinition = "varchar(20) comment '姓名'")
    private String name;

//    @Transient //注解修饰
//    @Schema(description = "年龄")
//    @Min(value = 1, message = "年龄不能小于1")
    private int age;

//    @Schema(description = "邮箱")
//    @Email(message = "E-mail格式不正确")
    @Column(nullable = false, length = 50)
    private String email;

//    @Schema(description = "生日")
//    @Past(message = "生日必须为过去的时间")
    private LocalDate birthDay;
}
这是测试Controller
test/java  com/pcy/controller/UserControllerTest.java
// Controller层的单元测试需要用到一个特定的类——MockMvc 专门为SpringMVC提供支持的
package com.pcy.controller;

import com.pcy.entity.User;
import com.pcy.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest
class UserControllerTest {
    MockMvc mockMvc;

    @Mock
    UserService userService;

    @InjectMocks
    UserController userController;

    @BeforeEach
    void setUp(){
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }

    @Test
    @DisplayName("Test Controller get")
    void get() throws Exception {
        Mockito.when(userService.getById(1)).thenReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
        BDDMockito.given(userService.getById(1)).willReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
        mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)
                        .accept("application/json;charset=UTF-8")
                        .contentType("application/json;charset=UTF-8"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("刘水镜"))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        log.info("Test Controller get");
    }
}

全局异常处理

/*
一、@RestControllerAdvice 注解的作用
@RestControllerAdvice 是 Spring Framework 为我们提供的一个复合注解,它是 @ControllerAdvice 和 @ResponseBody 的结合体。

@ControllerAdvice:该注解标志着一个类可以为所有的 @RequestMapping 处理方法提供通用的异常处理和数据绑定等增强功能。当应用到一个类上时,该类中定义的方法将在所有控制器类的请求处理链中生效。

@ResponseBody:表示方法的返回值将被直接写入 HTTP 响应体中,通常配合 Jackson 或 Gson 等 JSON 库将对象转换为 JSON 格式的响应。

因此,@RestControllerAdvice 就是专门为 RESTful 控制器设计的全局异常处理器,它的方法返回值将自动转换为响应体。
*/
“全球”异常
com/pcy/controller/UserController.java
@Operation(summary = "异常查询", description = "异常查询")
    @GetMapping(value = "/{id}")
    public Result<User> get(@PathVariable Integer id) {
        User user = userService.getById(id);
        if (user == null){
            throw new RuntimeException("找不到id信息" + id);
        }
        return Result.success(userService.getById(id));
    }
/*
当输入id信息错误的时候
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "creator": null,
    "modifier": null,
    "createTime": null,
    "updateTime": null,
    "id": 1,
    "name": "潘春尧",
    "age": 1,
    "email": "390@qq.com",
    "birthDay": "2024-08-10"
  }
}


当输入id信息错误的时候
{
  "code": 500,
  "message": "找不到id信息3323",
  "data": null
}
*/
com/pcy/utils/GlobalExceptionHandler.java
package com.pcy.utils;

import com.pcy.entity.MessageEnum;
import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//@ExceptionHandler注解用于在Spring MVC控制器中处理特定类型的异常。它可以应用于方法上
//当控制器方法抛出指定类型的异常时,@ExceptionHandler注解的方法将被调用来处理该异常
    @ExceptionHandler(Exception.class)
    public Result<Boolean> globalException(Exception e){
        Result<Boolean> result = new Result<>();
        result.setCode(MessageEnum.ERROR.getCode());
        result.setMessage(e.getMessage() == null ? MessageEnum.ERROR.getMessage() : e.getMessage());
        log.error(e.getMessage(), e);
        return result;
    }
}
com/pcy/entity/MessageEnum.java
package com.pcy.entity;

import lombok.Getter;

@Getter
public enum MessageEnum {
    SUCCESS(200, "操作成功"),
    ERROR(500, "操作失败");

    private final Integer code;
    private final String message;
    MessageEnum(Integer code, String message){
        this.code = code;
        this.message = message;
    }
}
com/pcy/entity/Result.java
package com.pcy.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
   private Integer code;
   private String message;
   private T data;
   // 用于生成一个没有具体数据内容的成功响应
   public static <T> Result<T> success(){
       return success(null);
   }
   // 用于生成包含数据的成功响应
    public static <T> Result<T> success(T data){
        return new Result<>(MessageEnum.SUCCESS.getCode(), MessageEnum.SUCCESS.getMessage(), data);
    }
    // 用于生成一个没有具体错误信息的默认错误响应
    public static<T> Result<T> error(){
        return error(MessageEnum.ERROR);
    }
    // 用于生成带有特定错误信息的错误响应,MessageEnum 是一个枚举类型,包含了不同的错误信息和代码。
    public static<T> Result<T> error(MessageEnum messageEnum){
        return new Result<>(messageEnum.ERROR.getCode(), messageEnum.getMessage(), null);
    }
    // 用于生成包含自定义错误信息的错误响应
    public static <T> Result<T> error(String message) {
        return error(message, MessageEnum.ERROR.getCode());
    }
    // 用于生成包含自定义错误信息和自定义状态码的错误响应
    protected static <T> Result<T> error(String message, Integer code) {
        return new Result<>(code, message, null);
    }
}
写个小异常
com/pcy/controller/ExceptionController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/exception")
@Tag(name = "Exception", description = "异常操作")
public class ExceptionController {
    @GetMapping("/runtimeexception")
    public Result<Boolean> runtimeException(){
        throw new RuntimeException();
    }
}
/*
开启全局异常处理的返回值
{
  "code": 500,
  "message": "操作失败",
  "data": null
}

没有全局异常处理的错误返回值
{
  "timestamp": "2024-08-13T08:21:43.192+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/exception/runtimeexception"
}
*/
//在SwaggerConfig中添加扫描路径 "/exception/**"  不然接口无法获取
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
你提到的 GlobalExceptionHandler 和 ExceptionController 是用于统一处理 Spring MVC 控制器中的异常。让我逐步分析它们的作用,以及为什么在某些情况下它返回错误值。

//1. GlobalExceptionHandler 的作用
@RestControllerAdvice:这个注解用来全局处理控制器层的异常。它会拦截所有抛出的异常,并根据异常类型调用相应的 @ExceptionHandler 方法。

@ExceptionHandler(Exception.class):这个注解标注的方法会在控制器抛出 Exception 或其子类时执行。它用来捕获并处理全局的异常,比如你代码中的 RuntimeException。

globalException(Exception e):这是一个全局异常处理方法。当控制器中出现 Exception 时,这个方法会被调用。它将返回一个带有错误状态码的 Result<Boolean> 对象,并且会将错误信息记录到日志中。

//2. ExceptionController 的作用
@RestController:声明这个类是一个 Spring MVC 控制器,处理 Web 请求并返回数据。

runtimeException() 方法:在这个方法中,你手动抛出了一个 RuntimeException,这会触发 GlobalExceptionHandler 中的 globalException 方法,并返回一个包含错误信息的 Result<Boolean> 对象。

//3. 为什么只有引用 runtimeException() 才返回错误值
runtimeException() 方法直接抛出了一个 RuntimeException,因此会被 GlobalExceptionHandler 捕获并处理。这就是为什么在访问 /exception/runtimeexception 时,你会看到返回的是错误信息。

//4. 在 get() 方法中返回 200 状态码的原因
在 get() 方法中,如果你传入的 id 是无效的,返回的 Result<User> 仍然会是 Result.success(userService.getById(id)),即使 userService.getById(id) 返回的是 null。这种情况下,你的 Result.success(null) 仍然会返回状态码 200,因为 Result.success() 的设计是用于表示成功状态的,且你没有抛出任何异常。

//5. 如何让 get() 方法在出错时返回错误信息
你可以通过以下方法来确保在 get() 方法中传入无效的 id 时,抛出异常并触发全局异常处理器:

手动抛出异常:

java
复制代码
@GetMapping(value = "/{id}")
public Result<User> get(@PathVariable Integer id) {
    User user = userService.getById(id);
    if (user == null) {
        throw new RuntimeException("User not found with id: " + id);
    }
    return Result.success(user);
}
在 userService.getById(id) 方法中抛出异常:如果你的业务逻辑要求在找不到用户时抛出异常,那么可以在 userService.getById(id) 方法中实现这个逻辑。

//6. 总结
GlobalExceptionHandler 用于捕获和处理全局异常。
当你手动抛出 RuntimeException 或其他异常时,它会捕获并返回带有错误信息的 Result。
在 get() 方法中,如果你想要在找不到用户时返回错误信息,需要手动抛出异常,这样才能触发 GlobalExceptionHandler。

日志级别

方法一:直接编写使用
// [配置方法:一种是直接在application.yml文件中配置、另一种是在外置logback-spring.xml文件中配置]
logging:
  pattern:
    console: "%d - %m%n"
方法二:引用外置xml文件
resources/pom.xml               <引用外部的配置>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

resources/application.yaml
logging:
  config: classpath:logback-spring.xml
               <如果你有更多样的配置需求,就需要使用外置XML文件的配置方式>
<?xml version="1.0" encoding="UTF-8" ?>

<configuration>

    <!--    日志文件存放路径-->
    <property name="PATH" value="C:/Users/Pluminary/Desktop/log"/>

    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <!-- 文件日志格式 -->
    <property name="FILE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n"/>

    <!-- 控制台输出配置-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>
                ${CONSOLE_LOG_PATTERN}
            </pattern>
        </layout>
    </appender>

    <!-- INFO 级别日志文件输出配置-->
    <appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--按级别过滤日志,只输出 INFO 级别-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--当天日志文件名-->
        <File>${PATH}/info.log</File>
        <!--按天分割日志文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--历史日志文件名规则-->
            <fileNamePattern>${PATH}/info.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
            <!--按大小分割同一天的日志-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>${FILE_LOG_PATTERN}</Pattern>
        </layout>
    </appender>

    <!-- ERROR 级别日志文件输出配置-->
    <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--按级别过滤日志,只输出 ERROR 及以上级别-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <!--当天日志文件名-->
        <File>${PATH}/error.log</File>
        <!--按天分割日志文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--历史日志文件名规则-->
            <fileNamePattern>${PATH}/error.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
            <!--按大小分割同一天的日志-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>${FILE_LOG_PATTERN}</Pattern>
        </layout>
    </appender>

    <!--日志级别-->
    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="info"/>
        <appender-ref ref="error"/>
    </root>
</configuration>
                   
                   
<
Logback 能够精确区分并输出特定日志级别的错误,是通过 Appender 配置中的 Filter 机制实现的。在你的 Logback 配置文件中,RollingFileAppender 使用了不同的 Filter 来确保只有指定级别的日志信息会被记录到特定的日志文件中。

工作原理
LevelFilter 和 ThresholdFilter:

LevelFilter: 这个过滤器允许你指定只接受特定日志级别的日志。例如,LevelFilter 被配置为只接受 INFO 级别的日志,而拒绝其他级别的日志。<level>INFO</level> 表示只记录 INFO 级别的日志。
ThresholdFilter: 这个过滤器允许你指定一个日志级别的下限,只有高于或等于这个级别的日志才会被记录。例如,ThresholdFilter 被配置为只接受 ERROR 级别及以上的日志(例如 ERROR 和 FATAL)。
日志级别的传递:

日志框架从最底层(比如 TRACE)开始逐级向上检查日志的级别,直到它与 Appender 中配置的 Filter 级别匹配。例如,如果一个 ERROR 级别的日志被触发,RollingFileAppender 的 ThresholdFilter 将检测到这个日志并允许它通过,然后将日志写入指定的 error.log 文件。
日志级别匹配:

当应用程序运行时,它会生成不同级别的日志信息(如 DEBUG、INFO、WARN、ERROR 等)。每个 Appender 都会根据它的 Filter 规则检查这些日志条目。只有符合条件的日志条目才会被记录到相应的日志文件中。
>

AOP切面

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
com/pcy/controller/AspectController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
    @GetMapping
    public Result aspect(String message){
        log.info("aspect controller");
        return Result.success(message);
    }
}
com/pcy/Swagger/SwaggerConfig.java //【增加"/aspect/**"】
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**","/aspect/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
com/pcy/utils/WebAspect.java
package com.pcy.utils;

import com.pcy.entity.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
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 java.util.HashMap;
import java.util.Map;

@Slf4j
@Aspect
@Component
public class WebAspect {
// ★★★★★★★★★★★ 一定要注意这个AOP切面扫描的包 ★★★★★★★★★★★
    @Pointcut("execution(public * com.pcy.controller.*.*(..))")
    public void pointCut() {
    }

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("======================================== 这是@Before ========================================");
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getName();
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        Map<String, Object> paramMap = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }

        log.info("before path:{}",request.getServletPath());
        log.info("before class name:{}",className);
        log.info("before method name:{}",methodName);
        log.info("before args:{}",paramMap.toString());
    }

    @After(value = "pointCut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("======================================== 这是@After =========================================");
        log.info("{} after", joinPoint.getSignature().getName());
    }

    @AfterReturning(value = "pointCut()", returning = "returnVal")
    public void afterReturning(JoinPoint  joinPoint, Object returnVal) {
        System.out.println("==================================== 这是@AfterReturning ====================================");
        log.info("{} after return, returnVal: {}", joinPoint.getSignature().getName(), returnVal);
    }
}

/*
2024-08-14 18:28:20.249  INFO 3296 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-14 18:28:20.249  INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-08-14 18:28:20.250  INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2024-08-14 18:28:20.261  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:18:28:20:261 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before path:/aspect
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before method name:aspect
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before args:{message=www}
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.controller.AspectController      : aspect controller
==================================== 这是@AfterReturning ====================================
2024-08-14 18:28:20.279  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : aspect after return, returnVal: Result(code=200, message=操作成功, data=www)
======================================== 这是@After =========================================
2024-08-14 18:28:20.280  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : aspect after
2024-08-14 18:28:20.308  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:18:28:20:308 毫秒
2024-08-14 18:28:20.308  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-14 18:28:20.309  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:47 毫秒

*/
若是调用UserController的get接口
com/pcy/controller/UserController.java
...
    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @GetMapping("/user/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }
...

Console控制台的报错信息:
/*
2024-08-14 18:33:22.383  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:18:33:22:383 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before path:/users/user/2
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.UserController
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before method name:get
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before args:{id=2}
==================================== 这是@AfterReturning ====================================
2024-08-14 18:33:22.425  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : get after return, returnVal: User(id=2, name=we2, age=2, email=2, birthDay=2024-08-10)
======================================== 这是@After =========================================
2024-08-14 18:33:22.426  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : get after
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:18:33:22:428 毫秒
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:45 毫秒
*/
异常善后处理
com/pcy/controller/AspectController.java //【浏览exception接口的时候会报错】
package com.pcy.controller;

import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
    @GetMapping
    public Result aspect(String message){
        log.info("aspect controller");
        return Result.success(message);
    }
    @GetMapping("/exception")
    public Result exception(){//抛出异常
        throw new RuntimeException("runtime exception");
    }
}


/*
======================================== 这是@Before ========================================
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before path:/aspect/exception
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before method name:exception
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before args:{}
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : exception after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : exception after
2024-08-15 15:09:20.588 ERROR 4200 --- [nio-8080-exec-4] com.pcy.utils.GlobalExceptionHandler     : runtime exception
*/
com/pcy/utils/WebAspect.java
@AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(JoinPoint  joinPoint, Exception e) {
        log.info("{} after throwing, message: {}", joinPoint.getSignature().getName(), e.getMessage());
    }
综上所述:after方法不关心方法是否成功,当方法执行完成之后就会被执行;afterReturning方法必须在目标方法成果return之后才会被执行;afterThrowing方法则会在目标方法抛出异常后被执行
性能统计

Around可以囊括以上所有能力

com/pcy/controller/AspectController.java
@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
  @GetMapping("/sleep/{time}")
    public Result sleep(@PathVariable("time") long time) {
        log.info("sleep");
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
           log.error("error", e);
        }
        if (time == 1000) {
            throw new RuntimeException("runtime exception");
        }
        log.info("wake up");
        return Result.success("wake up");
    }
}
@Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("around start");
        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("around error",e);
        }
        long endTime = System.currentTimeMillis();
        log.info("execute time:{} ms",endTime - startTime);
        return result;
    }
//【当输入time值为2004时】
2024-08-15 15:27:21.987  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:15:27:21:987 毫秒
2024-08-15 15:27:21.990  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : around start
======================================== 这是@Before ========================================
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before path:/aspect/sleep/2004
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before method name:sleep
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before args:{time=2004}
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController      : sleep
2024-08-15 15:27:23.996  INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController      : wake up
==================================== 这是@AfterReturning ====================================
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : sleep after return, returnVal: Result(code=200, message=操作成功, data=wake up)
======================================== 这是@After =========================================
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : sleep after
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : execute time:2007 ms
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:15:27:23:999 毫秒
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:12 毫秒


//【当输入time值为1000时】
2024-08-15 15:28:19.596  INFO 10844 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:15:28:19:596 毫秒
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : around start
======================================== 这是@Before ========================================
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before path:/aspect/sleep/1000
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before method name:sleep
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before args:{time=1000}
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.controller.AspectController      : sleep
2024-08-15 15:28:20.607  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : sleep after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:28:20.607  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : sleep after
2024-08-15 15:28:20.607 ERROR 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : around error

java.lang.RuntimeException: runtime exception
    at com.pcy.controller.AspectController.sleep(AspectController.java:32)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
......
同一切面内的执行顺序

先执行before方法,再执行afterReturning / afterThrowing方法,最后执行after方法
要验证的关键点是around方法和它们之间的先后关系

around方法早于before方法开始执行,并且晚于after方法结束执行,刚好将其他同志完全包裹了起来

//【注释掉WebAspect.java里面的代码不然会叠叠乐累加】
com/pcy/utils/AspectOne.java
package com.pcy.utils;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectOne {
    @Pointcut("execution(public * com.pcy.controller.*.*(..))")
    public void pointCut(){}
    @Before(value = "pointCut()")
    public void before(){
        log.info("before one");
    }
    @After(value = "pointCut()")
    public void after(){
        log.info("after one");
    }
    @AfterReturning(value = "pointCut()")
    public void afterReturning(){
        log.info("afterReturning one");
    }

    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("around one start");
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("around error", e);
        }
        log.info("around one end");
        return result;
    }
}

/*
2024-08-15 16:12:12.819  INFO 28788 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-15 16:12:12.819  INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-08-15 16:12:12.820  INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2024-08-15 16:12:12.839  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:16:12:12:839 毫秒
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : around one start
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : before one
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.controller.AspectController      : aspect controller
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : afterReturning one
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : after one
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : around one end
2024-08-15 16:12:12.908  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:16:12:12:908 毫秒
2024-08-15 16:12:12.909  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-15 16:12:12.909  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:69 毫秒
*/
不同切面间的执行顺序

将AspectOne复制两份命名AspectTwo和AspectThree [执行后是One→Three→Two]
在Spring中的加载顺序是根据类名升序排列的,Three字母排序排在Two前面
那如何指定执行顺序按照One Two Three?
分别为AspectOne/Two/Three加上@Order(1),@Order(2),@Order(3)

Redis

集成
pom.xml
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
spring:
  application:
    name: Pluminary
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
    username: root
    password: root

    redis:
      host: localhost port:6379
      connect-timeout: 1000
      jedis:
        pool:
          min-idle: 5
          max-active: 10
          max-idle: 10
          max-wait: 2000
com/pcy/controller/HelloController.java
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
 @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/hello")
    public String hello(){
        stringRedisTemplate.opsForValue().set("hello","world");
        return stringRedisTemplate.opsForValue().get("hello");
    }
}

//先访问hello接口 再去redis-cli中尝试访问自己定义的内容
http://localhost:8080/swagger-ui/index.html#/hello-controller/hello

/*
127.0.0.1:6379> get hello
"world"
*/

Spring Security

pom.xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
com/pcy/controller/HelloController.java
package com.pcy.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
    @GetMapping("/hi")
//  http://localhost:8080/hi
    public String hi(){
        log.info("hi");
        return "ok!";
    }

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/hello")
    public String hello(){
        stringRedisTemplate.opsForValue().set("hello","world");
        return stringRedisTemplate.opsForValue().get("hello");
    }
}

/* Console:
Using generated security password: 4147707e-58d6-46d9-b5cc-19865a2c523f
*/

账号:user
密码:4147707e-58d6-46d9-b5cc-19865a2c523f
com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 配置HTTP安全性
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
//          .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // 允许访问Swagger UI和API文档
                                .anyRequest().authenticated() // 所有请求都需要认证
                )
                .httpBasic(withDefaults()); // 使用HTTP Basic认证

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .inMemoryAuthentication()
                .withUser("pcy")
                .password(passwordEncoder().encode("123456"))
                .roles("admin");

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

/*
你可能无法访问 http://localhost:8080/swagger-ui/index.html 的原因可能与 Spring Security 配置有关。由于你启用了 Spring Security,默认情况下,所有请求都需要经过身份认证,这可能会阻止你访问 Swagger UI。

为了确保你能够访问 Swagger UI,你需要在 Spring Security 的配置中添加一个例外规则,允许对 /swagger-ui/** 和相关的 Swagger 资源进行无认证访问。

添加代码:.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()


账号:pcy
密码:123456
*/

从数据库中获取用户信息

com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .anyRequest().authenticated()
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults())
                .csrf(csrf -> csrf.disable());

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService)  // 使用数据库中的用户信息
                .passwordEncoder(passwordEncoder());

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
com/pcy/service/impl/UserDetailsServiceImpl.java
package com.pcy.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pcy.entity.SysUser;
import com.pcy.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
//确保你的 UserDetailsServiceImpl 类被 Spring 管理,且实现了 Spring Security 的 UserDetailsService 接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService  {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
        if (sysUser == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        return User.builder()
                .username(sysUser.getUsername())
                .password(sysUser.getPassword())
                .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getRole()))
                .build();
    }
}
com/pcy/entity/SysUser.java
package com.pcy.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema(name = "SysUser对象", description = "系统用户表")
public class SysUser extends Model<SysUser> {

    private static final long serialVersionUID = 1L;

    @Schema(description = "主键 id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @Schema(description = "用户名")
    private String username;

    @Schema(description = "密码")
    private String password;

    @Schema(description = "角色")
    private String role;


    @Override
    public Serializable pkVal() {
        return this.id;
    }
}
com/pcy/service/SysUserService.java
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;

public interface SysUserService extends IService<SysUser> {
    String getCurrentUser();
}
com/pcy/service/impl/SysUserServiceImpl.java
package com.pcy.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.common.ApiException;
import com.pcy.entity.SysUser;
import com.pcy.mapper.SysUserMapper;
import com.pcy.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    @Override
    public String getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 非匿名用户访问才能获得用户信息
        if (!(authentication instanceof AnonymousAuthenticationToken)) {
            String userName = authentication.getName();
            log.info("userName by SecurityContextHolder: {}", userName);
            return userName;
        }
        throw new ApiException("用户不存在!");
    }
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;

/**
 * <p>
 * 系统用户表 Mapper 接口
 * </p>
 */
public interface SysUserMapper extends BaseMapper<SysUser> {

}
com/pcy/common/ApiException.java
package com.pcy.common;

import com.pcy.entity.MessageEnum;
import lombok.Data;

@Data
public class ApiException extends RuntimeException {

    private Integer code;

    public ApiException(MessageEnum messageEnum) {
        super(messageEnum.getMessage());
        this.code = messageEnum.getCode();
    }

    public ApiException(String message) {
        super(message);
        this.code = 500;
    }
}
//【由于数据库的密码要被加密后的形式保存到数据中】
com/pcy/common/test.java
package com.pcy.common;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class test {
    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encodedPassword = encoder.encode("123456");
        System.out.println(encodedPassword);
    }
}

//$2a$10$GzDPdLyrzC9NudmE937AAetR2bef2VQzuSbP6KM6Y.I3045OuT/xC
修改创建SysUser用户的时候用Spring Security [登录的时候就可以用自己创建的了]
com/pcy/controller/UserController.java
/* 对比User数据
    @Operation(summary = "创建User用户", description = "创建一个新的User用户")
    @PostMapping("/create/")
    public User create(@RequestBody User User) {
        return userRepository.save(User);
    }
*/
    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Operation(summary = "创建SysUser用户", description = "创建一个新的SysUser用户")
    @PostMapping("/create/test")
    public SysUser create(@RequestBody SysUser sysUser) {
        sysUser.setPassword(passwordEncoder.encode(sysUser.getPassword()));
        sysUserService.save(sysUser);
        return sysUser;
    }
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;
public interface SysUserService extends IService<SysUser>{

    String getCurrentUser();
}
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

 @Override
    public boolean save(SysUser sysUser) {
        return SqlHelper.retBool(this.baseMapper.insert(sysUser));
    }
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;

/**
 * <p>
 * 系统用户表 Mapper 接口
 * </p>
 */
public interface SysUserMapper extends BaseMapper<SysUser> {

}
权限控制
com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/security/permitall").permitAll() // 允许所有人访问
                        .requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
                        .requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
                        .requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
                        .requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
                        .anyRequest().authenticated() // 其他所有请求需要认证
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults()) // 使用 HTTP Basic 认证
                .csrf(csrf -> csrf.disable()); // 禁用 CSRF

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService)  // 使用数据库中的用户信息
                .passwordEncoder(passwordEncoder());

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
com/pcy/controller/SecurityController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/security")
@Tag(name = "权限控制", description = "权限控制")
public class SecurityController {
    // Anyone
    @Operation(summary = "permitAll 权限")
    @GetMapping(value = "/permitall")
    public Result<String> permitAll(){
        return Result.success("permitAll");
    }
    // 未登录时可以访问
    @Operation(summary = "anonymous 权限")
    @GetMapping(value = "/anonymous")
    public Result<String> anonymous(){
        return Result.success("anonymous");
    }
    // xiaopan可以访问
    @Operation(summary = "config 权限")
    @GetMapping(value = "/config")
    public Result<String> config(){
        return Result.success("permitAll");
    }
    // xiaochun可以访问
    @Operation(summary = "Secured 权限")
    @GetMapping(value = "/Secured")
    @Secured({"ROLE_Secured"})
    public Result<String> Secured(){
        return Result.success("Secured");
    }
    // panchunyao可以访问
    @Operation(summary = "PreAuthorize 权限")
    @GetMapping(value = "/preAuthorize")
    @PreAuthorize("hasAnyAuthority('PreAuthorize')")
    public Result<String> PreAuthorize(){
        return Result.success("PreAuthorize");
    }
}

/*
首先,确保在数据库中创建几个测试用户,并为每个用户分配不同的角色或权限。假设你有以下几个用户:
User 1: Username: xiaopan, Password: 123456, Role: ROLE_config
User 2: Username: xiaochun, Password: 123456, Role: ROLE_Secured
User 3: Username: panchun, Password: 123456, Authority: PreAuthorize

尝试使用不同用户登录:
使用 xiaopan 登录后,尝试访问 /security/config。
使用 xiaochun 登录后,尝试访问 /security/Secured。
使用 panchunyao 登录后,尝试访问 /security/preAuthorize。

检查响应:
/security/config: 只有 xiaopan 能访问,其他用户会被拒绝访问。
/security/Secured: 只有 xiaochun 能访问,其他用户会被拒绝访问。
/security/preAuthorize: 只有 panchunyao 能访问,其他用户会被拒绝访问。
/security/permitall: 所有用户都可以访问。
/security/anonymous: 只有未登录的用户可以访问,登录的用户会被拒绝。

验证权限控制
每个请求的响应应该反映你在 SecurityConfig 中配置的权限。
如果用户没有适当的角色或权限,应该会返回 403 Forbidden 或其他错误响应。
*/
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**","/aspect/**","/test/**","/security/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }
}
// 【问答环节】
我的数据库创建的是Role字段但是为什么 下面这些有.hasAuthority 有.hasRole 还有其他的 这是怎么匹配到我数据库 按照你的方式设置的数据的

/*
1. hasRole() 和 hasAuthority() 的区别

hasRole(String role):
hasRole 方法通常用于检查用户是否拥有特定的角色。
Spring Security 会在你传递的角色名称前自动加上 "ROLE_" 前缀。因此,当你使用 hasRole("Secured") 时,实际上它会检查用户是否有 "ROLE_Secured" 这个权限。

hasAuthority(String authority):
hasAuthority 方法用于检查用户是否拥有特定的权限(或授权)。
hasAuthority 不会自动添加任何前缀。所以当你使用 hasAuthority("ROLE_config") 时,它会直接匹配 "ROLE_config",而不会添加任何前缀。


2. 匹配数据库中的角色和权限
hasRole("Secured"):
代码中的 hasRole("Secured") 实际上会匹配数据库中的 ROLE_Secured,因为 hasRole 方法会自动加上 "ROLE_" 前缀。

hasAuthority("ROLE_config"):
代码中的 hasAuthority("ROLE_config") 会直接匹配数据库中的 "ROLE_config",没有任何前缀变化。

hasAuthority("PreAuthorize"):
代码中的 hasAuthority("PreAuthorize") 会直接匹配数据库中的 "PreAuthorize",因为没有添加任何前缀。
*/
    
它为什么能查到我数据库的role字段里面的数据 如果我把这个字段换成test这个名字 它又是怎么去匹配到的呢
/*
Spring Security 默认会使用 UserDetails 接口中的 getAuthorities() 方法来获取用户的权限或角色信息。这些权限或角色信息通常是通过你在 UserDetailsService 实现类中定义的逻辑从数据库中获取的。
而在SecurityConfig中有代码:
@Autowired // 使用 Spring Security 的UserDetailsService
private UserDetailsService userDetailsService; 
回顾securityFilterChain代码
下面会有 .userDetailsService(userDetailsService) // 设置 UserDetailsService

Spring Security 本身并不直接访问你的数据库表或字段。它依赖于你在 UserDetailsService 中提供的 UserDetails 对象的 getAuthorities() 方法的返回值。因此,当你在 SecurityConfig 中使用 hasRole() 或 hasAuthority() 方法时,它实际上是在检查用户的权限信息,即 UserDetails 对象中的 authorities。
===========================================================================
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
    if (sysUser == null) {
        throw new UsernameNotFoundException("User not found with username: " + username);
    }
    return User.builder()
            .username(sysUser.getUsername())
            .password(sysUser.getPassword())
            .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getTest()))  // 修改为使用 'test' 字段
            .build();
}

*/

记住我 √ Remember Me

基于SpringSession的方式
pom.xml
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
//【要新搞个登录界面 .ftl】
application.yaml

spring:
  freemarker:
    template-loader-path: /templates/
    suffix: .ftl
resources/templates/loginPage.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>
<body>
<h1>Login</h1>
<form action="/login" method="post">
    <div>
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
    </div>
    <div>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required>
    </div>
    <div>
        <input type="checkbox" id="remember-me" name="remember-me">
        <label for="remember-me">Remember me</label>
    </div>
    <div>
        <button type="submit">Login</button>
    </div>
</form>
</body>
</html>
com/pcy/controller/LoginController.java
package com.pcy.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {
    @GetMapping("/login") // 修改为 "/custom-login"
    public String login() {
        return "loginPage"; // 返回的视图名仍然是 "loginPage"
    }
}

/*
http://localhost:8080/login

Please sign in
Username
    panchunyao
Password
    •••••••••••••
√ Remember me on this computer.


127.0.0.1:6379> keys spring*
1) "spring:session:sessions:96c83240-f939-4fd1-ac2c-93542f883aef"
2) "spring:session:sessions:56baf3c6-7a5c-483b-b04a-422b8a2be1b7"
*/
com/pcy/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/security/permitall").permitAll() // 允许所有人访问
                        .requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
                        .requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
                        .requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
                        .requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
                        .anyRequest().authenticated() // 其他所有请求需要认证
                )
                .formLogin(form -> form
//                        .loginPage("/custom-login") // 将登录页面的路径改为 "/custom-login" 打开会循环重定向
                        .defaultSuccessUrl("http://localhost:8080/swagger-ui/index.html", true) // 登录成功后的跳转路径
                        .permitAll()
                )
                .rememberMe(rememberMe -> rememberMe
                        .rememberMeServices(rememberMeServices()) // 配置 Remember Me 服务
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults()) // 使用 HTTP Basic 认证
                .csrf(csrf -> csrf.disable()); // 禁用 CSRF

        return http.build();
    }
}
......
//【问答环节】
分析一下freemarker这个配置的含义
    freemarker:
      template-loader-path: /templates/
      suffix: .ftl
/*
这些配置项的含义如下:
template-loader-path: /templates/: 这个配置指定了 Freemarker 模板文件的加载路径,也就是 /templates/ 目录。Spring Boot 会在这个目录下寻找所有的 .ftl 模板文件。

suffix: .ftl: 这个配置指定了 Freemarker 模板文件的文件后缀,也就是 .ftl。在控制器中返回视图名称时,Spring 会自动添加这个后缀来查找相应的模板文件。


配置的工作原理
@Controller
public class LoginController {
    @GetMapping("/login")
    public String login() {
        return "loginPage"; // 返回的视图名
    }
}
在 LoginController 中的 login() 方法中,你返回的是 "loginPage":
因为在 application.yaml 中已经配置了 template-loader-path 和 suffix,Spring Boot 会根据这些配置来查找模板文件:
它会在 template-loader-path 配置的 /templates/ 目录下寻找文件。
它会在视图名称 "loginPage" 后面自动添加 .ftl 后缀。
因此,最终 Spring Boot 会查找路径 /templates/loginPage.ftl,并使用这个模板文件来渲染登录页面。这就是为什么当你在控制器中返回 "loginPage" 时,Freemarker 能正确地找到并渲染 loginPage.ftl 模板。

如果你想修改 Freemarker 模板文件的目录或后缀,可以调整 application.yaml 中的相应配置。
视图名称在 return 中不需要包含目录或后缀,Spring 会根据配置自动处理。
*/

SpringBoot + Vue企业级狐狸

@RequestMapping 注解指定控制器类中的方法可以处理哪些格式的URL请求
@RequestMapping("/hello") 说明该方法将接收并处理格式为/hello的HTTP请求
@RestController 注解指定本类承担着SpringBoot项目的'控制器'效果
包名 所放置的业务代码类型
common 放置了通用的参数和业务方法
controller 放置了针对各业务请求的控制类
domain 放置了各种业务实体类
mapper 放置了针对MyBatis框架的映射关系类
service 放置了诸多实现业务逻辑的类