什么是 Spring Boot? 如果把 Java 开发比作开一家餐厅,传统的 Spring 框架就像是你买下了一块空地,你需要自己设计厨房、寻找厨具供应商、装修排烟系统、配置煤气管道。在你炒出第一盘青椒肉丝之前,可能已经忙活了三个月。而 Spring Boot 就是一家 “精装修的共享厨房”。它不仅帮你把炉灶、锅铲、调料都摆好了,甚至连火候都预设到了最合适的状态。你只需要带着食材(业务代码)进去,开火即炒,瞬间出餐。
诞生背景 在 2014 年 Spring Boot 1.0 正式发布之前,Java 开发者正经历着一段 “黑暗时期”:
XML 配置地狱: 那时的 Spring 被戏称为“配置框架”。写一个简单的 Hello World,你可能需要配置数百行的 XML 文件。开发者自嘲:“半天写代码,半天调配置”。
微服务浪潮的倒逼: 2013 年前后,Martin Fowler 提出了微服务 (Microservices) 概念。微服务要求把一个大系统拆成几十个小服务。如果每个小服务都要折腾半天配置、手动部署 Tomcat,那开发效率将是毁灭性的。
竞争对手的压力: 当时 Node.js 和 Python 的 Web 框架(如 Express, Flask)以“几行代码启动服务”的简洁性吸引了大量开发者。Spring 必须进化,否则就会被时代抛弃。
于是,Pivotal 团队在 2014 年 推出了 Spring Boot,目标只有一个:让 Spring 再次变得简单。
核心体系 Spring Boot 并不是凭空创造的新技术,它是站在 Spring 这个巨人肩膀上的“自动化管家”。它的核心理念是:约定优于配置 (Convention over Configuration) 。
自动配置 (Auto-Configuration) —— 告别配置地狱:
以前你需要手动告诉 Spring:“我要用 MySQL,驱动是这个,连接池是那个”。
Spring Boot 的出现,就如同智能手机。以前的手机连 WiFi 要设置 IP、子网掩码、网关;现在的手机只要点一下 WiFi 名,剩下的它自己搞定。
Spring Boot 启动时会利用 @EnableAutoConfiguration 扫描你引入的 jar 包。如果你引入了 Redis 的包,它就推断你要用 Redis,并自动在内存里帮你创建好 RedisTemplate 实例。
起步依赖 (Starter POMs) —— 依赖管理标准化:
以前引入 A 包,可能需要手动引入 B、C、D 及其对应的兼容版本,经常发生版本冲突(依赖地狱)。
而 Spring Boot 就像点外卖套餐。你不用单点米饭、肉、筷子,直接点一个 “鱼香肉丝套餐”,所有配套的东西由餐厅(Spring 官方)帮你配好。
它将原本分散的依赖进行了垂直打包。比如引入 spring-boot-starter-web,它会自动帮你下载 Spring MVC、Jackson(JSON解析)、Tomcat 等一整套经过官方兼容性测试的组件。
内嵌容器 (Embedded Containers) —— 迈向云原生:
以前部署 Java 应用需要:安装 Tomcat -> 配置环境变量 -> 拷贝 war 包。这在需要快速扩容的云环境中太慢了。
以前你要跑代码,得先买个大冰箱(Tomcat)把菜塞进去。现在 Spring Boot 把冰箱缩小成一个 “车载冷藏箱”,直接集成在菜里,提着就能走。
打包后的 Spring Boot 程序是一个独立的 “.jar” 文件。通过 “java -jar” 命令直接启动,因为服务器(Tomcat/Jetty)已经作为代码的一部分运行了。这是实现 Docker 容器化 和 CI/CD 自动化部署 的前提。
三分钟开发一个 SB 应用 可以参考《Spring Boot 简介及 hello world 应用的编写》 、《RESTful 简介以及基于 SSM 的完整案例》 、《Redis 集群搭建之主从哨兵客户端演示》 、《Redis 集群搭建之集群cluster模式的客户端构建》 、《Redis 支持读写分离的 RedisJSON 客户端组件》 、《Redis 集群模式下支持读写分离的 RediSearch 简单自研组件》 、《Redis 高级客户端 Redisson 的使用》 、《Shiro的工程实践》 等等。
两种 POM 的组织方式 第一种方式:继承派
这是官方最推荐新手使用的方式,也是 “全家桶” 模式。优点是极简且省心,SB 自动配置了 Java 编译版本、编码格式(UTF-8)、资源过滤(Resource Filtering), 预置了大量的插件配置(如 spring-boot-maven-plugin),你不需要自己写插件的版本号。缺点是太霸道,Maven 的 parent 标签是单继承的。如果你所属的公司要求项目必须继承公司统一的 “企业父 POM”(例如 company-parent),这种方式就直接哑火了。
1 2 3 4 5 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 3.5.15.RELEASE</version > </parent >
第二种方式:组合派
这是一种 “按需取经” 的模式。优点是自由解耦,不占用 parent 位置。你的项目可以继承自公司的父 POM,同时依然享受 Spring Boot 的版本管理。它只负责 “统一版本号”,不会强加给你额外的资源过滤配置或插件设置。缺点是稍显琐碎,你需要手动配置 Java 编译版本,且在使用 spring-boot-maven-plugin 等插件时,必须手动指定版本号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <properties > <java.version > 17</java.version > <maven.compiler.source > 17</maven.compiler.source > <maven.compiler.target > 17</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <spring-boot.version > 3.5.13</spring-boot.version > <logback.version > 1.5.25</logback.version > <lombok.version > 1.18.42</lombok.version > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > ${spring-boot.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement >
在实际正规的商业开发中,通常采用 “公司父 POM + Spring Boot 导入” 的组合模式,这为架构扩展留出了余地。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 <project > <parent > <groupId > com.yourcompany</groupId > <artifactId > corporate-parent</artifactId > <version > 1.0.0</version > </parent > <properties > <java.version > 17</java.version > <maven.compiler.source > 17</maven.compiler.source > <maven.compiler.target > 17</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <spring-boot.version > 3.5.9</spring-boot.version > <org.projectlombok.version > 1.18.30</org.projectlombok.version > <org.mapstruct.version > 1.5.5.Final</org.mapstruct.version > <lombok-mapstruct-binding.version > 0.2.0</lombok-mapstruct-binding.version > <logback.version > 1.5.25</logback.version > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > ${spring-boot.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <dependencies > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > ${spring-boot.version}</version > <configuration > <mainClass > com.demo.App</mainClass > </configuration > <executions > <execution > <goals > <goal > repackage</goal > </goals > </execution > </executions > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.11.0</version > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <annotationProcessorPaths > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${org.projectlombok.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok-mapstruct-binding</artifactId > <version > ${lombok-mapstruct-binding.version}</version > </path > <path > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${org.mapstruct.version}</version > </path > </annotationProcessorPaths > <compilerArgs > <arg > -parameters</arg > </compilerArgs > <parameters > true</parameters > </configuration > </plugin > </plugins > </build > <repositories > <repository > <id > public</id > <name > aliyun nexus</name > <url > https://maven.aliyun.com/repository/public</url > <releases > <enabled > true</enabled > </releases > </repository > <repository > <id > central</id > <name > Central Repository</name > <url > https://repo.maven.apache.org/maven2</url > <layout > default</layout > <snapshots > <enabled > false</enabled > </snapshots > </repository > </repositories > <pluginRepositories > <pluginRepository > <id > public</id > <name > aliyun nexus</name > <url > https://maven.aliyun.com/repository/public</url > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > false</enabled > </snapshots > </pluginRepository > </pluginRepositories > </project >
关于依赖的查找顺序 Maven 并不是同时对比所有仓库看谁优先级高,而是按照 由近及远 的顺序查找:
本地仓库 (Local Repository):你的 localRepository 文件夹(默认 .m2/repository)。如果有,直接用。
远程仓库 (Remote Repositories):如果本地没有,就开始找远程的。寻找顺序如下:
第一步:检查 settings.xml 中的 profiles。
第二步:检查 pom.xml 中的 repositories。
第三步:如果以上都没配置,最后找 Maven 默认的 Central。
mirrorOf 不是参与排队的优先级,它是强行拦截:不管你在 pom.xml 里配置了多少个 repository,一旦 settings.xml 中配置了 *(镜像所有),Maven 会无视你配置的所有 URL,强行把请求发往镜像地址。
执行远程访问时,如果在 settings.xml 的 activeProfiles 里配置了仓库,它们最先被访问。settings.xml 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 <?xml version="1.0" encoding="UTF-8" ?> <settings xmlns ="http://maven.apache.org/SETTINGS/1.2.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd" > <localRepository > /Library/apache-maven-3.8.5/repository</localRepository > <interactiveMode > true</interactiveMode > <pluginGroups > <pluginGroup > org.springframework.boot</pluginGroup > </pluginGroups > <servers > <server > <id > owlias-snapshots</id > <username > xxx</username > <password > xxx</password > </server > <server > <id > owlias-releases</id > <username > xxx</username > <password > xxx</password > </server > </servers > <mirrors > <mirror > <id > aliyun-mirror</id > <mirrorOf > central</mirrorOf > <name > 阿里云公共仓库</name > <url > https://maven.aliyun.com/repository/public</url > </mirror > </mirrors > <profiles > <profile > <id > oss-development</id > <repositories > <repository > <id > central</id > <url > https://repo.maven.apache.org/maven2</url > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > false</enabled > </snapshots > </repository > <repository > <id > spring-milestones</id > <url > https://repo.spring.io/milestone</url > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > false</enabled > </snapshots > </repository > <repository > <id > spring-snapshots</id > <url > https://repo.spring.io/snapshot</url > <releases > <enabled > false</enabled > </releases > <snapshots > <enabled > true</enabled > <updatePolicy > interval:15</updatePolicy > </snapshots > </repository > </repositories > <pluginRepositories > <pluginRepository > <id > central</id > <url > https://repo.maven.apache.org/maven2</url > </pluginRepository > <pluginRepository > <id > spring-milestones</id > <url > https://repo.spring.io/milestone</url > </pluginRepository > </pluginRepositories > </profile > </profiles > <activeProfiles > <activeProfile > oss-development</activeProfile > </activeProfiles > </settings >
POM 文件中的 Profiles:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <profiles > <profile > <id > dev</id > <properties > <env.type > dev</env.type > </properties > <activation > <activeByDefault > true</activeByDefault > </activation > </profile > <profile > <id > prod</id > <properties > <env.type > prod</env.type > </properties > <distributionManagement > <repository > <id > owlias-releases</id > <url > http://xxx:8081/repository/maven-releases/</url > </repository > <snapshotRepository > <id > owlias-snapshots</id > <url > http://xxx:8081/repository/maven-snapshots/</url > </snapshotRepository > </distributionManagement > </profile > </profiles > <build > <resources > <resource > <directory > src/main/resources</directory > <filtering > true</filtering > </resource > </resources > </build >
构建一个最基础的应用框架 这个初始项目主要介绍了一些开发中的一些常用技术,包括:怎样热部署、swagger文档管理、典型的分环境配置文件、springmvc常用技术、典型的logback-spring日志、MybatisPlus型ORM框架(内置接口及简化开发方式、创建和更新时间处理、业务字段、敏感字段、逻辑删除字段、枚举字段、自定义mapper接口或配置接口、分页插件和自定义分页查询、简单多表查询)、参数各种校验及分组校验、lombok和mapstruct简化开发等。
依赖配置 pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 <?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.5.9</version > <relativePath /> </parent > <groupId > com.koohub.demo01</groupId > <artifactId > demo01-init-springboot-project</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > demo01-init-springboot-project</name > <properties > <java.version > 17</java.version > <org.projectlombok.version > 1.18.30</org.projectlombok.version > <org.mapstruct.version > 1.5.5.Final</org.mapstruct.version > <lombok-mapstruct-binding.version > 0.2.0</lombok-mapstruct-binding.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${org.projectlombok.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > ${org.mapstruct.version}</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${org.mapstruct.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.springdoc</groupId > <artifactId > springdoc-openapi-starter-webmvc-ui</artifactId > <version > 2.8.16</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-spring-boot3-starter</artifactId > <version > 3.5.7</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.16</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > ${spring-boot.version}</version > <executions > <execution > <goals > <goal > repackage</goal > </goals > </execution > </executions > <configuration > <excludeDevtools > true</excludeDevtools > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.11.0</version > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <annotationProcessorPaths > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${org.projectlombok.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok-mapstruct-binding</artifactId > <version > ${lombok-mapstruct-binding.version}</version > </path > <path > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${org.mapstruct.version}</version > </path > </annotationProcessorPaths > <compilerArgs > <arg > -parameters</arg > </compilerArgs > <parameters > true</parameters > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-surefire-plugin</artifactId > <version > 3.2.5</version > <configuration > <skipTests > false</skipTests > <testFailureIgnore > true</testFailureIgnore > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-enforcer-plugin</artifactId > <version > 3.4.1</version > <executions > <execution > <id > enforce-no-javax-servlet</id > <goals > <goal > enforce</goal > </goals > <configuration > <rules > <bannedDependencies > <message > 检测到 Owlias 中混入了旧版的 javax.servlet 依赖,必须使用 jakarta.servlet。 请检查 Dependency Analyzer 找出是谁引入了 shiro-web (javax版) 或 javax.servlet-api! </message > <excludes > <exclude > javax.servlet:javax.servlet-api</exclude > <exclude > javax.servlet:servlet-api</exclude > <exclude > org.apache.shiro:shiro-web:*:jar:!(jakarta)</exclude > </excludes > </bannedDependencies > </rules > <fail > true</fail > </configuration > </execution > </executions > </plugin > </plugins > </build > </project >
日志文件配置 src/main/resources/logback-spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?xml version="1.0" encoding="UTF-8" ?> <configuration scan ="true" scanPeriod ="60 seconds" > <property name ="LOG_PATH" value ="./logs" /> <property name ="CONSOLE_LOG_PATTERN" value ="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta(${PID:- }) --- [%15.15thread] %cyan(%-40.40logger{39}) : %m%n" /> <property name ="FILE_LOG_PATTERN" value ="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- [%thread] %logger{50} - [%method,%line] - %m%n" /> <appender name ="CONSOLE" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > ${CONSOLE_LOG_PATTERN}</pattern > <charset > UTF-8</charset > </encoder > </appender > <appender name ="INFO_FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${LOG_PATH}/sys-info.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy" > <fileNamePattern > ${LOG_PATH}/archive/sys-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern > <maxFileSize > 100MB</maxFileSize > <maxHistory > 30</maxHistory > <totalSizeCap > 30GB</totalSizeCap > </rollingPolicy > <encoder > <pattern > ${FILE_LOG_PATTERN}</pattern > <charset > UTF-8</charset > </encoder > <filter class ="ch.qos.logback.classic.filter.LevelFilter" > <level > ERROR</level > <onMatch > DENY</onMatch > <onMismatch > ACCEPT</onMismatch > </filter > </appender > <appender name ="ERROR_FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${LOG_PATH}/sys-error.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy" > <fileNamePattern > ${LOG_PATH}/archive/sys-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern > <maxFileSize > 100MB</maxFileSize > <maxHistory > 60</maxHistory > </rollingPolicy > <encoder > <pattern > ${FILE_LOG_PATTERN}</pattern > <charset > UTF-8</charset > </encoder > <filter class ="ch.qos.logback.classic.filter.ThresholdFilter" > <level > ERROR</level > </filter > </appender > <root level ="INFO" > <appender-ref ref ="CONSOLE" /> <appender-ref ref ="INFO_FILE" /> <appender-ref ref ="ERROR_FILE" /> </root > </configuration >
主要配置文件 application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 spring: application: name: owlias main: banner-mode: CONSOLE profiles: active: dev messages: basename: static/i18n/messages jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss server: port: 8848 context-path: / tomcat: uri-encoding: UTF-8 accept-count: 1000 threads: max: 800 min-spare: 100 spring: application: name: demo01 main: banner-mode: CONSOLE profiles: active: dev logging: level: org.springframework.web.servlet.PageNotFound: ERROR org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: ERROR
application-dev.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 logging: level: root: info com.koohub: debug com.koohub.demo01.mapper: info org.springframework.web: info org.springframework: info spring: devtools: restart: enabled: true additional-paths: src/main/java, src/main/resources exclude: static/** mvc: static-path-pattern: static/** web: resources: static-locations: classpath:/templates/,classpath:/static servlet: multipart: max-file-size: 10MB max-request-size: 10MB datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://xxx:3306/zdemo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: xxx password: xxx springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: enabled: true path: /swagger-ui.html openapi: info: title: Springboot3简单项目的演示-${spring.profiles.active}-接口文档 description: Springboot3简单项目的演示文档。 version: 1.0 .0 contact: name: 技术支持-KJ email: support@koohub.com license: name: Apache 2.0 external-docs: description: 项目设计文档 url: https://github.com/koohub/docs group-configs: - group: 'default' paths-to-match: '/**' packages-to-scan: com.koohub.demo01.controller mybatis-plus: global-config: banner: false db-config: logic-delete-field: deleted logic-not-delete-value: 0 logic-delete-value: 1 mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.koohub.demo01.model.entity configuration: cache-enabled: true default-executor-type: simple default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler map-underscore-to-camel-case: true
application-prod.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 spring: devtools: restart: enabled: false mvc: static-path-pattern: static/** web: resources: static-locations: classpath:/templates/,classpath:/static servlet: multipart: max-file-size: 10MB max-request-size: 10MB datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://xxx:3306/zdemo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: xxx password: xxx springdoc: api-docs: enabled: false swagger-ui: enabled: false mybatis-plus: global-config: banner: false db-config: logic-delete-field: deleted logic-not-delete-value: 0 logic-delete-value: 1 mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.koohub.demo01.model.entity configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler map-underscore-to-camel-case: true
启动类 1 2 3 4 5 6 @SpringBootApplication public class Demo01Application { public static void main (String[] args) { SpringApplication.run(Demo01Application.class, args); } }
主要配置类 WebConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import com.koohub.demo01.interceptor.LoginInterceptor;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.CorsRegistry;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor ()).order(1 ).addPathPatterns("/api/test/**" ); } @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ) .allowedOriginPatterns("*" ) .allowedMethods("GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" ) .allowedHeaders("*" ) .allowCredentials(true ) .maxAge(7200 ); } }
MybatisPlusConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;import org.mybatis.spring.annotation.MapperScan;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration @MapperScan("com.koohub.demo01.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor (DbType.MYSQL); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }
MybatisPlusHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;import org.apache.ibatis.reflection.MetaObject;import org.springframework.stereotype.Component;import java.util.Date;@Component public class MybatisPlusHandler implements MetaObjectHandler { @Override public void insertFill (MetaObject metaObject) { this .strictInsertFill(metaObject, "createTime" , Date.class, new Date ()); this .strictInsertFill(metaObject, "updateTime" , Date.class, new Date ()); this .strictInsertFill(metaObject, "deleted" , Integer.class, 0 ); } @Override public void updateFill (MetaObject metaObject) { this .strictUpdateFill(metaObject, "updateTime" , Date.class, new Date ()); } }
MapStructConfig
1 2 3 4 5 6 import org.mapstruct.MapperConfig;import org.mapstruct.ReportingPolicy;@MapperConfig(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) public class MapStructConfig {}
公共的 Model BaseEntity
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @Accessors(chain = true) public class BaseEntity implements Serializable { @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; @TableLogic private int deleted; }
BasePageQuery
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @Schema(description = "公共分页查询基础类") public class BasePageQuery implements Serializable { @Schema(description = "当前页码", example = "1") private Integer pageNum = 1 ; @Schema(description = "每页条数", example = "10") @Max(value = 200, message = "单页条数超限") private Integer pageSize = 10 ; @Schema(hidden = true) public Integer getOffset () { return (pageNum - 1 ) * pageSize; } }
PageResult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Data @AllArgsConstructor @NoArgsConstructor @Schema(description = "统一分页返回结果") public class PageResult <T> implements Serializable { @Schema(description = "总记录数") private Long total; @Schema(description = "结果列表") private List<T> list; public static <T> PageResult<T> of (Long total, List<T> list) { return new PageResult <>(total, list); } }
ValidGroups
1 2 3 4 5 6 7 public interface ValidGroups { interface Insert {} interface Update {} }
Result
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Data public class Result <T> implements Serializable { private int code; private String msg; private T data; private long timestamp; private Result () { this .timestamp = System.currentTimeMillis(); } public static <T> Result<T> success (T data) { Result<T> result = new Result <>(); if (data == null ) { result.setCode(ResultCode.NO_DATA.getCode()); result.setMsg(ResultCode.NO_DATA.getMessage()); return result; } result.setCode(ResultCode.SUCCESS.getCode()); result.setMsg(ResultCode.SUCCESS.getMessage()); result.setData(data); return result; } public static <T> Result<T> success () { Result<T> result = new Result <>(); result.setCode(ResultCode.SUCCESS.getCode()); result.setMsg(ResultCode.SUCCESS.getMessage()); return result; } public static <T> Result<T> fail (int code, String msg) { Result<T> result = new Result <>(); result.setCode(code); result.setMsg(msg); return result; } public static <T> Result<T> fail (ResultCode resultCode) { return fail(resultCode.getCode(), resultCode.getMessage()); } }
ResultCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Getter public enum ResultCode { FAILURE(-1 , "业务异常" ), SUCCESS(0 , "操作成功" ), NO_DATA(2 , "数据不存在" ), VALID_ERROR(3 , "参数异常" ), UNAUTHORIZED(401 , "暂无权限" ), NOT_FOUND(404 , "资源不存在" ), INTERNAL_ERROR(500 , "服务器错误" ); private final int code; private final String message; ResultCode(int code, String message) { this .code = code; this .message = message; } }
字典或枚举 OrderStatusEnum
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import com.baomidou.mybatisplus.annotation.EnumValue;import com.fasterxml.jackson.annotation.JsonValue;import lombok.Getter;@Getter public enum OrderStatusEnum { UNPAID(0 , "待支付" ), PAID(1 , "已支付" ), CANCELLED(2 , "已取消" ), REFUNDED(3 , "已退款" ); OrderStatusEnum(int code, String desc) { this .code = code; this .desc = desc; } @EnumValue private final int code; @JsonValue private final String desc; }
异常处理类 GlobalExceptionHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import com.koohub.demo01.model.common.Result;import com.koohub.demo01.model.common.ResultCode;import jakarta.servlet.http.HttpServletRequest;import jakarta.validation.ConstraintViolation;import jakarta.validation.ConstraintViolationException;import lombok.extern.slf4j.Slf4j;import org.apache.ibatis.builder.BuilderException;import org.mybatis.spring.MyBatisSystemException;import org.springframework.context.MessageSourceResolvable;import org.springframework.validation.FieldError;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import org.springframework.web.method.annotation.HandlerMethodValidationException;import org.springframework.web.servlet.NoHandlerFoundException;import org.springframework.web.servlet.resource.NoResourceFoundException;import java.util.Comparator;@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ MethodArgumentNotValidException.class, HandlerMethodValidationException.class, ConstraintViolationException.class }) public Result<String> handleValidationExceptions (Exception ex) { String message = "参数校验失败" ; if (ex instanceof MethodArgumentNotValidException e) { message = e.getBindingResult().getFieldErrors().stream() .min(Comparator.comparing(FieldError::getField)) .map(FieldError::getDefaultMessage) .orElse(message); } else if (ex instanceof HandlerMethodValidationException e) { message = e.getValueResults().stream() .flatMap(res -> res.getResolvableErrors().stream()) .map(MessageSourceResolvable::getDefaultMessage) .findFirst().orElse(message); } else if (ex instanceof ConstraintViolationException e) { message = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .findFirst().orElse(message); } log.warn("参数校验未通过: {}" , message); return Result.fail(ResultCode.VALID_ERROR.getCode(), message); } @ExceptionHandler({MyBatisSystemException.class, BuilderException.class}) public Result<String> handleMyBatisException (Exception e, HttpServletRequest request) { log.error("数据库映射/语法异常 [{}]: " , request.getRequestURI(), e); return Result.fail(ResultCode.INTERNAL_ERROR.getCode(), "服务忙,请稍后重试" ); } @ExceptionHandler(RuntimeException.class) public Result<String> handleRuntimeException (RuntimeException e, HttpServletRequest request) { log.warn("业务运行时异常 [{}]: {}" , request.getRequestURI(), e.getMessage()); return Result.fail(ResultCode.FAILURE.getCode(), e.getMessage()); } @ExceptionHandler({NoHandlerFoundException.class, NoResourceFoundException.class}) public Result<Void> handleNotFoundException (Exception e, HttpServletRequest request) { String uri = request.getRequestURI(); if (uri.endsWith("/favicon.ico" ) || uri.contains(".well-known" )) { return Result.success(); } return Result.fail(ResultCode.NOT_FOUND); } @ExceptionHandler(Exception.class) public Result<String> handleDefaultException (Exception e, HttpServletRequest request) { log.error("服务器内部错误,请求路径: [{}]" , request.getRequestURI(), e); return Result.fail(ResultCode.INTERNAL_ERROR); } }
拦截器 LoginInterceptor
1 2 3 4 5 6 7 8 9 10 11 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("login?" ); if (true ) { return true ; } return HandlerInterceptor.super .preHandle(request, response, handler); } }
文件的上传下载业务 UploadController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import com.koohub.demo01.model.common.Result;import jakarta.servlet.http.HttpServletRequest;import org.springframework.core.io.FileSystemResource;import org.springframework.core.io.Resource;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;import java.io.File;import java.io.IOException;import java.nio.file.Files;@RestController public class UploadController { private static final String CONTEXT_PATH = "/upload/" ; @PostMapping("/up") public Result<Void> upload (String nickname, MultipartFile photo, HttpServletRequest request) throws IOException { System.out.println("文件大小:" + photo.getSize()); System.out.println("文件类型:" + photo.getContentType()); System.out.println("文件名称:" + photo.getOriginalFilename()); String path = request.getServletContext().getRealPath(CONTEXT_PATH); System.out.println(path); saveFile(photo, path); return Result.success(); } private void saveFile (MultipartFile file, String path) throws IOException { File f = new File (path); if (!f.exists()) { f.mkdir(); } File targetFile = new File (path + file.getOriginalFilename()); file.transferTo(targetFile); } @GetMapping("/view") public ResponseEntity<Resource> viewFile (@RequestParam String nickname, HttpServletRequest request) throws IOException { String realPath = request.getServletContext().getRealPath(CONTEXT_PATH); File file = new File (realPath + nickname); if (!file.exists()) { return ResponseEntity.notFound().build(); } Resource resource = new FileSystemResource (file); String contentType = Files.probeContentType(file.toPath()); if (contentType == null ) { contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; } return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .body(resource); } }
用户业务 UserRestController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 @Tag(name = "用户管理", description = "用户增删改查接口") @Validated @RestController public class UserRestController { @Resource private UserService userService; @Operation(summary = "根据ID获取用户") @GetMapping(value = "/user/{id}") public Result<UserVO> getUserById (@Min (value = 1 , message = "ID不合法" ) @Parameter(description = "用户编号") @PathVariable Integer id) { UserVO user = userService.findById(id); return Result.success(user); } @Operation(summary = "获取用户列表") @GetMapping(value = "/users") public Result<List<UserVO>> getUsers () { List<UserVO> users = userService.getAllUser(); return Result.success(users); } @Operation(summary = "分页查询用户(自定义 mapper 实现)") @GetMapping("/page_query") public PageResult<UserVO> pageQuery (@Validated UserPageQueryRequest pageQuery) { PageResult<UserVO> page = userService.pageQuery(pageQuery); return page; } @Operation(summary = "分页查询用户(自定义 mapper 实现,IPage 作为第一个参数,不必写 count 查询)") @GetMapping("/page_query2") public PageResult<UserVO> pageQuery2 (@Validated UserPageQueryRequest pageQuery) { PageResult<UserVO> page = userService.pageQuery2(pageQuery); return page; } @Operation(summary = "分页查询用户(内置 page query 实现)") @GetMapping("/page_query3") public PageResult<UserVO> pageQuery3 (@Validated UserPageQueryRequest pageQuery) { PageResult<UserVO> page = userService.pageQuery3(pageQuery); return page; } @Operation(summary = "新增用户", description = "传入用户信息进行保存") @PostMapping("/user") public Result<Void> saveUser (@Validated(ValidGroups.Insert.class) UserRequest user) { userService.saveUser(user); return Result.success(); } @PutMapping("/user") public Result<Void> updateUser (@Validated(ValidGroups.Update.class) UserRequest user) { userService.updateUser(user); return Result.success(); } @Operation(summary = "批量刷新密码", description = "批量刷新密码") @PutMapping("/batch_refresh_passwd") public Result<Void> batchRefreshPassword (@Validated @RequestBody BatchRefreshPasswordRequest req) { userService.batchUpdatePassword(req.getIds(), req.getNewPassword()); return Result.success(); } @DeleteMapping("/user/{id}") public Result<Void> deleteUser (@Min (value = 1 , message = "ID不合法" ) @PathVariable Integer id) { boolean result = userService.removeById(id); return result ? Result.success() : Result.fail(ResultCode.NOT_FOUND); } }
UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.baomidou.mybatisplus.extension.service.IService;import com.koohub.demo01.model.common.PageResult;import com.koohub.demo01.model.entity.UserEntity;import com.koohub.demo01.model.req.UserPageQueryRequest;import com.koohub.demo01.model.req.UserRequest;import com.koohub.demo01.model.vo.UserVO;import java.math.BigDecimal;import java.util.List;public interface UserService extends IService <UserEntity> { UserVO findById (Integer id) ; List<UserVO> getAllUser () ; PageResult<UserVO> pageQuery (UserPageQueryRequest pageQuery) ; PageResult<UserVO> pageQuery2 (UserPageQueryRequest pageQuery) ; PageResult<UserVO> pageQuery3 (UserPageQueryRequest pageQuery) ; void saveUser (UserRequest userRequest) ; void updateUser (UserRequest userRequest) ; int batchUpdatePassword (List<Integer> ids, String newPassword) ; int reduceBalance (Integer userId, BigDecimal amount) ; }
UserServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 @Slf4j @Service public class UserServiceImpl extends ServiceImpl <UserMapper, UserEntity> implements UserService { @Resource private UserConvert userConvert; public UserVO findById (Integer id) { UserEntity userEntity = baseMapper.selectById(id); UserVO userVo = userConvert.toVO(userEntity); return userVo; } public List<UserVO> getAllUser () { List<UserEntity> userEntities = baseMapper.selectList(null ); List<UserVO> userVoList = userConvert.toVOList(userEntities); return userVoList; } @Override public PageResult<UserVO> pageQuery (UserPageQueryRequest pageQuery) { Long total = baseMapper.pageQueryCount(pageQuery); List<UserVO> voList = Collections.emptyList(); if (total > 0 ) { List<UserEntity> entityList = baseMapper.pageQuery(pageQuery); voList = userConvert.toVOList(entityList); } return PageResult.of(total, voList); } @Override public PageResult<UserVO> pageQuery2 (UserPageQueryRequest pageQuery) { IPage<UserVO> page = new Page <>(pageQuery.getPageNum(), pageQuery.getPageSize()); IPage<UserVO> resultPage = baseMapper.selectUserPage2(page, pageQuery); return PageResult.of(resultPage.getTotal(), resultPage.getRecords()); } @Override public PageResult<UserVO> pageQuery3 (UserPageQueryRequest pageQuery) { IPage<UserEntity> page = new Page <>(pageQuery.getPageNum(), pageQuery.getPageSize()); LambdaQueryWrapper<UserEntity> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.like(pageQuery.getUsername()!= null , UserEntity::getUsername, pageQuery.getUsername()) .eq(pageQuery.getStartTime() != null , UserEntity::getBirthday, pageQuery.getStartTime()); IPage<UserEntity> resultPage = baseMapper.selectPage(page, queryWrapper); long total = resultPage.getTotal(); List<UserVO> voList = Collections.emptyList(); if (total > 0 ) { voList = userConvert.toVOList(resultPage.getRecords()); } return PageResult.of(resultPage.getTotal(), voList); } @Override public void saveUser (UserRequest userRequest) { UserEntity user = userConvert.toEntity(userRequest); baseMapper.insert(user.setId(null )); log.info("主键ID:{}" , user.getId()); } @Override public void updateUser (UserRequest userRequest) { UserEntity user = userConvert.toEntity(userRequest); baseMapper.updateById(user); } @Override public int batchUpdatePassword (List<Integer> ids, String newPassword) { int result = baseMapper.batchUpdatePassword(ids, newPassword); return result; } @Override public int reduceBalance (Integer userId, BigDecimal amount) { int result = baseMapper.reduceBalance(userId, amount); return result; } }
UserConvert
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.koohub.demo01.config.MapStructConfig;import com.koohub.demo01.model.entity.UserEntity;import com.koohub.demo01.model.req.UserRequest;import com.koohub.demo01.model.vo.UserVO;import org.mapstruct.Mapper;import org.mapstruct.Mapping;import java.util.List;@Mapper(config = MapStructConfig.class) public interface UserConvert { @Mapping(target = "birthday", dateFormat = "yyyy-MM-dd") UserVO toVO (UserEntity userEntity) ; List<UserVO> toVOList (List<UserEntity> userEntities) ; @Mapping(target = "birthday", source = "birthday", dateFormat = "yyyy-MM-dd") UserEntity toEntity (UserRequest userRequest) ; }
UserMapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.koohub.demo01.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.core.metadata.IPage;import com.koohub.demo01.model.entity.UserEntity;import com.koohub.demo01.model.req.UserPageQueryRequest;import com.koohub.demo01.model.vo.UserVO;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import org.apache.ibatis.annotations.Update;import java.math.BigDecimal;import java.util.List;import java.util.Map;@Mapper public interface UserMapper extends BaseMapper <UserEntity> { @Select("SELECT YEAR(birthday) as year, COUNT(*) as count " + " FROM zdemo01_user " + " GROUP BY YEAR(birthday) " + " ORDER BY year DESC") List<Map<String, Object>> selectCountByBirthYear () ; Long pageQueryCount (@Param("query") UserPageQueryRequest pageQuery) ; List<UserEntity> pageQuery (@Param("query") UserPageQueryRequest pageQuery) ; IPage<UserVO> selectUserPage2 (IPage<UserVO> page, @Param("query") UserPageQueryRequest query) ; int batchUpdatePassword (@Param("ids") List<Integer> ids, @Param("newPassword") String newPassword) ; @Update("UPDATE zdemo01_user SET balance = balance - #{amount} WHERE id = #{userId} AND balance >= #{amount} AND deleted = 0") int reduceBalance (@Param("userId") Integer userId, @Param("amount") BigDecimal amount) ; }
resources/mapper/UserMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <?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.koohub.demo01.mapper.UserMapper" > <resultMap id ="BaseResultMap" type ="com.koohub.demo01.model.entity.UserEntity" > <id column ="id" property ="id" jdbcType ="INTEGER" /> <result column ="username" property ="username" jdbcType ="VARCHAR" /> <result column ="password" property ="password" jdbcType ="VARCHAR" /> <result column ="birthday" property ="birthday" jdbcType ="TIMESTAMP" /> <result column ="balance" property ="balance" jdbcType ="DECIMAL" /> <result column ="create_time" property ="createTime" jdbcType ="TIMESTAMP" /> <result column ="update_time" property ="updateTime" jdbcType ="TIMESTAMP" /> </resultMap > <sql id ="Base_Column_List" > id, username, password, birthday, balance, create_time, update_time </sql > <sql id ="Base_Logic_Condition" > deleted = 0 </sql > <sql id ="User_Page_Where_Clause" > <where > <include refid ="Base_Logic_Condition" /> <if test ="query.username != null and query.username != ''" > AND username LIKE CONCAT('%', #{query.username}, '%') </if > <if test ="query.startTime != null" > AND birthday > = #{query.startTime} </if > </where > </sql > <select id ="pageQueryCount" resultType ="java.lang.Long" > SELECT COUNT(*) FROM zdemo01_user <include refid ="User_Page_Where_Clause" /> </select > <select id ="pageQuery" resultMap ="BaseResultMap" > SELECT <include refid ="Base_Column_List" /> FROM zdemo01_user <include refid ="User_Page_Where_Clause" /> ORDER BY id DESC LIMIT #{query.offset}, #{query.pageSize} </select > <select id ="selectUserPage2" resultType ="com.koohub.demo01.model.vo.UserVO" > SELECT <include refid ="Base_Column_List" /> FROM zdemo01_user <include refid ="User_Page_Where_Clause" /> ORDER BY id DESC </select > <update id ="batchUpdatePassword" > UPDATE zdemo01_user SET password = #{newPassword}, update_time = NOW() WHERE id IN <foreach collection ="ids" item ="id" open ="(" separator ="," close =")" > #{id} </foreach > </update > </mapper >
UserEntity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Data @Accessors(chain = true) @EqualsAndHashCode(callSuper = false) @TableName("zdemo01_user") public class UserEntity extends BaseEntity { @TableId(type = IdType.AUTO) private Integer id; @TableField(value = "username", whereStrategy = FieldStrategy.NOT_NULL) private String username; @TableField(select = false) private String password; @TableField(whereStrategy = FieldStrategy.NOT_NULL) private Date birthday; @TableField(whereStrategy = FieldStrategy.NOT_NULL) private BigDecimal balance; @TableField(exist = false) private List<OrderEntity> orders; }
BatchRefreshPasswordRequest
1 2 3 4 5 6 7 8 9 10 11 12 @Schema(description = "批量刷新用户密码请求") @Data public class BatchRefreshPasswordRequest { @Schema(description = "用户id列表") @NotEmpty(message = "用户id列表不能为空") private List<Integer> ids; @Schema(description = "新密码") @NotEmpty(message = "新密码不能为空") private String newPassword; }
UserPageQueryRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import com.koohub.demo01.model.common.BasePageQuery;import com.koohub.demo01.model.vo.UserOrderVO;import io.swagger.v3.oas.annotations.media.Schema;import jakarta.validation.Valid;import lombok.Data;import lombok.EqualsAndHashCode;import org.springframework.format.annotation.DateTimeFormat;import java.util.Date;@Schema(description = "用户分页查询请求信息实体") @Data @EqualsAndHashCode(callSuper = true) public class UserPageQueryRequest extends BasePageQuery { @Schema(description = "用户", example = "张三") private String username; @Schema(description = "开始日期", example = "2023-11-11") @DateTimeFormat(pattern = "yyyy-MM-dd") private Date startTime; @Valid private UserOrderVO otherConditions; }
UserRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Schema(description = "用户请求信息实体") @Data public class UserRequest { @Schema(description = "用户编号(新增时不传)", example = "10", minimum = "10", maximum = "100") @Null(message = "新增时不能指定ID", groups = ValidGroups.Insert.class) @NotNull(message = "更新时用户ID不能为空", groups = ValidGroups.Update.class) private Integer id; @Schema(description = "用户", example = "张三") @NotNull(message = "新增时用户名称不能为空", groups = ValidGroups.Insert.class) @Size(min = 2, message = "名字长度不合法") private String username; @Schema(description = "密码", example = "123") @NotBlank(message = "新增时才强制密码不能为空", groups = ValidGroups.Insert.class) @Pattern(regexp="[0-9a-zA-Z@$#&]{8,}", message = "密码太简单") private String password; @Schema(description = "出生日期", example = "2023-11-11") @Pattern(regexp="\\d{4}-\\d{1,2}-\\d{1,2}", message = "出生日期格式有问题") private String birthday; }
UserVO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Schema(description = "用户响应信息实体") @Data public class UserVO { @Schema(description = "用户编号", example = "10", minimum = "10", maximum = "100") private Integer id; @Schema(description = "用户", example = "张三") private String username; @Schema(description = "密码", example = "123") private String password; @Schema(description = "出生日期", example = "2023-11-11") @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private String birthday; }
订单业务 OrderRestController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Tag(name = "订单管理", description = "订单相关接口") @Validated @RestController public class OrderRestController { @Resource private OrderService orderService; @GetMapping("/order/user_order_info/{userId}") public Result<List<UserOrderVO>> getUserOrderInfo (@PathVariable("userId") Integer userId) { List<UserOrderVO> resultList = orderService.getUserOrders(userId); return Result.success(resultList); } @PostMapping("/order/submit_order") public Result<Void> submitOrder (@RequestBody OrderRequest orderRequest) { return orderService.submitOrder(orderRequest); } }
OrderService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface OrderService extends IService <OrderEntity> { List<UserOrderVO> getUserOrders (Integer userId) ; Result<Void> submitOrder (OrderRequest orderRequest) ; }
OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.koohub.demo01.mapper.OrderMapper;import com.koohub.demo01.model.common.Result;import com.koohub.demo01.model.common.ResultCode;import com.koohub.demo01.model.comvert.OrderConvert;import com.koohub.demo01.model.dict.OrderStatusEnum;import com.koohub.demo01.model.entity.OrderEntity;import com.koohub.demo01.model.entity.UserEntity;import com.koohub.demo01.model.req.OrderRequest;import com.koohub.demo01.model.vo.UserOrderVO;import jakarta.annotation.Resource;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;import java.util.List;import java.util.UUID;@Slf4j @Service public class OrderServiceImpl extends ServiceImpl <OrderMapper, OrderEntity> implements OrderService { @Resource private OrderConvert orderConvert; @Resource private UserService userService; @Override public List<UserOrderVO> getUserOrders (Integer userId) { List<UserEntity> userEntityList = baseMapper.selectUserOrders(userId); List<UserOrderVO> resultList = orderConvert.toUserOrderVOList(userEntityList); return resultList; } @Override @Transactional(rollbackFor = Exception.class) public Result<Void> submitOrder (OrderRequest orderRequest) { UserEntity user = userService.getById(orderRequest.getUserId()); BigDecimal balance = user.getBalance(); BigDecimal amount = orderRequest.getAmount(); if (balance == null || balance.compareTo(amount) < 0 ) { return Result.fail(ResultCode.FAILURE.getCode(), "余额不足,下单失败!" ); } int updated = userService.reduceBalance(orderRequest.getUserId(), orderRequest.getAmount()); if (updated == 0 ) { throw new RuntimeException ("余额不足,下单失败!" ); } OrderEntity order = new OrderEntity (); order.setUid(orderRequest.getUserId()) .setOrderNo(UUID.randomUUID().toString().replace("-" , "" )) .setAmount(orderRequest.getAmount()) .setStatus(OrderStatusEnum.PAID); this .save(order); log.info("用户 {} 下单成功,单号:{}" , orderRequest.getUserId(), order.getOrderNo()); return Result.success(); } }
OrderConvert
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Mapper(config = MapStructConfig.class) public interface OrderConvert { @Mapping(source = "orders", target = "orders") @Mapping(target = "username", expression = "java(user.getUsername() + \"(VIP)\")") UserOrderVO toUserOrderVO (UserEntity user) ; List<UserOrderVO> toUserOrderVOList (List<UserEntity> userList) ; @Mapping(source = "createTime", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") @Mapping(source = "updateTime", target = "updateTime", dateFormat = "yyyy-MM-dd HH:mm:ss") @Mapping(source = "status.code", target = "statusCode") @Mapping(source = "status.desc", target = "statusDesc") UserOrderVO.OrderInfoVO toOrderInfoVO (OrderEntity orderEntity) ; }
OrderMapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Mapper public interface OrderMapper extends BaseMapper <OrderEntity> { @Select("select id,uid,order_no,amount,status,create_time,update_time from zdemo01_order where uid = #{userId} and deleted = 0") List<OrderEntity> selectByUserId (@Param("userId") Integer userId) ; @Select("select id, username, birthday, balance, create_time, update_time from zdemo01_user where id = #{userId} and deleted = 0") @Results({ @Result(column = "id", property = "id"), @Result(column = "username", property = "username"), @Result(column = "birthday", property = "birthday"), @Result(column = "balance", property = "balance"), @Result(column = "id", property = "orders", javaType = List.class, many = @Many(select = "com.koohub.demo01.mapper.OrderMapper.selectByUserId")), }) List<UserEntity> selectUserOrders (@Param("userId") Integer userId) ; @Select("select id,uid,order_no,amount,status,create_time,update_time from zdemo01_order") @Results({ @Result(column = "id", property = "id"), @Result(column = "order_no", property = "orderNo"), @Result(column = "amount", property = "amount"), @Result(column = "status", property = "status"), @Result(column = "uid", property = "user", javaType = UserEntity.class, one = @One(select = "com.koohub.demo01.mapper.UserMapper.selectById")), }) List<OrderEntity> listOrderAndUser () ; }
OrderEntity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data @Accessors(chain = true) @EqualsAndHashCode(callSuper = false) @TableName("zdemo01_order") public class OrderEntity extends BaseEntity { @TableId(type = IdType.AUTO) private Long id; private Integer uid; private String orderNo; private BigDecimal amount; private OrderStatusEnum status; @TableField(exist = false) private UserEntity user; }
OrderRequest
1 2 3 4 5 @Data public class OrderRequest { private Integer userId; private BigDecimal amount; }
UserOrderVO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Data public class UserOrderVO { private Long id; private String username; private Date birthday; private List<OrderInfoVO> orders; @Data public static class OrderInfoVO { private Long id; private String orderNo; private BigDecimal amount; private Integer statusCode; private String statusDesc; private Date createTime; private Date updateTime; } }
相关测试类 UserEntityMapperTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j @SpringBootTest class UserEntityMapperTest { @Resource private UserMapper userMapper; @Test void test01 () { List<Map<String, Object>> result = userMapper.selectCountByBirthYear(); log.info("test01 result:{}" , result); } @Test void test02 () { UserPageQueryRequest queryRequest = new UserPageQueryRequest (); queryRequest.setUsername("zhangsan" ); Long result = userMapper.pageQueryCount(queryRequest); log.info("test02 result:{}" , result); } }
OrderEntityMapperTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Slf4j @SpringBootTest class OrderEntityMapperTest { @Resource private OrderMapper orderMapper; @Test void test01 () { List<UserEntity> result = orderMapper.selectUserOrders(1 ); log.info("test01 result:{}" , result); } @Test public void test02 () { List<OrderEntity> result = orderMapper.listOrderAndUser(); log.info("test02 result:{}" , result); } }
构建动态多数据源的应用 配置文件的变动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: master: url: jdbc:mysql://xxx:3306/zdemo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: xxx password: xxx slave: enabled: true url: jdbc:mysql://xxx:3306/edu?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: xxx password: xxx initialSize: 5 minIdle: 10 maxActive: 20 maxWait: 60000 connectTimeout: 30000 socketTimeout: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 maxEvictableIdleTimeMillis: 900000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true allow: url-pattern: /druid/* login-username: owlias login-password: xxx filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
相关配置类和组件 DruidConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 import com.alibaba.druid.pool.DruidDataSource;import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;import com.alibaba.druid.support.jakarta.StatViewServlet;import com.alibaba.druid.util.Utils;import jakarta.servlet.*;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import javax.sql.DataSource;import java.io.IOException;import java.util.HashMap;import java.util.Map;@Slf4j @Configuration @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) public class DruidConfig { @Bean(autowireCandidate = false) @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource (DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } @Bean(autowireCandidate = false) @ConfigurationProperties("spring.datasource.druid.slave") @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") public DataSource slaveDataSource (DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } @Bean(name = "dataSource") @Primary public DynamicDataSource dataSource (DruidProperties druidProperties) { Map<Object, Object> targetDataSources = new HashMap <>(); DataSource masterDataSource = masterDataSource(druidProperties); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); try { DataSource slaveDataSource = slaveDataSource(druidProperties); if (slaveDataSource != null ) { targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource); } } catch (Exception e) { log.info("从数据库未配置或未开启,仅加载主库。" ); } return new DynamicDataSource (masterDataSource, targetDataSources); } @Bean @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true") public static ServletRegistrationBean<StatViewServlet> druidStatViewServlet () { ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean <>(new StatViewServlet (), "/druid/*" ); registrationBean.addInitParameter("loginUsername" , "xxx" ); registrationBean.addInitParameter("loginPassword" , "xxx" ); registrationBean.addInitParameter("resetEnable" , "false" ); return registrationBean; } @Bean @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true") public static FilterRegistrationBean<Filter> removeDruidFilterRegistrationBean (DruidStatProperties druidProperties) { DruidStatProperties.StatViewServlet config = druidProperties.getStatViewServlet(); String pattern = (config != null && config.getUrlPattern() != null ) ? config.getUrlPattern() : "/druid/*" ; String commonJsPattern = pattern.replaceAll("\\*" , "" ) + "js/common.js" ; Filter filter = getFilter(); FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean <>(); registrationBean.setFilter(filter); registrationBean.addUrlPatterns(commonJsPattern); registrationBean.setOrder(Integer.MIN_VALUE); return registrationBean; } private static Filter getFilter () { final String filePath = "support/http/resources/js/common.js" ; return new Filter () { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String text = Utils.readFromResource(filePath); text = text.replaceAll("buildFooter\\s*:\\s*function\\s*\\(\\)\\s*\\{[\\s\\S]*?}(?=\\s*,\\s*|\\s*})" , "buildFooter: function() {}" ); text = text.replaceAll("<a.*?druid_banner_click.*?>.*?</a><br/>" , "" ); response.setCharacterEncoding("UTF-8" ); response.setContentType("application/javascript" ); response.getWriter().write(text); } }; } }
DruidProperties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Data @ConfigurationProperties(prefix = "spring.datasource.druid") @Component public class DruidProperties { private int initialSize; private int minIdle; private int maxActive; private int maxWait; private int connectTimeout; private int socketTimeout; private int timeBetweenEvictionRunsMillis; private int minEvictableIdleTimeMillis; private int maxEvictableIdleTimeMillis; private String validationQuery; private boolean testWhileIdle; private boolean testOnBorrow; private boolean testOnReturn; public DruidDataSource dataSource (DruidDataSource datasource) { datasource.setInitialSize(initialSize); datasource.setMaxActive(maxActive); datasource.setMinIdle(minIdle); datasource.setMaxWait(maxWait); datasource.setConnectTimeout(connectTimeout); datasource.setSocketTimeout(socketTimeout); datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); datasource.setValidationQuery(validationQuery); datasource.setTestWhileIdle(testWhileIdle); datasource.setTestOnBorrow(testOnBorrow); datasource.setTestOnReturn(testOnReturn); return datasource; } }
DynamicDataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource (DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { super .setDefaultTargetDataSource(defaultTargetDataSource); super .setTargetDataSources(targetDataSources); super .afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey () { return DynamicDataSourceContextHolder.getDataSourceType(); } }
DynamicDataSourceContextHolder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Slf4j public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal <>(); public static void setDataSourceType (String dataSource) { log.info("切换数据源:{}" , dataSource); CONTEXT_HOLDER.set(dataSource); } public static String getDataSourceType () { return CONTEXT_HOLDER.get(); } public static void clearDataSourceType () { CONTEXT_HOLDER.remove(); } }
DataSourceType
1 2 3 4 5 6 7 public enum DataSourceType { MASTER, SLAVE; }
DataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface DataSource { DataSourceType value () default DataSourceType.MASTER; }
切面配置 增加依赖:
1 2 3 4 5 6 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > <version > ${spring-boot.version}</version > </dependency >
DataSourceAspect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Slf4j @Aspect @Order(20) @Component public class DataSourceAspect { @Pointcut("@annotation(com.koohub.demo01.config.datasource.DataSource) || " + "@within(com.koohub.demo01.config.datasource.DataSource)") public void dataSourcePointCut () { } @Around("dataSourcePointCut()") public Object around (ProceedingJoinPoint point) throws Throwable { DataSource dataSource = getDataSource(point); if (Objects.nonNull(dataSource)) { DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); } try { return point.proceed(); } finally { DynamicDataSourceContextHolder.clearDataSourceType(); } } public DataSource getDataSource (ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); if (Objects.nonNull(dataSource)) { return dataSource; } return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); } }
相关业务类 DictController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController public class DictController { @Resource private DictService dictService; @GetMapping(value = "/dict/{id}") public Result<DictVO> getUserById (@Min(value = 1, message = "ID不合法") @Parameter(description = "字典ID") @PathVariable Long id) { DictVO dictVo = dictService.getDictById(id); return Result.success(dictVo); } @GetMapping(value = "/dict/getByType") public Result<List<DictVO>> getByType (@Parameter String type) { List<DictVO> dictList = dictService.getDictListByType(type); return Result.success(dictList); } }
DictService
1 2 3 4 public interface DictService { DictVO getDictById (Long id) ; List<DictVO> getDictListByType (String type) ; }
DictServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Slf4j @Service public class DictServiceImpl extends ServiceImpl <DictMapper, DictEntity> implements DictService { @Resource private DictConvert dictConvert; @DataSource(DataSourceType.SLAVE) @Override public DictVO getDictById (Long id) { DictEntity dictEntity = baseMapper.selectById(id); return dictConvert.toDictVO(dictEntity); } @Override public List<DictVO> getDictListByType (String type) { try { DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE.name()); LambdaQueryWrapper<DictEntity> query = new LambdaQueryWrapper <>(); query.eq(DictEntity::getType, type).orderByAsc(DictEntity::getSort); List<DictEntity> dictEntities = baseMapper.selectList(query); return dictConvert.toDictVOList(dictEntities); } finally { DynamicDataSourceContextHolder.clearDataSourceType(); } } }
DictConvert
1 2 3 4 5 @Mapper(config = MapStructConfig.class) public interface DictConvert { DictVO toDictVO (DictEntity dict) ; List<DictVO> toDictVOList (List<DictEntity> dictList) ; }
DictMapper
1 2 3 @Mapper public interface DictMapper extends BaseMapper <DictEntity> {}
标题:
深入浅出 springboot - 简介和示例