SpringBoot多模块项目快速搭建

268 阅读15分钟

SpringBoot

为什么使用SpringBoot

核心内容

自动配置、起步依赖、Actuator、命令行界面

1、更快的开发

Spring BootSpring 生态系统上做出了很多决定和固执己见的默认设置。 这种性质有助于开发人员快速设置并投入开发。

例如,Spring MVC 可以通过大量 XML bean 定义和自定义 servlet 类实现。 但是使用 Spring Boot,它就像添加启动器依赖项一样简单。 绝对不需要代码生成 XML 配置。

2、一切皆有先机

Spring Boot Starters 是包含库和它们的一些自动配置的 Maven 描述符。 而且,这些启动器有助于为 Spring Boot 应用程序提供功能。

​ 想要建立数据库连接? 有一个启动器依赖项。

​ 想与消息队列通话或发送电子邮件? Spring Boot 涵盖了这一切。

​ 对于几乎所有的 Spring 模块,都有一个启动器依赖项可以为您配置所有内容。 甚至一些第三方库也通过他们的启动模块提供对 Spring 的支持。 如果没有这些启动器,作为开发人员将不得不维护依赖项和 XML 配置。 这是应该使用 Spring Boot 的另一个原因。

3、嵌入式服务器

​ Spring Boot 为嵌入式 TomcatJettyUndertow 服务器提供开箱即用的支持。 这样,开发人员就不必担心在传统应用服务器中部署 Web 应用程序。 通过适当的启动器依赖项,甚至可以将一种服务器技术与其他服务器技术交换。 所以实际上最终得到了一个可以像任何 JAR 一样运行的 JAR 文件。 在启动时,JAR 包含足够的库和配置以作为应用程序服务器启动并侦听请求。

​ 如果对嵌入式服务器不感兴趣,可以随时将 Spring Boot 应用程序从 JAR 转换为 WAR,并将它们部署到传统服务器。

4、IDE 对 Spring Boot 的支持

​ 所有主要的 IDE 都提供对 Spring Boot 代码帮助的支持。 例如,IntelliJ IDEA Ultimate 为 Spring Boot 项目提供了出色的代码完成和导航功能。除此之外,VSCode 和 Eclipse 也对其提供了丰富的功能支持。

5、生产环境常用功能

​ Spring Boot 提供了生产就绪的特性,例如监控、指标和开箱即用的注销。 有了这些特性,开发人员可以避免额外的配置。

例如,健康执行器端点等功能使应用程序状态监控成为可能。

例如,可以让像 Prometheus 这样的工具收集应用程序指标,在 Kubernetes 或 Openshift 环境中使用就绪性和活跃度健康端点;只需添加其他属性或通过 /actuator/logging 端点即可更改日志记录级别。

​ 此外,开发人员可以使用自己的自定义健康端点配置这些执行器端点。

6、开箱即用的 JUnit 支持

​ 默认情况下,所有 Spring Boot 项目都带有 JUnit 5。 此外,Spring Boot 提供了**@SpringBootTest** 注解来在我们需要时初始化测试上下文。 所以开发人员只需要编写测试用例。不必再担心测试用例的复杂 spring 上下文。

7、Spring Boot的优势

  1. 快速创建独立运行的Spring项目以及与主流框架集成

  2. 嵌入的Tomcat,无需打包成WAR包

  3. starters自动依赖与版本控制

  4. 大量自动配置,简化开发,也可修改默认值

  5. 无需配置xml,无代码生成,开箱即用

    。。。。。。。

简言之就是整个Spring技术栈的整合,来简化Spring应用开发,约定大于配置,去繁从简,just run 就能创建一个独立的,产品级别的应用。

如何搭建?

方式一:

进入官网,选择Maven项目,java语言,打包方式jar包,版本1.8后,可以explore预览一下

image-20230831151228942.png

最后可以直接download下载 image-20230831151732399.png IDEA打开demo项目(maven需要配置),刷新maven后,项目正常加载完成

image-20230831152929826.png

方式二:

New Project-->选择Maven项目,java语言,打包方式jar包,版本1.8后Next下一步

image-20230831153126808.png

image-20230831153232643.png

选择spring boot版本及基本依赖(个人认为Lombok非常好用),后续缺少依赖可以再去pom中添加,最后Finish创建项目 image-20230831153544394.png

配置文件中,给一个端口号 image-20230831155051372.png

新建一个测试方法并且启动

@RestController/**@RestController:将类标记为控制器*/
@RequestMapping("/test")/**@RequestMapping("/test"):将请求 URL 映射到 /test 路径*/
public class demo {

    /**将请求 URL 映射到 /hello 路径*/
    @GetMapping("/hello")
    public String hello() {
        return "Hello World!";
    }

}

image-20230831160800124.png

浏览器访问http://localhost:8081/test/hello,接口路径正确,可以看到输出的是我们测试方法中返回的Hello World!

image-20230831160908653.png

至此,快速SpringBoot已完成,不需要额外的配置文件,额外的依赖,甚至是额外的服务器,只需要一个启动类,就可以实现一个最基本的Spring Boot的应用

分析下pom文件

<?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>
    <!--继承 SpringBoot 框架的一个父项目,所有自己开发的 Spring Boot 都必须的继承-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <!--当前项目的 GAV 坐标-->
    <groupId>com.xxw</groupId>
    <artifactId>demo1</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <!--maven 项目名称,可以删除-->
    <name>demo1</name>
    <!--maven 项目描述,可以删除-->
    <description>demo1</description>

    <dependencies>
        <!--SpringBoot 框架 web 项目起步依赖,通过该依赖自动关联其它依赖,不需要我们一个一个去添加-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--SpringBoot 框架的测试起步依赖,例如:junit 测试,如果不需要的话可以删除-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--SpringBoot提供的打包编译等插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

核心配置文件

.properties 文件(默认采用该文件)

#设置内嵌 Tomcat 端口号 
server.port=8081 

.yml 文件

yml 是一种 yaml 格式的配置文件,主要采用一定的空格、换行等格式排版进行配置。

它能够直观的被计算机识别数据序列化格式,容易被人类阅读,yaml 类似于 xml,但是语法比 xml 简洁很多,值与前面的冒号配置项必须要有一个空格, yml 后缀也可以使用 yaml 后缀 。

#设置内嵌 Tomcat 端口号 
server:
  port: 8081

注意:当两种格式配置文件同时存在时,使用的是.properties 配置文件。

多环境配置(.properties方式)

在实际开发的过程中,我们的项目会经历很多的阶段(开发->测试->上线),每个阶段 的配置也会不同,那么这个时候为了方便在不同的环境之间切换,SpringBoot 提供了多环境配置:

application-dev.properties

#开发环境
#设置内嵌 Tomcat 默认端口号
server.port=8080

application-product.properties

#生产环境
#配置内嵌 Tomcat 默认端口号
server.port=80

application-test.properties

#生产环境
#配置内嵌 Tomcat 默认端口号
server.port=8081

application.properties

进行环境的激活

#SpringBoot 的总配置文件
#激活开发环境
#spring.profiles.active=dev
#激活生产环境
#spring.profiles.active=product
#激活测试环境
spring.profiles.active=test

多环境配置(.yml方式)

application-dev.yml

#开发环境
#设置内嵌 Tomcat 默认端口号
server:
  port: 8080

application-product.yml

#生产环境
#配置内嵌 Tomcat 默认端口号
server:
  port: 80

application-test.yml

#生产环境
#配置内嵌 Tomcat 默认端口号
server:
  port: 8081

application.yml

进行环境的激活

#springboot 总配置文件 
#激活开发环境 
#spring: 
# profiles: 
#  active: dev 
 
#激活生产环境 
#spring: 
# profiles: 
#  active: product
  
#激活测试环境 
spring: 
 profiles: 
  active: test 

自定义配置

@Value注解

在核心配置文件 applicatin.properties 中,添加两个自定义配置项 school.name 和 website

在 IDEA 中可以看到这两个属性不能被 SpringBoot 识别,背景是桔色的

properties

image-20230831163445459.png

yml

#设置端口号及上下文根 
server: 
 port: 8081 

school: 
 name: xxw 
websit: www.baidu.com 

demo测试

在 demo 中定义属性,并使用 @Value 注解或者自定义配置值,并对其方法进行测试

image-20230831163809549.png

运行访问

image-20230831163721791.png

@ConfigurationProperties注解

在 com.xxw.demo1.config 包下创建 ConfigInfo 类,并为该类加上 ComponentConfigurationProperties 注解,并在 ConfigurationProperties 注解中添加属性 prefix,可以区分同名配置 。(@Data为前面内容添加的lombok依赖,注在类上提供类的get、set、equals、hashCode、canEqual、toString方法)

额外内容学习:

@AllArgsConstructor : 注在类上,提供类的全参构造
@NoArgsConstructor : 注在类上,提供类的无参构造
@Setter : 注在属性上,提供 set 方法
@Getter : 注在属性上,提供 get 方法
@EqualsAndHashCode : 注在类上,提供对应的 equals 和 hashCode 方法
@Log4j/@Slf4j : 注在类上,提供对应的 Logger 对象,变量名为 log

image-20230831164301000.png 如果出现Spring Boot Configuration Annotation Processor not configured

1、在pom中添加依赖,这将会启用Spring Boot的配置处理器。

<!--解决使用@ConfigurationProperties 注解出现警告问题-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

2、在IDE中,确保项目的Annotation Processor已经被启用。 "Settings > Build, Execution, Deployment > Compiler > Annotation Processors", 确保"Enable annotation processing"选项已经被勾选。

image-20230831165847206.png 修改此前properties配置文件中参数,需要加上school,与prefix = "school" 对应

school.name=xxw 
school.websit=www.baidu.com 

使用@Autowired注入 ConfigInfo 配置类

@Autowired
private ConfigInfo configInfo;

测试方法

@GetMapping("/say")
public String say() {
    return configInfo.getName()+"------>"+configInfo.getWebsit();
}

image-20230831171115744.png

整合Mybatis-Plus+代码生成

1、新建项目

新建多模块项目xxwdemo,并删除src文件

image-20230901154703661.png

新建maven项目,ctrl+shift+alt+s,调出项目结构,然后点击modules--“+”号--选择new module--选择maven项目,然后命名创建

image-20230901155455702.png 这边创建两个模块:user-core核心部分做统一处理、user-test测试部分做代码测试,可以在父pom中看到两个模块自动添加成功。

image-20230901155921551.png

依赖整理

父依赖:

<?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>

    <packaging>pom</packaging>

    <modules>
        <module>user-core</module>
        <module>user-test</module>
    </modules>

    <groupId>com.xxw</groupId>
    <artifactId>xxwdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <properties>
        <hutool.version>5.7.18</hutool.version>
        <mybatis-plus.version>3.5.0</mybatis-plus.version>
        <spring.boot.version>2.5.12</spring.boot.version>
        <knife4j.version>3.0.3</knife4j.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

在test模块添加:

2、添加模板引擎

模板引擎(模板生成代码在test模块)

<!--模板引擎-->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.23</version>
</dependency>

3、添加Mysql

<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

4、添加lombok

<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

在core模块添加

5、添加MybatisPlus

<!--mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.0</version>
</dependency>

6、添加mysql

<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

7、添加JPA

<!--Spring Data JPA-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.github.biyanwen</groupId>
    <artifactId>jpa-comment-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

6、添加hutool、redis、Redisson

Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发

        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>

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

        <!--高级的分布式协调Redis客服端-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.9.1</version>
        </dependency>

core模块:

核心模块,整合MybatisPlus,供服务模块快速进行业务开发

1、新建config、cache.resp文件

  • 先配置RespKeyEnum错误枚举类,随后在resp下创建RespKeyInterfaceREspKey接口,供RespKeyEnum使用
/**
 * @author Created by xxw on 2023-09-01 15:30
 * @Description resp枚举
 */
@Getter
@AllArgsConstructor
public enum RespKeyEnum implements RespKeyInterface {
    SUCCESS(200, "成功"),
    NO_LOGIN(401, "尚未登录"),
    NO_ACCESS(403, "尚未拥有权限"),
    PARAM_ERROR(412, "参数错误"),
    INNER_ERROR(500, "系统内部错误"),
    UN_AUTH(601, "未实名认证"),
    UN_USE_OPENID(602, "openId未使用"),
    DONT_HAVE_WX_OPEN_ID(603, "缺少微信openId"),
    FROZEN(625, "账号被冻结"),
    ;
    //分类名称
    private Integer code;
    //key描述
    private String keyDescription;
}
/**
 * @author Created by xxw on 2023-09-01 15:30
 * @Description RespKey接口
 */
public interface RespKeyInterface {
    /**分类名称*/
    Integer getCode();

    /**key描述*/
    String getKeyDescription();
}
  • 随后创建Resp统一返回类,统一所有返回
/**
 * @author Created by xxw on 2023-09-01 14:43
 * @Description 统一Resp
 */
@Slf4j
@Data
public class Resp<T> implements Serializable {
    /**返回码,200->成功 401->尚未登录 412->参数错误 500->未知错误*/
    @ApiModelProperty("返回码,200->成功 401->尚未登录 412->参数错误 500->未知错误")
    private int code;
    /**返回信息*/
    @ApiModelProperty("返回信息")
    private String message;
    /**返回数据*/
    @ApiModelProperty("返回数据")
    private T result;
    /**请求id*/
    @ApiModelProperty("请求id")
    private String traceId;
    /**详细错误信息*/
    @ApiModelProperty("详细错误信息")
    private String errorMessage;

    /**Resp枚举*/
    @Getter
    @AllArgsConstructor
    public enum RespEnum implements RespKeyInterface {
        SUCCESS(200, "成功"),
        NO_LOGIN(401, "尚未登录"),
        NO_ACCESS(403, "尚未拥有权限"),
        PARAM_ERROR(412, "参数错误"),
        INNER_ERROR(500, "系统内部错误"),
        ;
        private Integer code;
        private String keyDescription;
    }

    /**
     * @param <T> T
     * @return 成功200
     */
    public static <T> Resp<T> success() {
        return restResult(null, RespKeyEnum.SUCCESS, null);
    }

    /**
     * @param msg msg
     * @param <T> T
     * @return 成功200、msg
     */
    public static <T> Resp<T> success(String msg) {
        return restResult(null, RespKeyEnum.SUCCESS, msg);
    }

    /**
     * @param data data
     * @param <T> T
     * @return 成功200、data
     */
    public static <T> Resp<T> success(T data) {
        return restResult(data, RespKeyEnum.SUCCESS, "success");
    }

    /**
     * @param data data
     * @param msg msg
     * @param <T> T
     * @return 成功200、data、msg
     */
    public static <T> Resp<T> success(T data, String msg) {
        return restResult(data, RespKeyEnum.SUCCESS, msg);
    }

    /**
     * @param <T> T
     * @return 参数错误412
     */
    public static <T> Resp<T> error() {
        return restResult(null, RespKeyEnum.PARAM_ERROR, null);
    }

    /**
     * @param msg msg
     * @param <T> T
     * @return 参数错误412、msg
     */
    public static <T> Resp<T> error(String msg) {
        return restResult(null, RespKeyEnum.PARAM_ERROR, msg);
    }

    /**
     * @param data data
     * @param <T> T
     * @return 参数错误412、data
     */
    public static <T> Resp<T> error(T data) {
        return restResult(data, RespKeyEnum.PARAM_ERROR, null);
    }

    /**
     * @param data data
     * @param msg msg
     * @param <T> T
     * @return 参数错误412、data、msg
     */
    public static <T> Resp<T> error(T data, String msg) {
        return restResult(data, RespKeyEnum.PARAM_ERROR, msg);
    }

    /**
     * @param data data
     * @param status RespKeyEnum
     * @param msg msg
     * @param <T> T
     * @return 错误码、data、msg
     */
    public static <T> Resp<T> error(T data, RespKeyEnum status, String msg) {
        return restResult(data, status, msg);
    }

    /**
     * @param status RespKeyEnum
     * @param msg msg
     * @param <T> T
     * @return 通过RespKeyEnum返回错误码、msg
     */
    public static <T> Resp<T> error(RespKeyEnum status, String msg) {
        return restResult(null, status, msg);
    }

    /**
     * @param code code
     * @param msg msg
     * @param <T> T
     * @return 直接返回错误码、msg
     */
    public static <T> Resp<T> error(Integer code, String msg) {
        return restResult(null, code, msg);
    }

    /**
     * @param code code
     * @param msg msg
     * @param errorMessage errorMessage
     * @param <T> T
     * @return 直接返回错误码、msg、errorMessage
     */
    public static <T> Resp<T> error(Integer code, String msg, String errorMessage) {
        return restResult(null, code, msg, errorMessage);
    }


    /**
     * @param data data
     * @param code code
     * @param msg msg
     * @param <T> T
     * @return 返回data、错误码、msg
     */
    private static <T> Resp<T> restResult(T data, Integer code, String msg) {
        Resp<T> apiResult = new Resp<>();
        apiResult.setCode(code);
        apiResult.setResult(data);
        apiResult.setMessage(msg);
        return apiResult;
    }

    /**
     * @param data data
     * @param code code
     * @param msg msg
     * @param errorMessage errorMessage
     * @param <T> T
     * @return
     */
    private static <T> Resp<T> restResult(T data, Integer code, String msg, String errorMessage) {
        Resp<T> apiResult = new Resp<>();
        apiResult.setCode(code);
        apiResult.setResult(data);
        apiResult.setMessage(msg);
        apiResult.setErrorMessage(errorMessage);
        String traceId = "";
        try {
            //MDC获取请求id
            traceId = MDC.get("traceId");
        } catch (Exception e) {
            log.error("traceId获取错误:{}",e.getMessage());
        }

        apiResult.setTraceId(traceId);
        return apiResult;
    }

    private static <T> Resp<T> restResult(T data, RespKeyInterface respEnum, String msg) {
        return restResult(data, respEnum.getCode(), msg);
    }


    /**
     * 按错误码获取状态
     * @param code
     * @return
     */
    public static RespKeyEnum getStatusByCode(Integer code) {
        for (RespKeyEnum status : RespKeyEnum.values()) {
            if (code.equals(status.getCode())) {
                return status;
            }
        }
        return RespKeyEnum.INNER_ERROR;
    }
  • config中配置MyBatis-Plus拦截器配置
/**
 * @author Created by xxw on 2023-08-31 17:47
 * @Description MyBatis Plus拦截器配置
 */
@Configuration
public class MybatisPlusConfig {
    /**
     * @return interceptor
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
  • 创建MyMetaObjectHandler类,继承MetaObjectHandler重写insertFill、updateFill方法,用于插入时对创建时间公共字段的填充
/**
 * @author Created by xxw on 2023-09-04 10:58
 * @Description MetaObject处理
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 重写插入元对象字段填充(用于插入时对公共字段的填充)
     * @param metaObject metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        boolean hasSetter = metaObject.hasSetter("createTime");
        if (hasSetter) {
            this.setFieldValByName("createTime", new Date(), metaObject);
        }
    }


    /**
     * 更新元对象字段填充(用于更新时对公共字段的填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        boolean hasSetter = metaObject.hasSetter("updateTime");
        if (hasSetter) {
            this.setFieldValByName("updateTime", new Date(), metaObject);
        }
    }
}

2、创建exception文件

  • 基本异常处理类BaseException,继承RuntimeException
/**
 * @author Created by xxw on 2023-09-01 14:38
 * @Description 基本异常
 */
@Getter
public class BaseException extends RuntimeException {

    private Integer code;
    private boolean isShowErrorStack = false;

    public BaseException(String message) {
        super(message);
        this.code = RespKeyEnum.PARAM_ERROR.getCode();
    }

    public BaseException(String message, Object... params) {
        super(StrUtil.format(message, params));
        this.code = RespKeyEnum.PARAM_ERROR.getCode();
    }

    public BaseException(RespKeyInterface respEnum) {
        super(respEnum.getKeyDescription());
        this.code = respEnum.getCode();
    }

    public BaseException(RespKeyInterface respEnum, String message) {
        super(message);
        this.code = respEnum.getCode();
    }

    public BaseException(RespKeyInterface respEnum, String message, Object... params) {
        super(StrUtil.format(message, params));
        this.code = respEnum.getCode();
    }

    public BaseException(String message, boolean isShowErrorStack) {
        super(message);
        this.code = RespKeyEnum.PARAM_ERROR.getCode();
        this.isShowErrorStack = isShowErrorStack;
    }

}

orm

Object Relative Mapping)对象-关系映射

它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。

ORM作为项目中间件形式实现数据在不同场景下数据关系映射. 对象关系映射(Object Relational Mapping,简称ORM)是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术

3、新建orm.domain文件

  • 创建基础实体类BaseEntity,用于处理id,createTime公共字段,供实体类继承
/**
 * @author Created by xxw on 2023-09-04 11:07
 * @Description 基础实体
 */
@Data
@MappedSuperclass
public class BaseEntity implements Serializable {
    @Id
    @Column(length = 32)
    @ApiModelProperty(value = "id", hidden = true)
    private String id;

    /**Date转换成String  一般后台传值给前台时用*/
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    /**插入时填充字段*/
    @TableField(fill = FieldFill.INSERT)
    @ApiModelProperty(value = "创建时间", hidden = true)
    private Date createTime;

    /**Date转换成String  一般后台传值给前台时用*/
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    /**更新时填充字段*/
    @TableField(fill = FieldFill.UPDATE)
    @ApiModelProperty(value = "更新时间", hidden = true)
    private Date updateTime;
}

4、创建orm.dto文件

  • 先创建一个util.ParamUtil.class参数工具类,做各种参数的判断
/**
 * @author Created by xxw on 2023-08-31 17:38
 * @Description 参数工具类
 */
public class ParamUtil {

    /**
     * 判断一个集合对象是否为null或者为空集合
     *
     * @return
     */
    public static boolean isBlank(java.util.Collection<?> collection) {
        return collection == null || collection.size() == 0;
    }

    /**
     * 判断一个字符串是否为null或者为空
     *
     * @return
     */
    public static boolean isBlank(String string) {
        return string == null || string.trim().equals("");
    }

    /**
     * 判断数字是否为null或者为0
     *
     * @return
     */
    public static boolean isBlank(Number num) {
        return num == null || num.equals(0);
    }

    /**
     * 判断一个long[]是否为null或者长度为0
     *
     * @return
     */
    public static boolean isBlank(Long[] ids) {
        return ids == null || ids.length == 0;
    }

    /**
     * 判断一个String[]是否为null或者长度为0
     *
     * @return
     */
    public static boolean isBlank(String[] strings) {
        return strings == null || strings.length == 0;
    }

    /**
     * 判断一个Object是否为null
     *
     * @return
     */
    public static boolean isBlank(Object obj) {
        return obj == null;
    }

}
  • 配置分页查询DTO
/**
 * @author Created by xxw on 2023-09-04 11:04
 * @Description 分页DTO
 */
@Data
public class PageRequestDTO implements Serializable {
    @ApiModelProperty("页号")
    /**页号*/
    private int pageNumber;

    @ApiModelProperty("页面大小")
    /**页面大小*/
    private int pageSize;

    @ApiModelProperty("排序字段")
    /**排序字段*/
    private String pageSort;

    @ApiModelProperty("排序方式 asc/desc")
    /**排序方式 asc/desc*/
    private String pageOrder;

    public PageRequestDTO() {
        initMpPage();
    }

    public PageRequestDTO(int pageNumber, int pageSize) {
        this.pageNumber = pageNumber;
        this.pageSize = pageSize;
        initMpPage();
    }

    public PageRequestDTO(int pageNumber, int pageSize, String pageSort, String pageOrder) {
        this.pageNumber = pageNumber;
        this.pageSize = pageSize;
        this.pageSort = pageSort;
        this.pageOrder = pageOrder;
        initMpPage();
    }

    private void initMpPage() {
        if (this.pageNumber < 1) {
            this.pageNumber = 1;
        }

        if (this.pageSize < 1) {
            this.pageSize = 10;
        }

        if (ParamUtil.isBlank(this.pageSort)) {
            this.pageSort = "createTime";
        }

        if (ParamUtil.isBlank(this.pageOrder)) {
            this.pageOrder = "desc";
        }
    }

    public void setPageSort(String pageSort) {
        if (ParamUtil.isBlank(pageSort)) {
            return;
        }
        this.pageSort = pageSort;
    }
}
  • 创建UpdateRequestDTO,用来根据id进行修改
/**
 * @author Created by xxw on 2023-09-04 11:36
 * @Description 更新请求DTO
 */
@Data
public class UpdateRequestDTO {
    @ApiModelProperty(value = "id", required = true)
    private String id;
}
  • 创建MpPredicateDTO
/**
 * @author Created by xxw on 2023-09-04 11:09
 * @Description mybatis-plus条件构造器DTO
 */
@AllArgsConstructor
@Data
public class MpPredicateDTO {
    private String property;
    private Object val;
}
  • 创建MpPredicatePropertyDTO
/**
 * @author Created by xxw on 2023-09-04 11:52
 * @Description mybatis-plus条件构造器各属性DTO
 */
@Data
public class MpPredicatePropertyDTO {
    private Field field;
    private String propertyName;
    private BiConsumer<QueryWrapper<?>, MpPredicateDTO> predicateFunc;
}

5、创建orm.enumeration、orm.annotation文件

新建MpPredicateEnum枚举类,主要是跟mybatis-plus,通过自定义注解的形式使用常用条件构造器

/**
 * @author Created by xxw on 2023-09-04 11:41
 * @Description mybatis-plus条件构造器谓语枚举类
 */
@Getter
@AllArgsConstructor
@SuppressWarnings("unchecked")
public enum MpPredicateEnum {
    //equal
    EQ((wrapper, dto) -> wrapper.eq(dto.getProperty(), dto.getVal())),
    //not equal
    NE((wrapper, dto) -> wrapper.ne(dto.getProperty(), dto.getVal())),
    //grater than
    GT((wrapper, dto) -> wrapper.gt(dto.getProperty(), dto.getVal())),
    //grater than equal
    GE((wrapper, dto) -> wrapper.ge(dto.getProperty(), dto.getVal())),
    //less than
    LT((wrapper, dto) -> wrapper.lt(dto.getProperty(), dto.getVal())),
    //less than equal
    LE((wrapper, dto) -> wrapper.le(dto.getProperty(), dto.getVal())),
    //full like %XX%
    F_LK((wrapper, dto) -> wrapper.like(dto.getProperty(), dto.getVal())),
    //left like %XX
    L_LK((wrapper, dto) -> wrapper.likeLeft(dto.getProperty(), dto.getVal())),
    //right like XX%
    R_LK((wrapper, dto) -> wrapper.likeRight(dto.getProperty(), dto.getVal())),
    //in
    // IN((wrapper, dto) -> wrapper.in(dto.getProperty(),dto.getVal())),
    IN((wrapper, dto) -> {
        Object[] obj = null;
        if (dto.getVal() instanceof Collection) {
            Collection c = (Collection) dto.getVal();
            obj = c.toArray();
        }
        wrapper.in(!ParamUtil.isBlank(obj), dto.getProperty(), obj);
    }),
    //not in
    NI((wrapper, dto) -> {
        Object[] obj = null;
        if (dto.getVal() instanceof Collection) {
            Collection c = (Collection) dto.getVal();
            obj = c.toArray();
        }
        wrapper.notIn(!ParamUtil.isBlank(obj), dto.getProperty(), obj);
    }),
    ;
    private BiConsumer<QueryWrapper<?>, MpPredicateDTO> predicateFunc;
}
  • 新建annotation.MpPredicate自定义注解
/**
 * @author Created by xxw on 2023-09-04 11:45
 * @Description mybatis-plus自定义注解
 */
@Target({FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MpPredicate {
    String property() default "";//对应字段名

    MpPredicateEnum value() default MpPredicateEnum.EQ;//谓语动作
}

6、创建orm.controller文件

  • 新建BaseContent,用于处理success、error及异常处理
/**
 * @author Created by xxw on 2023-09-01 14:35
 * @Description 基础内容
 */
@Slf4j
public class BaseContent {

    /**错误消息标志*/
    @Value("${sys.error-message-flag:false}")
    private Boolean errorMessageFlag;


    /**
     * 内部异常
     * @param e Exception
     * @return error
     */
    @ExceptionHandler(Exception.class)
    public Resp handleException(Exception e) {
        log.error("内部异常:", e);
        return error(RespKeyEnum.INNER_ERROR, e.getMessage() == null ? "接口请求异常" : e.getMessage(), errorMessageFlag ? JSONUtil.toJsonStr(e) : "");
    }

    /**
     * 基本异常
     * @param myException BaseException
     * @return error
     */
    @ExceptionHandler(BaseException.class)
    public Resp baseException(BaseException myException) {
        try {
            log.error("业务异常:{}-{}", myException.getStackTrace()[0], myException.getMessage());
        } catch (Exception e) {
            log.error("业务异常:{}", myException.getMessage());
        }

        return error(ParamUtil.isBlank(myException.getCode()) ? RespKeyEnum.INNER_ERROR
                : Resp.getStatusByCode(myException.getCode()), myException.getMessage(), errorMessageFlag ? JSONUtil.toJsonStr(myException) : "");
    }

    public static <T> Resp<T> error(Integer code, String msg) {
        return Resp.error(code, msg);
    }

    public static <T> Resp<T> error(RespKeyEnum respEnum, String msg) {
        return Resp.error(respEnum, msg);
    }

    public static <T> Resp<T> error(RespKeyEnum respEnum, String msg, String errorMessage) {
        return Resp.error(respEnum.getCode(), msg, errorMessage);
    }

    public static <T> Resp<T> error(String msg) {
        return Resp.error(msg);
    }

    public static <T> Resp<T> success(T data, String msg) {
        return Resp.success(data, msg);
    }

    public static <T> Resp<T> success(String msg) {
        return Resp.success(msg);
    }

    public static <T> Resp<T> success(T data) {
        return Resp.success(data);
    }
}
  • 创建BaseController抽象类,继承于BaseContent,其中参数有两个<M extends BaseMapper, E extends BaseEntity>,一个为继承了BaseMapper的mapper接口,一个为继承了前面创建的BaseEntity实体类。
  • BaseController抽象类主要是提供给各个功能controller继承,实现CRUD
/**
 * @author Created by xxw on 2023-09-04 12:06
 * @Description 基本控制器
 */
public abstract class BaseController<M extends BaseMapper<E>, E extends BaseEntity> extends BaseContent {

    public abstract BaseService<M, E> getService();

    public Resp<Page<E>> list(PageRequestDTO condition) {
        return Resp.success(getService().pageList(condition));
    }

    public Resp save(E entity) {
        getService().save(entity);
        return success("添加成功");
    }

    public Resp update(UpdateRequestDTO updateEntity) {
        getService().commonUpdate(updateEntity);
        return success("修改成功");
    }

    public Resp delete(String[] ids) {
        getService().removeByIds(Arrays.asList(ids));
        if (getService().definedRedisKey() != null) {
            for (String id : ids) {
                RedisUtil.delete(getService().definedRedisKey(), id);
            }
        }
        return success("删除成功");
    }
}

7、创建orm.service文件

  • 创建BaseService类,继承ServiceImpl,其中参数有两个<M extends BaseMapper, E extends BaseEntity>,一个为继承了BaseMapper的mapper接口,一个为继承了前面创建的BaseEntity实体类。
  • BaseService类主要是提供给各个功能service继承
/**
 * @author Created by xxw on 2023-09-04 11:55
 * @Description 基本服务
 */
@Slf4j
public class BaseService<M extends BaseMapper<E>, E extends BaseEntity> extends ServiceImpl<M, E> {

    protected static CopyOptions COPY_OPTIONS = CopyOptions.create().setTransientSupport(false).setIgnoreNullValue(true);

    public RedisKeyInterface definedRedisKey() {
        return null;
    }

    /**仅根据id获取redis缓存,请自己管理好缓存的生命周期*/
    public E findByIdFromRedis(String id) {
        RedisKeyInterface keyEnum = definedRedisKey();
        if (keyEnum == null) {
            throw new BaseException("请先定义redisKey");
        }
        return RedisUtil.get(keyEnum, id, () -> this.getById(id), 3, TimeUnit.HOURS);
    }

    /**分页查询*/
    public Page<E> pageList(PageRequestDTO condition) {
        QueryWrapper<E> wrapper = createWrapper(condition);
        if ("asc".equalsIgnoreCase(condition.getPageOrder())) {
            wrapper.orderByAsc(StrUtil.toUnderlineCase(condition.getPageSort()));
        } else {
            wrapper.orderByDesc(StrUtil.toUnderlineCase(condition.getPageSort()));
        }
        return this.page(new Page<>(condition.getPageNumber(), condition.getPageSize()), wrapper);
    }
    /**分页查询*/
    public Page<E> pageList(QueryWrapper<E> wrapper, PageRequestDTO condition) {
        if ("asc".equalsIgnoreCase(condition.getPageOrder())) {
            wrapper.orderByAsc(StrUtil.toUnderlineCase(condition.getPageSort()));
        } else {
            wrapper.orderByDesc(StrUtil.toUnderlineCase(condition.getPageSort()));
        }
        return this.page(new Page<>(condition.getPageNumber(), condition.getPageSize()), wrapper);
    }

    private static final Map<Class<?>, List<MpPredicatePropertyDTO>> MP_PREDICATE_CACHE = new HashMap<>();

    /**创建Wrapper*/
    public QueryWrapper<E> createWrapper(Object condition) {
        QueryWrapper<E> queryWrapper = new QueryWrapper<>();
        //得到类对象
        Class cls = condition.getClass();
        if (PageRequestDTO.class.equals(cls)) {
            return queryWrapper;
        }
        //得到类中的所有属性集合
        List<MpPredicatePropertyDTO> list = MP_PREDICATE_CACHE.computeIfAbsent(cls, aClass -> {
            List<MpPredicatePropertyDTO> propertyDTOS = new LinkedList<>();
            //获得一个类中所有字段列表,包括其父类中的字段
            Field[] fieldArr = ReflectUtil.getFields(cls);
            for (Field field : fieldArr) {
                field.setAccessible(true);
                MpPredicate annotation = field.getAnnotation(MpPredicate.class);
                if (annotation == null) {
                    continue;
                }
                String propertyName = ParamUtil.isBlank(annotation.property()) ? field.getName() : annotation.property();
                propertyName = StrUtil.toUnderlineCase(propertyName);
                MpPredicatePropertyDTO dto = new MpPredicatePropertyDTO();
                dto.setField(field);
                dto.setPropertyName(propertyName);
                dto.setPredicateFunc(annotation.value().getPredicateFunc());
                propertyDTOS.add(dto);
            }
            return propertyDTOS;
        });
        if (!ParamUtil.isBlank(list)) {
            for (MpPredicatePropertyDTO dto : list) {
                Object val = ReflectUtil.getFieldValue(condition, dto.getField());
                if (val == null || val.toString().equals("")) {
                    continue;
                }
                dto.getPredicateFunc().accept(queryWrapper, new MpPredicateDTO(dto.getPropertyName(), val));
            }
        }
        return queryWrapper;
    }


    public void beforeUpdate(E e) {
    }

    /**更新*/
    public E commonUpdate(UpdateRequestDTO entity) {
        E e = getById(entity.getId());
        //进行属性拷贝
        BeanUtil.copyProperties(entity, e, COPY_OPTIONS);
        beforeUpdate(e);
        updateById(e);
        if (definedRedisKey() != null) {
            RedisUtil.delete(definedRedisKey(), e.getId());
        }
        return e;
    }

    /**逻辑删除*/
    public void logicDelete(String[] ids) {
        if (!ParamUtil.isBlank(ids)) {
            UpdateWrapper<E> wrapper = new UpdateWrapper<>();
            wrapper.set("del_flag", true);
            wrapper.in("id", Arrays.asList(ids));
            this.update(wrapper);
            if (definedRedisKey() != null) {
                for (String id : ids) {
                    RedisUtil.delete(definedRedisKey(), id);
                }
            }
        }
    }
}

8、创建core.annotation.url文件

自定义注解CURD

/**
 * @author Created by xxw on 2023-09-04 15:40
 * @Description
 */
@Target({ElementType.METHOD})//作用于参数或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@RequestMapping(value = "/delete", method = RequestMethod.POST)
public @interface DeleteUrl {
}
/**
 * @author Created by xxw on 2023-09-04 15:40
 * @Description
 */
@Target({ElementType.METHOD})//作用于参数或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@RequestMapping(value = "/list", method = RequestMethod.GET)
public @interface ListUrl {
}
/**
 * @author Created by xxw on 2023-09-04 15:40
 * @Description
 */
@Target({ElementType.METHOD})//作用于参数或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@RequestMapping(value = "/save", method = RequestMethod.POST)
public @interface SaveUrl {
}
/**
 * @author Created by xxw on 2023-09-04 15:40
 * @Description
 */
@Target({ElementType.METHOD})//作用于参数或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@RequestMapping(value = "/update", method = RequestMethod.POST)
public @interface UpdateUrl {
}

17、整合上redis

  • 创建rache.redis文件

  • 首先需要创建一个redis工具类:

/**
 * @author Created by xxw on 2023-08-31 17:46
 * @Description Redis工具类
 */
@SuppressWarnings("unchecked")
@Component
public class RedisUtil implements ApplicationContextAware {

    private static RedisTemplate redisTemplate;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        redisTemplate = applicationContext.getBean("redisTemplate", RedisTemplate.class);
    }

    public static Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public static <T> Optional<T> get(RedisKeyInterface keyEnum, String key) {
        Optional<T> optional = Optional.ofNullable((T) redisTemplate.opsForValue().get(buildKey(keyEnum, key)));
        return optional;
    }

    public static <T> T get(RedisKeyInterface keyEnum, String key, Supplier<T> supplier) {
        Optional<T> v = get(keyEnum, key);
        if (!v.isPresent()) {
            T t = supplier.get();
            if (t != null) {
                //若为null,则不保存
                if (t instanceof Collection) {
                    if (!ParamUtil.isBlank((Collection) t)) {
                        //若为空集合,则不保存
                        set(keyEnum, key, t);
                    }
                } else {
                    //若不为集合,则保存
                    set(keyEnum, key, t);
                }
            }
            return t;
        } else {
            return v.get();
        }
    }

    public static <T> T get(RedisKeyInterface keyEnum, String key, Supplier<T> supplier, long time, TimeUnit timeUnit) {
        Optional<T> v = get(keyEnum, key);
        if (!v.isPresent()) {
            T t = supplier.get();
            if (t != null) {
                //若为null,则不保存
                if (t instanceof Collection) {
                    if (!ParamUtil.isBlank((Collection) t)) {
                        //若为空集合,则不保存
                        set(keyEnum, key, t, time, timeUnit);
                    }
                } else {
                    //若不为集合,则保存
                    set(keyEnum, key, t, time, timeUnit);
                }
            }
            return t;
        } else {
            return v.get();
        }
    }

    public static boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public static void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public static void set(RedisKeyInterface keyEnum, String key, Object value) {
        redisTemplate.opsForValue().set(buildKey(keyEnum, key), value);
    }

    public static void set(String key, Object value, long time, TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, time, timeUnit);
    }

    public static void set(RedisKeyInterface keyEnum, String key, Object value, long time, TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(buildKey(keyEnum, key), value, time, timeUnit);
    }

    public static void delete(RedisKeyInterface keyEnum, String key) {
        redisTemplate.delete(buildKey(keyEnum, key));
    }

    public static void vagueDelete(RedisKeyInterface keyEnum, String key) {
        Set keys = redisTemplate.keys(buildKey(keyEnum, key) + "*");
        redisTemplate.delete(keys);
    }

    public static void delete(String key) {
        redisTemplate.delete(key);
    }

    public static void delete(RedisKeyInterface keyEnum) {
        Set<String> keys = redisTemplate.keys(buildPre(keyEnum) + ":*");
        redisTemplate.delete(keys);
    }

    public static boolean getLock(RedisKeyInterface keyEnum, String key, long millisecond) {
        String finalKey = buildKey(keyEnum, key);
        return redisTemplate.opsForValue().setIfAbsent(finalKey, "lock", millisecond, TimeUnit.MILLISECONDS);
    }

    public static void releaseLock(RedisKeyInterface keyEnum, String key) {
        String finalKey = buildKey(keyEnum, key);
        redisTemplate.delete(finalKey);
    }

    private static StringBuilder buildPre(RedisKeyInterface keyEnum) {
        StringBuilder sb = new StringBuilder();
        if (RedisKeyInterface.RedisDeleteType.NOT_DELETE.equals(keyEnum.getDeleteType())) {
            sb.append(RedisKeyInterface.RedisDeleteType.NOT_DELETE.toString()).append(":");
        } else {
            sb.append(RedisKeyInterface.RedisDeleteType.DELETE.toString()).append(":");
        }
        if (!ParamUtil.isBlank(keyEnum.getFolder())) {
            sb.append(keyEnum.getFolder());
        }
        return sb;
    }

    private static String buildKey(RedisKeyInterface keyEnum, String key) {
        StringBuilder sb = buildPre(keyEnum);
        return sb.append(":").append(key).toString();
    }

    public static <T> List<T> getAllKeys(RedisKeyInterface keyEnum) {
        Set<String> keys = redisTemplate.keys(buildPre(keyEnum) + ":*");
        if (!ParamUtil.isBlank(keys)) {
            List list = new ArrayList(keys.size());
            for (String key : keys) {
                T t = (T) redisTemplate.opsForValue().get(key);
                list.add(t);
            }
            return list;
        }
        return new ArrayList();
    }
}
  • 创建一个RedisKey接口
/**
 * @author Created by xxw on 2023-09-01 15:18
 * @Description redisKey接口
 */
public interface RedisKeyInterface {
    /**Redis删除类型*/
    enum RedisDeleteType {
        DELETE, NOT_DELETE
    }

    /**分类名称*/
    String getFolder();

    /**是否随意清空缓存*/
    RedisDeleteType getDeleteType();

    /**key描述*/
    String getKeyDescription();

}
  • 在config中,对redis、redisson进行配置
/**
 * @author Created by xxw on 2023-08-31 17:48
 * @Description Redis配置
 */
@Component
public class RedisConfiguration {
    /**
     * @param redisConnectionFactory redisConnectionFactory
     * @return redis模板
     */
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        ObjectMapper objectMapper = new ObjectMapper();
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        return redisTemplate;
    }
}
@Component
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfiguration {

    private Integer dataBase = 0;
    private String password;
    private String host;
    private String port;

    @Bean
    public RedissonClient getRedissonClient() {
        Config config = new Config();
        config.setCodec(new JsonJacksonCodec());
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://" + host + ":" + port);
        singleServerConfig.setDatabase(dataBase);
        singleServerConfig.setPassword(password);
        return Redisson.create(config);
    }
}

9、最后加上swagger

<!-- swagger-API获取包 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
    <exclusions>
        <exclusion>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>1.5.21</version>
</dependency>
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-models</artifactId>
    <version>1.5.21</version>
</dependency>
<!-- swagger-UI界面 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
<!--整合Knife4j-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.4</version>
</dependency>

至此,核心部分代码完成

目录:

user-core
  └─src
      └─main
         └─java
             └─com
                 └─xxw
                     └─core 
                         ├─annotation                   ``自定义注解``
                         │  └─url
                         │          DeleteUrl.class             ``删除``
                         │          ListUrl.class               ``查询``
                         │          SaveUrl.class               ``新增``
                         │          UpdateUrl.class             ``修改``
                         ├─cache                        ``缓存``
                         │  ├─redis                         ``redis``
                         │  │      RedisKeyInterface.java       ``RedisKey接口``
                         │  │      RedisUtil.java               ``RedisUtil工具类``
                         │  │
                         │  └─resp                          ``resp``
                         │          RespKeyInterface.java       ``RespKey接口``
                         │
                         ├─config                       ``配置``
                         │      MybatisPlusConfig.java      ``MyBatisPlus分页插件配置``
                         │      MyMetaObjectHandler.java    ``MybatisPlus公共字段自动填充``
                         │      RedisConfiguration.java     ``Redis配置``
                         │      RedissonConfiguration.java  ``Redisson配置``
                         │      RespKeyEnum.java            ``resp枚举``
                         │
                         ├─exception                    ``异常``
                         │      BaseException.java          ``基本异常``
                         │
                         ├─model                        ``数据承载实体``
                         │      Resp.java                   ``统一Resp``
                         │
                         ├─orm                          ``orm``
                         │  ├─annotation                    ``注解``
                         │  │      MpPredicate.java             ``MybatisPlus自定义注解``
                         │  │   
                         │  ├─controller                    ``控制器``
                         │  │      BaseContent.java             ``基础内容``
                         │  │      BaseController.java          ``基础控制器,继承于BaseContent``
                         │  │   
                         │  ├─domain                        ``领域``
                         │  │      BaseEntity.java              ``基础实体``
                         │  │   
                         │  ├─dto                           ``数据传输对象``
                         │  │      MpPredicateDTO.java          ``MybatisPlus条件构造器DTO``
                         │  │      MpPredicatePropertyDTO.java  ``MybatisPlus条件构造器各属性DTO``
                         │  │      PageRequestDTO.java          ``分页``
                         │  │      UpdateRequestDTO.java        ``更新``
                         │  │
                         │  ├─enumeration                   ``枚举``
                         │  │      MpPredicateEnum.java         ``MybatisPlus条件构造器枚举类``
                         │  │
                         │  └─service                       ``服务``
                         │          BaseService.java            ``基础服务``
                         │
                         └─util                         ``工具``
                                 ParamUtil.java             ``参数判断工具类``


test模块:

1、配置swagger

创建config.SwaggerCongig

/**
 * @author Created by xxw on 2023-09-04 16:19
 * @Description Swagger配置
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    /**文档名*/
    @Value("${swagger.title}")
    private String title;
    /**简介*/
    @Value("${swagger.description}")
    private String description;
    /**项目的版本*/
    @Value("${swagger.version}")
    private String version;
    /**服务URL*/
    @Value("${swagger.termsOfServiceUrl}")
    private String termsOfServiceUrl;

    @Bean
    public Docket createRestApiApp() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(this.apiInfo())
                .select()
                // 扫描带@ApiOperation注解的方法注册进swagger
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                //所有路径
                .paths(PathSelectors.any())
                //指定路径
                //.paths(PathSelectors.ant("/xxw/**"))
                .build()
                .groupName("用户端接口")
                .ignoredParameterTypes(ApiIgnore.class)
                .globalOperationParameters(this.getHeaderParameters());
    }
    /**获取标头参数*/
    public List<Parameter> getHeaderParameters() {
        List<Parameter> pars = new ArrayList<>();
        ParameterBuilder token = new ParameterBuilder();
        pars.add(token.name("accessToken")
                .description("用户登录凭证")
                .modelRef(new ModelRef("String"))
                .parameterType("header")
                .required(false)
                .build());
        ParameterBuilder client = new ParameterBuilder();
        pars.add(client.name("client")
                .defaultValue("customer")
                .description("用户当前客户端名称,固定填:customer")
                .modelRef(new ModelRef("String"))
                .parameterType("header")
                .required(false)
                .build());
        return pars;

    }

    /**api信息*/
    public ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                //标题
                .title(title)
                //描述
                .description(description)
                //版本
                .version(version)
                //地址
                .termsOfServiceUrl(termsOfServiceUrl)
                .build();
    }
}

2、创建启动类

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @author Created by xxw on 2023-09-04 16:10
 * @Description
 */
/**扫描发现包中的Entity定义*/
@EntityScan
/**swagger*/
@EnableSwagger2
/**指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类*/
@MapperScan({"com.xxw.test.*.*.mapper"})
/**扫描某com.xxw包中所有的类,然后将满足一定条件的类作为bean注册到spring容器容器中*/
@ComponentScan(basePackages = {"com.xxw"})
/**标记了一个类作为Spring Boot应用程序的入口点。
 * 并且这是一个复合注解,包含了 @Configuration@EnableAutoConfiguration
 * 和 @ComponentScan 三个注解。使用此注解可以标识一个主类,并且会自动配置 Spring Boot 应用程序。*/
@SpringBootApplication(exclude = {FreeMarkerAutoConfiguration.class})
public class XxwTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(XxwTestApplication.class, args);
    }
}

3、连接mysql数据库

image-20230904170754544.png

输入账号密码数据库,点击Test Connection测试链接,显示Succeeded连接成功

image-20230904172417802.png

连接后可以看到数据表,选择需要生成的表,MybatisX-Generator(这边使用到了一个MybatisX,非常好用,强烈推荐!)

4、自动生成实体

image-20230905114026990.png

根据项目需求,选择并填写一下指定内容,其中类名可以选择驼峰或者与表明相同,自行选择

image-20230905114756454.png

Next

image-20230905115322007.png

预览生成的文件,可以全部删除,我们使用模板来生成controller、mapper、service,这边只是用MybatisX来生成实体类

image-20230905115624300.png

在实体类中,我们需要修改一些东西:

  • 首先先继承我们自己的基础实体:extends BaseEntity

  • 生成的实体中只有两个注解:@Date:lombok、@TableName("tb_student"):设置实体类对应的表名,我们需要加上我们自己需要的:

    • @Entity:说明这个 class 是实体类,并且使用默认的 orm 规则,即 class 名即数据库表中表名,class 字段名即表中的字段名。@Entity 注解指名这是一个实体 Bean

    • @Table(name = "tb_student"):注解指定了 Entity 所要映射带数据库表,其中 @Table.name() 用来指定映射表

      的表名

    • @ApiModel(description = "学生表"):swagger描述信息,加在实体类上

    • @ApiModelProperty:实体类中的参数添加额外的描述信息,加在字段上

image-20230905142636697.png

5、自定义模板

创建资源文件resources下创建code_template文件,自定义controller、mapper、service模板:

controller
package ${controllerPackage};

//这里需要修改路径
import com.xxw.core.orm.controller.BaseController;
import com.xxw.core.orm.service.BaseService;
import ${domainPath};
import ${mapperPackage}.${domainName}Mapper;
import ${servicePackage}.${domainName}Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

/**
* @author Created by xxw on $dateTime$
* @Description $END$
*/
@RestController
@RequestMapping("/test/${lowerFirstDomainName}")//这里修改访问路径
public class ${domainName}Controller extends BaseController<${domainName}Mapper, ${domainName}> {

    @Resource
    private ${domainName}Service ${lowerFirstDomainName}Service;

    @Override
    public BaseService<${domainName}Mapper, ${domainName}> getService() {
        return ${lowerFirstDomainName}Service;
    }
}
mapper
package ${mapperPackage};

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import ${domainPath};

/**
* @author Created by xxw on $dateTime$
* @Description $END$
*/
public interface ${domainName}Mapper extends BaseMapper<${domainName}> {
}
service
package ${servicePackage};

import com.xxw.core.orm.service.BaseService;
import ${domainPath};
import ${mapperPackage}.${domainName}Mapper;
import org.springframework.stereotype.Service;

/**
* @author Created by xxw on $dateTime$
* @Description $END$
*/
@Service
public class ${domainName}Service extends BaseService<${domainName}Mapper, ${domainName}> {
}

6、代码生成

新建generator文件,创建两个类,GeneratorInfo生成器信息,Generator代码生成器:

GeneratorInfo

/**
 * @author Created by xxw on 2023-09-05 14:33
 * @Description 生成器信息
 */
@Data
public class GeneratorInfo {
    private String domainName;
    private String lowerFirstDomainName;
    private String domainPath;
    private String domainPackage;

    private String mapperPackage;
    private String servicePackage;
    private String controllerPackage;



    private String resPath;
    private String mapperResPath;
    private String serviceResPath;
    private String controllerResPath;

    public void setDomainName(String domainName) {
        this.domainName = domainName;
        this.lowerFirstDomainName = StrUtil.lowerFirst(domainName);
    }

    public void setDomainPackage(String domainPackage) {
        this.domainPackage = domainPackage;
        this.mapperPackage = domainPackage+".mapper";
        this.servicePackage = domainPackage+".service";
        this.controllerPackage = domainPackage+".controller";
    }

    public void setResPath(String resPath) {
        this.resPath = URLDecoder.decode(resPath, StandardCharsets.UTF_8);;
        this.mapperResPath = this.resPath + "/mapper/";
        this.serviceResPath = this.resPath + "/service/";
        this.controllerResPath = this.resPath + "/controller/";
    }
}

Generator:

/**
 * @author Created by xxw on 2023-09-05 14:33
 * @Description 代码生成器
 */
public class Generator {
    private Configuration configuration;

    /**完成环境初始化*/
    public void init() throws Exception {//抛出为最大异常
        //1-1实例化
        configuration = new Configuration(Configuration.getVersion());
        //1-2设置模版所在路径
        String path = this.getClass().getClassLoader().getResource("").getPath();
        configuration.setDirectoryForTemplateLoading(new File(path));
    }

    /**转换*/
    private List<GeneratorInfo> convert(List<Class<? extends BaseEntity>> clsList) {
        return clsList.stream().map(cls -> {
            GeneratorInfo info = new GeneratorInfo();
            // com.xxw.test.model.student.domain
            String path = cls.getPackage().getName();
            // com.xxw.test.model.student
            String domainPackage = StrUtil.sub(cls.getPackage().getName(), 0, path.lastIndexOf("."));
            //com.xxw.test.model.student.domain.Student
            String domainPath = cls.getName();
            //Student
            String domainName = cls.getSimpleName();
            info.setDomainName(domainName);
            info.setDomainPackage(domainPackage);
            info.setDomainPath(domainPath);
            String resourcePath = cls.getResource("").getPath();
            String abPath = StrUtil.subBefore(resourcePath, "target/classes/", false)+"src/main/java/";
            abPath = StrUtil.sub(abPath,1,abPath.length());
            info.setResPath(abPath+StrUtil.replace(info.getDomainPackage(),".","/"));

            return info;
        }).collect(Collectors.toList());
    }

    //生成代码
    public void process(List<Class<? extends BaseEntity>> clsList) throws IOException, TemplateException {
        //获取模版对应实例
        Template mapperTemp = configuration.getTemplate("/code_template/mapper.ftl");
        Template serviceTemp = configuration.getTemplate("/code_template/service.ftl");
        Template controllerTemp = configuration.getTemplate("/code_template/controller.ftl");
        //组装数据
        List<GeneratorInfo> generatorInfos = this.convert(clsList);
        for (GeneratorInfo generatorInfo : generatorInfos) {
            //初始化保存路径
            // FileOutputStream stream = new FileOutputStream(savePath);
            // OutputStreamWriter outputStreamWriter = new OutputStreamWriter(stream);
            Map<String, Object> param = BeanUtil.beanToMap(generatorInfo);
            String mapperFilePath = generatorInfo.getMapperResPath() + generatorInfo.getDomainName() + "Mapper.java";
            // D:\dev\test\xxwdemo\ user-test\src\main\java\com\xxw\test\model\student\mapper\StudentMapper
            if (!FileUtil.exist(mapperFilePath)) {
                if (!FileUtil.exist(generatorInfo.getMapperResPath())) {
                    FileUtil.mkdir(generatorInfo.getMapperResPath());
                }
                FileOutputStream stream = new FileOutputStream(mapperFilePath);
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(stream);
                //传参,生成数据
                mapperTemp.process(param, outputStreamWriter);
            }

            String serviceFilePath = generatorInfo.getServiceResPath() + generatorInfo.getDomainName() + "Service.java";
            if (!FileUtil.exist(serviceFilePath)) {
                if (!FileUtil.exist(generatorInfo.getServiceResPath())) {
                    FileUtil.mkdir(generatorInfo.getServiceResPath());
                }
                FileOutputStream stream = new FileOutputStream(serviceFilePath);
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(stream);
                //传参,生成数据
                serviceTemp.process(param, outputStreamWriter);
            }


            String controllerFilePath = generatorInfo.getControllerResPath() + generatorInfo.getDomainName() + "Controller.java";
            if (!FileUtil.exist(controllerFilePath)) {
                if (!FileUtil.exist(generatorInfo.getControllerResPath())) {
                    FileUtil.mkdir(generatorInfo.getControllerResPath());
                }
                FileOutputStream stream = new FileOutputStream(controllerFilePath);
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(stream);
                //传参,生成数据
                controllerTemp.process(param, outputStreamWriter);
            }


        }

    }

    public static void main(String[] args) throws IOException {
        //将类进行实例化
        Generator generator = new Generator();
        try {
            generator.init();
            // D:/dev/test/xxwdemo/user-test/src/main/java
            // D:\dev\test\xxwdemo\ user-test\src\main\java
            generator.process(Arrays.asList(Student.class));
        } catch (Exception e) {
            e.printStackTrace();
        }

         //Class<Student> cls = Student.class;
         //File f = new File("");
         //System.out.println(f.getCanonicalPath());
         //System.out.println(System.getProperty("user.dir"));
         //System.out.println(cls.getResource("").getPath());
         //String path = cls.getResource("").getPath();
         //System.out.println(StrUtil.sub(path,1,StrUtil.indexOf(path,"/target",1,false))+"/src/main/java");
         //System.out.println(StrUtil.replace("\\" + cls.getPackage().getName(), ".", "\\"));
         //String a = "D:/dev/test/xxwdemo/user-test/target/classes/com/xxw/test/model/domain/src/main/java/target";
         //String s = StrUtil.subBefore(a, "target/classes/", false);
         //System.out.println(7);
    }
}

特别注意:generator.process(Arrays.asList(Student.class));中Student.class为要生产代码的实体,需要替换!

生成后的目录:

image-20230905145239548.png

测试

StudentController中,直接重写list,使用分页查询所有学生信息

@Override
@ListUrl
@ApiOperation(value = "学生列表")
public Resp<Page<Student>> list(PageRequestDTO condition) {
    Page<Student> activityPage = studentService.pageList(condition);
    return success(activityPage);
}

对配置文件进行详细配置,配置redis,mysql,swagger等(本地redis需要启动):

#生产环境
#配置内嵌 Tomcat 默认端口号
server:
  port: 8081
  servlet:
    context-path: /api
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/xxwtest?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
  application:
    name: user-test
  redis:
    port: 6379
    host: 127.0.0.1

swagger:
  title: xxwAPI文档
  description: xxwAPI文档介绍
  version: 1.0
  termsOfServiceUrl: http://127.0.0.1:${server.port}${server.servlet.context-path}/doc.html

启动项目,http://localhost:8081/api/doc.html,进入swagger接口文档

image-20230905145711194.png

查看接口

image-20230905145822914.png

调试

image-20230905150007888.png

由于是测试接口,查询所有学生信息,所以入参为PageRequestDTO分页查询,如需要给定参数,则可以创建DTO继承PageRequestDTO后,入参查询

查询后结果:

image-20230905150122500.png