Springframework
- 应用场景:Spring是一个开源设计层面框架,实现了业务逻辑层和其他层的解耦,它的核心思想是面向接口编程
| 模块 | 核心功能 | 典型场景 |
|---|---|---|
| Core Container | IOC/DI、Bean 管理 | 基础对象创建 |
| Data Access | JDBC、ORM、事务 | 数据库操作 |
| Web | MVC、WebFlux | Web 应用开发 |
| AOP | 切面编程 | 日志、事务 |
| Test | 测试支持 | 单元/集成测试 |
| Security | 认证、授权 | 安全控制 |
| Integration | 消息、批量任务 | 企业集成 |
-
Spring发展过程
Spring3.x:通过xml文件加载IOCSpring4.x:废弃xml文件,推荐使用配置类+配置文件方式加载IOCSpring Boot:基于Spring4+的Web项目框架,极大地简化了Web应用的配置
IOC
-
描述:是对工厂模式和单例模式的综合实现,开发者不需要再临时手动创建配置、获取对象
- 代码解耦,提高代码可读性,和可维护性
- IOC默认实现了单例模式,提高了性能
Bean生命周期
| 阶段 | 触发方式 |
|---|---|
| Bean实例化(Instantiation) | 构造方法注入 |
| 依赖注入(Dependency Injection) | 字段注入、Setter注入 |
| BeanPostProcessor 前置处理 | 实现BeanPostProcessor接口的postProcessBeforeInitialization()方法 |
| 初始化前(Initializing) | @PostConstruct |
| BeanPostProcessor 后置处理 | 实现BeanPostProcessor接口的postProcessAfterInitialization()方法 |
| 使用 | 调用业务方法 |
| 销毁,容器关闭时调用 | @PreDestroy |
Bean注册(IOC)
-
描述:将创建的类实例加入IOC容器中
-
注册方式的应用场景
- 命名空间注册方式只用于Spring旧项目,不建议使用
- 业务类注解通过构造方法注册IOC,初始化过程简单快速,适合加载业务组件快速使用其方法
- 配置类通过
set方法注册IOC,可以实现实例的复杂逻辑的初始化过程,适合加载常用的配置Bean
命名空间注册
- 原理:Java EE要求Web项目默认加载
/WEB-INF/web.xml,指定xxx.xml配置Bean
xml
<?xml version="1.0" encoding="UTF-8"?> <!-- web.xml -->
<web-app
version="3.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!-- applicationContext.xml用于配置Bean -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 自定义myService.xml用于配置Bean -->
<context-param>
<param-name>myService</param-name>
<param-value>classpath:myService.xml</param-value>
</context-param>
...
</web-app>
xml
<?xml version="1.0" encoding="UTF-8"?> <!-- 默认指定applicationContext-custom.xml配置Bean -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- 1. 定义一个简单的 Bean -->
<bean id="ser" class="com.wyh.entities.User">
<!-- 2. Setter 注入依赖 -->
<property name="userId" value="1"/>
<property name="userName" value="wyh"/>
<property name="userMoney" value="666"/>
</bean>
</beans>
配置类方法注册
-
原理:配置类注册IOC
- 动态代理:配置类会生成一个代理类,代理类会拦截所有
@Bean方法的调用 - 调用拦截:拦截所有
@Bean方法后,代理类会先检查 Bean 是否单例,防止重复创建对象 - 注册IOC:如果容器中已有单例则直接返回,否则创建新实例并放入容器
- 动态代理:配置类会生成一个代理类,代理类会拦截所有
-
配置类相关注解
@Configuration:加在类上,表示这个类是配置类,Spring会默认加载所有配置类@Bean:将返回值注册Bean,必须用于Spring管理的类中(一般是配置类),否则注册的Bean无法自动注入@Value:引用全局配置文件(application.yaml),注入属性值@PropertySource:配合@Value,可以加载属性文件中的值(.properties、.yml)@ImportResource:将文件中定义的 Bean 注册到 Spring 容器中,用于新项目兼容老项目对 XML 的支持@scope:Bean 的作用域(如单例、原型、请求、会话等)@Lazy:Bean懒加载,即使用时才注册@ComponentScan:指定路径下开启注解扫描,否则注解不生效@Import:导入其他配置类
java
@Configuration
// 如果application.yaml定义了相同的属性,application.yaml的值会覆盖db.properties中的值,因为它的优先级更高
@PropertySource("classpath:db.properties") // 引入db.properties文件数据
public class DruidConfig {
@Value("${druid.url}")
private String dbUrl;
@Value("${druid.username}")
private String username;
@Value("${druid.password}")
private String password;
@Bean(value="druidDataSource") // 显式指定Bean名称,默认就是方法名
// @Bean(name = {"druidDataSource", "ds"}) // name属性支持多别名,druidDataSource和ds都可以引用
public DataSource druidDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(dbUrl);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
@Bean // 可以在Bean方法入参直接简化注入Bean
public PlatformTransactionManager transactionManager(@Qualifier("ds") DataSource druidDataSource) {
return new DataSourceTransactionManager(druidDataSource);
}
}
业务类注解注册
-
常见业务注解:默认Bean名称是类名首字母小写
@Component:普通类@Service:业务层@Controller:控制器层@Repository:持久化层
-
原理
- 开启扫描:Spring启动默认不加载通用注解注册,需要再开启注解扫描
- 注册IOC:开启注解扫描后,会反射构造器(默认空参),创建Bean加入IOC
- 注入Bean:注册完毕后,其他Bean也可以作为成员注入(详见下文)
-
开启注解扫描方法
- Spring3:在xml文件中
<context:component-scan base-package="包路径"/>开启 - SpringBoot:启动类默认开启了同路径注解扫描,也可以在配置类中使用
@ComponentScan("包路径")开启注解扫描
- Spring3:在xml文件中
java
@SpringBootApplication(scanBasePackages = {"com.wyh"}) // 设置注解扫描
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
java
@Service
//@Component("userService") // 可以手动指定Bean名称,默认Bean名称是类名首字母小写
public class UserService implements IUserService {
//TODO
}
Bean注入(DI)
-
描述:根据类型或者名称从IOC容器中取出对应Bean实例
-
注入方法应用场景
- 不要手动加载Bean,否则会破坏单例模式
- 一般业务类中首选字段注入
- 如果存在循环依赖风险,应该使用构造器方法显式注入
手动加载
ClassPathXmlApplicationContext:获取xml定义的BeanAnnotationConfigApplicationContext:获取注解/配置类定义的Bean
java
// 1. 初始化 Spring 容器(通过 XML 配置文件)
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2. 按名称获取 Bean
UserService userService = context.getBean("userService");
// 3. 按类型获取 Bean
UserService userService = context.getBean(UserService.class);
//1. 初始化 Spring 容器 (通过注解/配置类)
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanConfiguration.class);
// 2. 按名称获取 Bean
UserService userService = (UserService) context.getBean("userService");
// 3. 按类型获取 Bean
UserService userService = (UserService) context.getBean(UserService.class);
属性注入
@Autowired(推荐):基于类型@Resource:基于名称,是JavaEE提供的注解,未与Spring深度集成,与@Primary等注解不兼容@Qualifier:@Autowired有多个同类型Bean时,再加@Qualifier名称匹配
java
@Service
class UserService {
@Autowired // 基于类型注入
private UserServiceImpl1 userServiceImpl1;
@Resource // 基于名称注入
private UserServiceImpl2 userServiceImpl2;
@Autowired
@Qualifier("impl2") // 存在同类型Bean,再使用名称匹配
private UserService userService;
}
构造器注入
- 循环依赖:构造器注入可以防止自调用的循环依赖问题
- 数据安全:构造器注入的成员是静态属性,不会被篡改
java
@Service
public class RQueueService {
private final RedissonClient redissonClient;
private final RQueue<String> taskQueue;
@Autowired // Spring 4.3+ 可以不加@Autowired
public OrderQueueConsumer(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
taskQueue = redissonClient.getQueue("taskQueue");
}
}
AOP
-
描述:Spring对动态代理模式的功能封装(现版本默认使用cglib动态代理)
- 将业务核心和增强部分解耦,实现代码复用
- 统一管理增强类,提升可维护性
-
应用场景
- 事务处理(
@transactional) - 权限校验(
Spring Security) - 异步任务(
@Async) - 参数校验(
@Validated) - Sentinel流控异常处理(
@SentinelResource) - 打印日志
- AOP是运行时注解处理器的最佳解决方案
- 事务处理(
-
AOP失效场景
- 方法自调用场景
- 对
private、final、static代理会失效 - 没有加入IOC
- 切面类互相干扰
-
AOP自调用失效的解决方案
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true):绕过this引用,直接调用代理对象的方法- 业务类注入自身Bean,避免直接
this引用
开启AOP
- Spring3.x
xml
<!-- Spring AOP + AspectJ -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>3.2.18.RELEASE</version> <!-- 根据实际版本调整 -->
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version> <!-- AspectJ 核心库 -->
</dependency>
xml
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 1. 启用组件扫描(自动发现 @Component、@Service 等注解的类) -->
<context:component-scan base-package="com.example" />
<!-- 2. 启用 AspectJ 自动代理(支持 @Aspect 注解) -->
<aop:aspectj-autoproxy />
</beans>
- SpringBoot:默认配置 AOP的基础支持(如
@Transactional),需要手动开启完整的 AOP 功能
xml
<!-- Spring Boot Starter AOP(核心依赖) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
java
@SpringBootApplication(scanBasePackages = {"com.wyh"}) // 默认扫描启动类所在路径
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true) // 开启AOP,强制CGLIB代理,解决自调用失效问题
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
方法AOP
@Aspect+@Component是固定格式,可以自定义组合注解
JAVA
/**
* 自定义组合注解:替代 @Aspect + @Component
* 作用等价于 @RestController(@Controller + @ResponseBody)
*/
@Target(ElementType.TYPE) // 仅作用于类
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented // 生成文档时包含该注解
@Aspect // 聚合 AOP 切面标记
@Component // 聚合 Spring Bean 注册
public @interface AspectComponent {
// 可复用 @Component 的 value 属性(指定 Bean 名称)
String value() default "";
}
- 选择要增强的核心业务类
java
@Service
public class UserService implements IUserService {
@ovverride
public void test(Long id){
System.out.println("核心业务...");
}
}
-
通过注解
@Aspect定义切面类,编写通知逻辑应用到指定业务类- 前置通知:
@Before - 返回通知:
@AfterReturning - 后置通知:
@After - 异常通知:
@AfterThrowing - 环绕通知:
@Around
- 前置通知:
java
@AspectComponent
public class UserServiceAspect {
// 定义方法锚点,*是单通配符,..是多通配符
@Pointcut("execution(public * com.wyh.service.impl.UserService.*(..))")
// @Pointcut("execution(public * com.wyh.service.impl.UserService.(..))")
public void pointCut() {
// 切入点方法体约定统一为空,仅做标记作用
}
// 前置通知
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
// joinPoint用于获取切入点方法/注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取方法对象
}
// 返回通知
@AfterReturning(pointcut = "pointCut()", returning = "result")
public void after(Object result) {
// 业务方法正常返回才执行,result是业务方法返回的值
System.out.println("[AOP] 方法返回结果: " + result);
}
// 后置通知
@After("pointCut()") //插入切入点后
public void after(JoinPoint joinPoint) {
System.out.println("[AOP] 方法调用后... " );
}
//环绕通知
@Around("pointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { //joinPoint代表方法
// 方法执行前逻辑, joinPoint.getSignature()获取被拦截方法(目标方法)的信息对象
System.out.println("AroundBefore method: " + joinPoint.getSignature().getName());
// 执行目标方法(必须调用)
Object result = joinPoint.proceed();
// 方法执行后逻辑
System.out.println("AroundAfter method: " + joinPoint.getSignature().getName());
return result; // 可以拦截并修改目标方法的返回值,然后再返回给调用方
}
// 异常通知
@AfterThrowing(pointcut = "pointCut()", throwing = "ex")
public void exceptionThrowing(Exception ex) {
//ex是业务方法生成的异常
log.warn("[AOP] 方法抛出异常: " + ex.getMessage());
}
}
- 调用指定业务类方法时,AOP自动生效
java
public class UserServiceTest extends BaseTest {
@Autowired
private UserService userService;
@Test
public void test() {
userService.test(1L);
}
}
效果:
AroundBefore method: getUserByTradition
[AOP] 方法调用前...
核心业务...
[AOP] 方法返回结果: null
[AOP] 方法调用后...
AroundAfter method: getUserByTradition
注解AOP
-
实现动态代理:AOP是运行时注解处理器的最佳解决方案
- 运行时实现:运行时通过反射动态执行逻辑,首选 AOP
- 编译时实现:在类加载或编译时修改字节码,可以使用 Java Processor 或 JavaPoet实现
java
// 自定义注解实现日志记录
@Component
@Aspect
public class DataSourceAspect {
@Pointcut("@annotation(com.wyh.annotation.DS)")
public void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
DS ds = signature.getMethod().getAnnotation(DS.class); // 获取注解信息
//设置DynamicDataSource
DataSourceContextHolder.setDataSourceKey(ds.value());
}
@After("pointCut()")
public void after(){
DataSourceContextHolder.clearDataSourceKey();
}
@AfterThrowing("pointCut()")
public void afterThrowing(){
DataSourceContextHolder.clearDataSourceKey();
}
}
MVC
-
Spring MVC 支持多种网络传输,其中处理 HTTP 请求是核心功能,也兼容其他连接类型(例如Websocket)
- MVC通过封装原生Servlet对HTTP请求的API,极大地简化了控制器层的开发
- 每一个HTTP请求由Servlet 容器的线程池为其分配一个线程,请求结束后线程自动回收
- HTTP和TCP/IP协议的区别:TPC/IP协议是传输层协议,HTTP协议是应用层协议,我们主要关注HTTP协议下的请求处理
-
SpringBoot引入web启动器即可
xml
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
HTTP请求处理
-
HTTP请求和WebSocket请求
- HTTP请求是无状态短连接(三次握手四次挥手),WebSocket请求是双向实时通信的长连接
- HTTP请求适用于 请求-响应式交互场景,WebSocket请求适用于需要状态保持的场景
-
HTTP请求和HTTPS请求
-
HTTP 是超文本明文传输,HTTPS在【TPC/IP传输层】 和 【HTTP 应用层】之间加入了 SSL/TLS 安全协议,实现加密传输
-
HTTP 的端口号是 80,HTTPS 的端口号是 443
-
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的
-
请求结构
-
请求结构:标准的HTTP请求包含了三部分,请求行、请求头、请求体
-
请求行结构:
<Method(请求方法)> <Request-URI(请求资源)> <HTTP-Version(请求协议)>Method:设置请求方法,常见有GET、PUT、DELETE、POSTRequest-URI:请求资源路径,查询参数用?或/连接HTTP-Version:协议版本
-
请求头结构:以键值对形式存储数据,用于传递与请求或客户端相关的额外信息
请求头key 说明 举例 User-Agent标识客户端类型 Mozilla/5.0 (Windows NT 10.0; Win64; x64)Authorization传递认证凭证 Bearer <token>、Basic <base64(username:password)>Cookie用户相关参数 {sessionid=abc123, user_token=xyz456}Content-Type请求体的数据类型 application/json、multipart/form-dataContent-Length请求体长度 1024Accept接收的响应类型 text/html、application/json、image/pngX-自定义请求头约定 X-开头X-token=122345665 -
请求体:由请求头的属性
Content-Type指定,一般是json,其他常见的有文件类型等
apl
POST adduser/1 HTTP/1.1 # 请求行
Content-Type: application/json # 请求头
{
"name": "Alice",
"age": 25, # 请求体
"email": "alice@example.com"
}
MVC请求映射
-
注解匹配
- 请求方法:
@getMapping、@postMapping、@putMapping、@deleteMapping - 请求参数:
@RequestParam(入参可为空),@PathVariable(入参不为空)、@DateTimeFormat(pattern = "yyyy-MM-dd") - 请求头:
@RequestHeader、@CookieValue - 请求体:
@requestBody会将请求体反Json序列化为指定实例
- 请求方法:
-
HttpServletRequest API
- 请求头:
getHeader、getCookies - 请求体:
getReader()
- 请求头:
-
最佳实践
- 请求方法通过注解形式映射
- 请求参数通过注解形式映射
- 请求体通过注解形式映射
- 请求头通过HttpServletRequest API形式映射
响应结构
-
一个标准的响应包括三部分:响应状态码、响应头、响应体
-
响应行:协议版本 + 响应状态码
-
响应头:以键值对形式存储数据
响应头key 说明 举例 Content-Type响应体的数据类型 application/json、application/pdf、image/pngContent-Length响应体的数据长度 1024Content-Disposition浏览器处理响应体方式 attachment; filename="example.pdf"(不直接显式下载的文件)Location重定向目标 URL Location: https://example.com/new-pathHeader自定义响应头 X-Version:1.0 -
响应体:数据类型和字符集由响应头
Content-Type指定,一般响应体中的数据类型是json,文件类型见下方文件传输
apl
HTTP/1.1 200 OK # 状态码
Content-Type: application/json # 响应头
{
"name": "Alice",
"age": 25, # 响应体
"email": "alice@example.com"
}
MVC返回响应
-
注解匹配
- 响应码:
@ResponseStatus - 响应体:
@ResponseBody会将方法返回值自动序列化为Json字符串写入响应体
- 响应码:
-
HttpServletRequest API
- 响应码:
setStatus() - 响应头:
setHeader()、setContentType()、setCharacterEncoding() - 响应体:
getWriter().write()、ImageIO.write()
- 响应码:
-
ResponseEntity
- 响应码:
.status() - 响应头:
.header()、.contentType() - 响应体:
.body()
- 响应码:
-
最佳实践
- 响应头通过HttpServletRequest API形式映射
- 响应体通过注解形式映射
- 简单场景推荐使用
ResponseEntity快速开发
java
@RestController // @ResponseBody响应体自动转化为JSON字符串
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
/*
GET /user/getInfo?id=1 HTTP/1.1
User-Agent:Mozilla/5.0
Cookies: [Uid=12d32,Uname=wyh]
{
"name": "Alice",
"age": 25,
"email": "alice@example.com"
}
*/
@GetMapping("/getInfo")
public ResultJson<User> getInfo( // 使用注解形式
@RequestParam(value = "id", required = false) Long userId, // @RequestParam匹配请求?连接参数,id可以不传参
@CookieValue("Uname") String name, // @CookieValue 注解匹配cookie字段
@RequestBody User user) // @RequestBody 注解匹配请求体
{
System.out.println(name); //wyh
System.out.println(user.getName()); //Alice
return new ResultJson<User>(200,new User());
}
/*
GET /user/getUser/{id} HTTP/1.1
User-Agent:Mozilla/5.0
Cookies: [Uid=12d32,Uname=wyh]
*/
@GetMapping("/getUser")
public void getUser(HttpServletRequest request, HttpServletResponse response) { // 使用原生servlet API,返回void
Cookie[] cookies = request.getCookies(); // 获取cookies
Integer id;
for (Cookie cookie : cookies) {
if ("id".equals(cookie.getName())) { // 从cookies中获取id
id = Long.parseLong(cookie.getValue());
break;
}
}
response.setContentType(MediaType.APPLICATION_JSON_VALUE); // MediaType新版不再包含字符集声明
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 单独设置编码
if(id == null){
response.getWriter().write(JSON.toJSONString(Result.fail("无效的 id 格式"))); //写入响应体
}
User user = userService.getUserById(id);
response.setHeader("X-Version", "1.0"); //写入自定义响应头
response.setStatus(HttpServletResponse.SC_OK); //写入响应码
response.getWriter().write(JSON.toJSONString(Result.ok(user))); //写入响应体
// 不需要手动关闭IO流,因为 Servlet 容器会在响应结束后自动关闭底层流
}
/*
GET /user/getUser2/{id} HTTP/1.1
User-Agent:Mozilla/5.0
*/
@GetMapping("/getUser2/{id}")
public ResponseEntity<User> getUser2(@PathVariable("id") Long id) { // ResponseEntity泛型控制响应体的类型
ResponseEntity<User> body = ResponseEntity.status(200)
.header("X-Request-ID", UUID.randomUUID().toString())
.header("X-Version", "1.0")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new ResultJson(200,new User())); // 响应体会自动序列化为JSON字符串
return body;
}
/*
PUT /user/update/666 HTTP/1.1
Content-Type: application/json
{
"name": "Alice",
"age": 25,
"email": "alice@example.com"
}
*/
@PutMapping("/updateuser/{id}") //请求体JSON数据,也可以映射为Map
public void updateUser(@PathVariable("id") Integer userId, @RequestBody Map<String, Object> map){
System.out.println(map.get("name")); //Alice,Json映射为Map
}
}
统一返回体
-
描述:为了精细化业务异常,建议将响应体统一格式
- 强制所有接口返回统一格式,避免因开发者习惯不同导致响应结构混乱,前端也便于统一处理响应
- 业务逻辑上的异常返回200,而不是5xx/4xx,这样可以快速区分业务场景异常和服务器异常
java
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Result<T> {
private Boolean status; // 状态标识,快速判断是否正常响应,如果异常再查自定义响应码和描述信息
private String code; // 自定义内部状态码
private String msg; // 描述信息
private T data; // 响应体数据
public static <T> Result<T> ok(){
return new Result<>(true, "200", null, null);
}
public static <T> Result<T> ok(T data){
return new Result<>(true, "200", null, data);
}
public static <T> Result<T> fail(String msg, String code){
return new Result<>(false, code, msg, null);
}
}
- 请求体Json映射VO使用Jacskon反序列化,默认相同名称映射,也可以自定义映射规则
java
@Data
@ToString
@AllArgsConstructor
public class User{
@JsonProperty("id") // 映射Json数据的"id"键
private Integer userId;
@JsonProperty("name") // 映射Json数据的"name"键
private String userName;
@JsonProperty("email") // 映射Json数据的"email"键
private String userEmail;
@JsonProperty("time") // 映射Json数据的"time"键
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",locale = "zh") // 指定映射规则
private Date time; // 时间类型还是建议以Long类型传递
}
文件传输
- 文件传输是HTTP特殊场景,请求体是文件数据即文件上传功能,响应体是文件数据即文件下载功能
yaml
# SpringBoot默认已配置MultipartResolver的Bean,可以使用统一配置文件或者配置类修改属性
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
file-size-threshold: 1MB
enabled: true
location: /tmp/uploads
java
//也可以使用配置类,MultipartResolver的Bean会根据@RequestParam自动注入MultipartFile
@Configuration
public class MultipartConfig {
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSize(10485760); // 10MB
resolver.setDefaultEncoding("UTF-8");
return resolver;
}
}
文件上传
- 前端一般通过表单传输文件,一个键代表一种类型的文件,可以是一个,也可以是多个文件
javascript
<input type="file" id="fileInput-file1" multiple /> <!--多个文件-->
<input type="file" id="fileInput-file2" multiple /> <!--一个文件-->
<button onclick="uploadFile()"/>上传
async function uploadFile() {
const files1 = document.getElementById('fileInput-file1').files; //获取所有fileInput-file1文件
const files2 = document.getElementById('fileInput-file2').files; //获取fileInput-file2文件
const formData = new FormData();
for (let i = 0; i < files1.length; i++) {
formData.append('files', files1[i]); // 键名:files,必须与后端一致
}
formData.append('file', files2[0]); // 键名:file,必须与后端一致
try {
const response = await fetch('/user/upload-file', {
method: 'POST',
body: formData, // 直接发送 FormData(无需设置 Content-Type)
});
alert("上传成功!");
} catch (error) {
console.error('上传失败:', error);
alert('上传失败!');
}
}
- 后端mvc接收:使用
MultipartFile类接收,使用@RequestParam指定文件(组)
java
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("upload-file")
public void insertFiles(
@RequestParam("files") MultipartFile[] files,
@RequestParam("file") MultipartFile file
) {
// 1. 检查并创建上传目录
if (!new File("upload").mkdir()) {
throw new RuntimeException("mkdir failed");
}
// 2. 处理单个文件(files-2)
if (!file.isEmpty()) {
saveFile(file, "upload");
}
// 3. 处理多个文件(files)
for (MultipartFile file : files) {
if (!file.isEmpty()) {
saveFile(file, "upload");
}
}
}
/**
* 保存文件到指定目录(避免代码重复)
* @param file MultipartFile 对象
* @param uploadDir 目标目录
*/
private void saveFile(MultipartFile file, String uploadDir) {
try {
// 1. 获取文件名
String originalFilename = file.getOriginalFilename(); // 可能返回 "C:\Users\test.jpg"
// 2. 防止路径遍历攻击
String safeFilename = Paths.get(originalFilename).getFileName().toString(); //去除路径,即"test.jpg"
Path targetPath = Paths.get(uploadDir, safeFilename); //组成文件路径
// 3. 使用 NIO 方式保存文件(更高效)
Files.copy(
file.getInputStream(),
targetPath,
StandardCopyOption.REPLACE_EXISTING // 如果文件已存在则覆盖
);
} catch (IOException e) {
throw new RuntimeException("文件保存失败: " + file.getOriginalFilename(), e);
}
}
}
文件下载
java
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("{id}/show-file") //原生HttpServletResponse构造响应
public void showFile(@PathVariable("id") Long id, HttpServletResponse response) throws IOException {
// 返回的文件
File file = new File("upload/JavaSE.pdf");
// 设置响应头
response.setContentType("application/pdf");
// 客户端默认直接显示文件需要手动下载,可以设置为直接下载文件
response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
response.setContentLength((int) file.length());
// 读取文件并写入响应输出流
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[4096];
while (true) {
int length = inputStream.read(buffer);
if (length == -1) {
break;
}
outputStream.write(buffer, 0, length);
outputStream.flush();
}
}
}
@GetMapping("{id}/download-file") //ResponseEntity构造响应,普通文件建议使用Resource泛型
public ResponseEntity<Resource> downLoadFile(@PathVariable("id") Long id) throws IOException {
Resource resource = new UrlResource("upload/JavaSE.pdf");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM) // 通用二进制流类型
//设置为直接下载文件
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
@GetMapping("{id}/download-bigfile") //ResponseEntity构造响应,大文件使用fileInputStream泛型,防止内存溢出
public ResponseEntity<InputStreamResource> downLoadBigFile(@PathVariable("id") Long id) throws IOException {
// 返回的文件
File file = new File("upload/JavaSE.pdf");
FileInputStream fileInputStream = new FileInputStream(file);
// 设置内容类型和响应头
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
//设置为直接下载文件
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
.body(new InputStreamResource(fileInputStream));
}
}
Web配置类
-
目的:如果要进一步设置Spring MVC高级功能,可以通过实现
WebMvcConfigurer接口来创建MVC配置类 -
使用方法
- 实现组件接口,编写逻辑内容
- 在
WebMvcConfigurer配置类中的重写方法中引用组件接口
-
常用组件方法
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { //静态资源处理器,直接返回本地资源
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/public/") // 本地资源
.setCachePeriod(3600); // 缓存时间(秒)
}
@Override
public void addViewControllers(ViewControllerRegistry registry) { //跳转指定页面
registry.addViewController("/home").setViewName("home"); // 访问/home跳转到home.html
registry.addRedirectViewController("/old", "/new"); // 重定向
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { //信息转换器
converters.add(new FastJsonHttpMessageConverter()); // 使用FastJson转换
}
}
拦截器
-
目的:在控制器接收请求前,拦截请求,先进行指定逻辑语句
-
替代方案
- 过滤器:在请求到达servlet前就拦截请求
- AOP:指定控制器方法前执行指定语句
- Spring Security:更复杂的权限控制
java
//拦截器
public class Interceptor01 implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler //handler一般为HandlerMethod子类,用于获取请求的控制器方法
) throws Exception {
//TODO
return true; //返回true代表请求会继续向下执行,返回false会拦截请求直接返回响应
}
@Override
public void postHandle( // 控制器方法执行后(响应还未返回客户端)
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView
) throws Exception {
//TODO
}
@Override
public void afterCompletion( // 请求完成时(响应已返回客户端)
HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex
) throws Exception {
//TODO
}
}
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截器1
registry.addInterceptor(new Interceptor01())
.addPathPatterns("/**") //拦截哪些请求
.excludePathPatterns( //不拦截哪些请求
"/user/code",
"/voucher/**"
)
.order(0); //order设置执行顺序,值越小越先执行
//拦截器2
registry.addInterceptor(new Interceptor02()).order(1);
}
}
- 举例:动态参数的路径跳转
java
public class MoveInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
response.sendRedirect("/newAddress"); // 302 重定向,实现网址迁移
return false; // 终止后续处理
}
}
跨域处理
-
描述:前端访问后端时,因为同源规则导致后端禁止前端访问
-
原理
- 同源指的是:用户访问前端的请求 和 前端发送给后端的请求 之间的域名、端口、协议必须一样
- 跨域访问本质上不是错误,而是浏览器的限制行为(例如React访问Tomcat,但二者部署在不同的IP上)
-
解决方法
- 前端直接设位置服务器代理
- 前端由Nginx托管,一定是同源的
- 服务/网关设置跨域
全局统一配置(推荐)
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配指定访问路径,一般匹配所有请求
.allowedOrigins("*") // 浏览器允许所有的域访问,注意allowCredentials(true)时,allowedOrigins不能为 *
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") // 允许跨域请求携带认证凭据 (如Cookie、Authorization、SSL 证书)
.maxAge(3600); // 预检请求(OPTIONS)的缓存时间(秒)
}
}
局部跨域设置
java
//作用于控制器方法
@CrossOrigin(origins = {"http://localhost:8080", "http://localhost:8081"})
@getMapping("test")
public String test() {
return "ok";
}
//作用于控制器类
@RestController
@RequestMapping("user")
@CrossOrigin(origins = {"*"}) //表示所有域都可以访问
public class TestController {
...
}
全局异常处理器
-
描述:对Controller层异常处理的兜底逻辑,可以认为是整个Web项目的全局异常处理机制的兜底
-
注解说明
@ControllerAdvice:指定异常扫描路径@ExceptionHandler:用于指定异常类型@RestControllerAdvice = @ControllerAdvice + @ResponseBody
java
@RestControllerAdvice(basePackages = {"com.wyh.controller"})
@Slf4j
public class ControllerExceptionHandler {
//处理权限异常,可以一次定义多个:@ExceptionHandler({xxxException.class, xxxException.class})
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ResultDto<Object>> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(403)
.contentType(MediaType.APPLICATION_JSON)
.body(new ResultDto<>("500", e.getCause().getClass().toString(), null));
}
// 处理请求体校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<JsonResult> handleValidException(MethodArgumentNotValidException ex) {
JsonResult fail = JsonResult.fail("method valid fail", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(fail);
}
// 处理参数校验异常
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleValidationException(ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String fieldName = violation.getPropertyPath().toString();
String message = violation.getMessage();
errors.put(fieldName, message);
});
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(Exception.class)
public void globalHandle(HttpServletResponse res, Exception e) {
res.setContentType("application/json; charset=utf-8");
ResultDto<Object> resultDto = new ResultDto<>("500", e.getCause().getClass().toString(), null);
String jsonResult = JSON.toJSONString(resultDto);
try {
res.getWriter().write(jsonResult);
} catch (IOException ex) { // Exception处理器是最后的兜底,不能再抛异常了
log.error(ex.getMessage(), ex);
}
}
}
数据校验
-
目的:校验入参的数据格式是否正确
-
原理
@Valid通过ModelAttributeMethodProcessor或RequestResponseBodyMethodProcessor直接调用验证器完成校验@Validated通过AOP 代理对象(MethodValidationInterceptor),在方法调用前触发校验逻辑
开启数据校验
- Spring3
xml
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version> <!-- 或兼容版本 -->
</dependency>
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 显式声明 Validator Bean -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
</beans>
- SpringBoot
xml
<!-- 引入依赖后会自动注册LocalValidatorFactoryBean -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
校验规则注解
-
空值检查
@NotNull:元素不能为null@NotEmpty:元素不能为null且不能为空(适用于字符串、集合、数组、Map)@NotBlank:字符串不能为null,且去除首尾空格后不能为空(仅适用于字符串)
-
数值范围检查
@Min(value):数值必须大于或等于value@Max(value):数值必须小于或等于value@DecimalMin(value):数值(包括小数)必须大于或等于value@DecimalMax(value):数值(包括小数)必须小于或等于value@Positive:数值必须是正数@PositiveOrZero:数值必须是正数或零@Negative:数值必须是负数@NegativeOrZero:数值必须是负数或零
-
字符串检查
@Size(min, max):字符串、集合、数组的大小必须在min和max之间@Length(min, max):字符串的长度必须在min和max之间(功能与@Size类似,@Size更通用)@Pattern(regexp):字符串必须匹配指定的正则表达式
-
日期检查
@Past:日期必须是过去的时间@PastOrPresent:日期必须是过去的时间或当前时间@Future:日期必须是未来的时间@FutureOrPresent:日期必须是未来的时间或当前时间
-
其他检查
@Email:字符串必须是一个合法的电子邮件地址@AssertTrue:布尔值必须为true@AssertFalse:布尔值必须为false
java
public class User {
@NotBlank(message = "用户名不能为空")
private String name;
@Valid // 关键:标记嵌套对象需要校验
private Address address;
// getters & setters
}
public class Address {
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "街道不能为空")
private String street;
// getters & setters
}
@Valid
- 应用场景:
@Valid来源于Java Validation API,用于控制器层请求参数 - 如果校验失败就会抛出
MethodArgumentNotValidException
java
@RestController
@RequestMapping("user")
public class UserController {
// 添加BindingResult参数,发送校验异常后会将异常封装在BindingResult中
@PostMapping
public ResponseEntity<?> createUser(@Valid @RequestBody UserDTO userDTO, BindingResult result) {
// 校验失败时抛出:MethodArgumentNotValidException,可以使用BindingResult接受异常
if (result.hasErrors()) {
// 返回校验错误信息(JSON 格式)
Map<String, String> errors = new HashMap<>();
result.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
// 校验通过,执行业务逻辑
userService.save(userDTO);
return ResponseEntity.ok("用户创建成功");
}
}
@Validated
- 应用场景:Spring 扩展注解,支持
@Valid的所有功能,额外支持 分组校验 和 方法参数校验 - 如果校验失败就会抛出会抛出
ConstraintViolationException - 注意:必须在类或者方法上使用
@Validated,才能生效对应的校验功能
java
@RestController
@RequestMapping("/users")
@Validated // @Validated 必须加在类上,否则方法参数校验不会生效
public class UserController {
@GetMapping("/search/{keyword}")
public String searchUser(
@NotBlank(message = "查询关键词不能为空") @PathVariable("keyword") String keyword) {
// @RequestParam 和 @PathVariable 校验失败会抛出 ConstraintViolationException
return "查询结果: " + keyword;
}
}
java
@Service
@Validated // 必须加这个注解才能让 Service 层的 @Valid 生效
public class UserService {
public void createUser(@Valid User user) {
// 校验失败时抛出:MethodArgumentNotValidException
}
// 校验字符串非空且长度 ≥3
public void updateUserName(@NotBlank @Size(min = 3) String name) {
// 校验失败时抛出:ConstraintViolationException
}
}
分组校验
- 描述:
@Validated支持分组校验功能(例如添加功能和更新功能对参数的校验不同) - 定义分组接口(空接口),仅用于标识校验规则的适用场景,不建议使用已有类直接耦合
java
public interface CreateGroup {}
public interface UpdateGroup {}
- 在实体类中指定分组
java
public class User {
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "用户名不能为空")
private String username;
@NotBlank(groups = CreateGroup.class, message = "密码不能为空")
@Size(groups = CreateGroup.class, min = 6, max = 20)
private String password;
// 其他字段...
}
- 在 Controller 中指定分组
java
@PostMapping
public String createUser(@Validated(CreateGroup.class) @RequestBody User user) {
return "用户创建成功";
}
@PutMapping("/{id}")
public String updateUser(
@PathVariable Long id,
@Validated(UpdateGroup.class) @RequestBody User user) {
return "用户更新成功";
}
异步任务
-
描述:对多线程异步执行任务功能的封装
-
@Async原理- 被
@Async标记的方法通过AOP提交到配置的ThreadPoolTaskExecutor线程池,异步执行 - 方法可以返回
CompletableFuture,获取异步结果或处理异常
- 被
服务端配置
- Spring3
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd">
<!--开启注解扫描-->
<context:component-scan base-package="使用注解包路径" />
<!-- 启用异步注解驱动 -->
<task:annotation-driven executor="taskExecutor" exception-handler="asyncExceptionHandler"/>
</beans>
- SpringBoot
java
@Configuration
@EnableAsync // 启用异步支持
@ComponentScan(basePackages = "包路径") // 设置注解扫描
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
-
配置
ThreadPoolTaskExecutor线程池:Spring提供的核心线程池,用于实现异步任务,默认会自动配置execute(Runnable task):不阻塞主线程执行,不可接收返回值submit(Runnable task):不阻塞主线程执行,可接收Future返回值shutdown():优雅关闭线程池,停止接受新任务,但会执行完已提交的任务
java
@Configuration
public class AsyncConfig {
@Bean
public ThreadPoolTaskExecutor consumerExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setThreadNamePrefix("OrderConsumer-");
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
@Async指定线程池
java
@Service
public class MyService {
@Async("customTaskExecutor") // 指定使用线程池
public void asyncMethodWithConfiguredExecutor() {
...
}
}
使用异步任务
- 原理:调用带有
@Async的方法时,由AOP拦截调用,并提交到线程池异步执行 - 同类中
@Async方法调用另一个@Async方法时,可能会失效,因为是AOP自调用失效
java
@Service
public class AsyncService {
public void callAsyncMethod() {
CompletableFuture<String> future = executeTaskUsingCustomExecutor();
// 推荐CompletableFuture非阻塞处理
future.whenComplete((result, ex) -> {
if (ex != null) {
// 异常处理
} else {
System.out.println("正常运行");
}
});
}
@Async // 异步任务
public void executeTaskUsingCustomExecutor() {
System.out.println("异步任务");
}
}
异常处理机制
-
异步方法执行后,调用方无法感知异常
- 可以使用
AsyncConfigurer配置统一异常处理 - 可以将异步任务返回
CompletableFuture,响应式处理 - 实际上,异步任务中就应该处理异常,而不是抛出
- 可以使用
java
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { // 异步任务统一异常处理
return (ex, method, params) -> {
// 处理异常,这里不建议抛出异常,因为是由 Spring 异步任务线程在后台调用的,没有外部调用方能捕获你抛出的异常
System.err.println("Async method [" + method.getName() + "] threw exception: " + ex.getMessage());
};
}
}
java
@Service
public class MyAsyncService {
// 将返回类型从 void 修改为 CompletableFuture<Void>
@Async
public CompletableFuture<Void> executeTaskUsingCustomExecutor() {
System.out.println("异步任务开始执行...");
// 在这里执行你的业务逻辑
try {
// 模拟一些耗时操作
Thread.sleep(1000);
System.out.println("异步任务执行完成!");
} catch (InterruptedException e) { // 异步任务中就应该catch处理好异常,而不是向上传递
Thread.currentThread().interrupt(); // 恢复中断状态
// 异常会被包含在返回的 CompletableFuture 中
return CompletableFuture.failedFuture(e);
}
// 如果任务成功完成,返回一个已完成的 CompletableFuture<Void>
return CompletableFuture.runAsync(() -> {
// 这里可以放入任务成功后需要执行的最终逻辑
// 对于不需要返回值的任务,这里通常为空
});
}
public String triggerAsyncTask() {
// 调用异步方法,立即返回一个 CompletableFuture 对象
CompletableFuture<Void> futureTask = asyncService.executeTaskUsingCustomExecutor();
// 你可以在这里继续执行其他逻辑,而不需要等待异步任务完成
System.out.println("Controller 已触发异步任务,正在返回响应...");
// 你也可以对 futureTask 进行一些编排,例如:
futureTask.thenRun(() -> {
System.out.println("【回调通知】:异步任务已经成功完成了!");
}).exceptionally(ex -> {
System.err.println("【回调通知】:异步任务执行失败!原因:" + ex.getMessage());
return null; // 对于 CompletableFuture<Void>,这里返回 null
});
return "ok";
}
}
定时任务
-
目的:自动化循环执行任务,生命周期和Spring容器一致
-
原理:调用
ThreadPoolTaskScheduler任务调度器异步执行,不会阻塞主线程 -
应用场景
- 用户下单后30分钟未付款,每隔固定时间(如 1 分钟)执行一次扫描,取消超时订单
- 保证数据安全性,可以定时对数据库中的数据进行备份
- 保证应用正常运行,可以定时清理过期数据以节省性能
- 利用定时任务实现定时通知的功能
-
替代方案
- Quartz
- MQ
-
推荐工具:在线Cron表达式生成器
服务端配置
- Spring3.x
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd">
<!--开启注解扫描-->
<context:component-scan base-package="使用注解包路径" />
<!--开启定时任务-->
<task:annotation-driven />
</beans>
- SpringBoot
java
@EnableScheduling // 开启定时任务
@SpringBootApplication(scanBasePackages = {"com.wyh"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 配置
ThreadPoolTaskScheduler:既是定时任务的调度器,又是执行器,用于实现定时任务,默认会自动配置
java
@Configuration
public class SchedulerConfig {
@Bean(value = "scheduler") //ThreadPoolTaskScheduler是Spring提供基于线程池的任务调度器,用于实现定时任务
public TaskExecutor threadPoolTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 线程池大小
scheduler.setThreadNamePrefix("scheduled-task-"); // 线程名前缀
scheduler.setAwaitTerminationSeconds(60); // 关闭时等待任务完成的最长时间(秒)
scheduler.setWaitForTasksToCompleteOnShutdown(true); // 关闭时等待任务完成
scheduler.setRemoveOnCancelPolicy(true); // 取消任务时从队列中移除
return scheduler;
}
}
@Scheduled指定调度器
java
@Component
public class MultiSchedulerTasks {
@Scheduled(fixedRate = 1000, scheduler = "scheduler") // 手动指定ThreadPoolTaskScheduler
public void taskWithScheduler1() {
System.out.println("Task on scheduler1: " + Thread.currentThread().getName());
}
}
使用定时任务
-
@Scheduled在spring容器就绪时自动执行,不需要主动调用 -
cron:定义基于日期的复杂定时任务(crontab执行时间计算 - 在线工具)zone参数:时区,例如指定zone = "Asia/Shanghai",默认使用JVM 默认时区,JVM未指定时取系统时区cron参数:整数类型,可以省略年字段,可以使用通配符*:表示所有值,表示 "每单位"(如每分钟、每小时等),例如在秒字段上*表示每秒执行一次?:任意值,即定时任务不关心此字段,用于解决冲突,例如星期和日可能冲突,此时在星期上加?-:区间,例如在小时字段上5-7表示5、6、7点触发/:递增触发,例如在秒字段上5/10表示从第5秒开始每隔10秒触发一次#:序号,例如在周字段上6#3表示每月的第三个周六
-
fixedRate:简单循环执行,指定循环周期(毫秒) -
fixedDelay:下一次任务会在上一次任务完成后,再延时执行(毫秒) -
initialDelay:在定时规则基础上,指定任务第一延迟执行(毫秒)
java
// @Scheduled(cron = "秒 分 时 日 月 星期 [年]", zone = "时区ID")
// 设置每 10 分钟执行一次(无论前一次是否完成)
@Scheduled(cron = "0 */10 * * * * ?") // (cron = 0 0/10 * * * * ?)效果一样,但这样会让人困惑,误以为 0 是特殊标记
public void scheduleTaskTest(){
System.out.println("每 10 分钟执行一次");
}
// 每5秒执行一次(无论前一次是否完成)
@Scheduled(fixedRate = 5000)
public void fixedRateTask() {
System.out.println("Fixed rate task executed at: " + new Date());
}
// 延迟5秒执行(必须等前一次任务执行完毕才会延迟5秒)
@Scheduled(fixedDelay = 5000)
public void fixedDelayTask() {
System.out.println("Fixed delay task started at: " + new Date());
Thread.sleep(3000); // 模拟耗时操作
}
// 首次运行延迟5秒
@Scheduled(
initialDelay = 5000, // 首次延迟 5 秒
cron = "0 */10 * * * * ?" // 之后每 10 分钟执行一次(无论前一次是否完成)
)
public void dailyTask() {
System.out.println("每日任务执行: " + new Date());
}
@Scheduled默认是异步单线程的,即同一个@Scheduled方法的多次调用仍然是串行的,如果并行可以配合@Async开启多线程
java
@Component
public class CombinedTask {
@Async
@Scheduled(fixedRate = 1000)
public void run() {
System.out.println("Running in thread: " + Thread.currentThread().getName());
}
}
异常处理机制
-
定时任务本质是异步任务
- 可以使用
SchedulingConfigurer配置统一异常处理 - 可以将定时任务以异步任务形式处理,套用异步任务的处理逻辑
- 实际上,异步任务中就应该处理异常,而不是抛出
- 可以使用
java
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 1. 配置任务执行器(可选,默认是单线程)
// taskRegistrar.setTaskExecutor(taskExecutor());
// 2. 配置全局异常处理器
/**
* Spring 提供了两个默认的 ErrorHandler:
* TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER:记录异常日志并抑制异常(默认行为)
* TaskUtils.LOG_ERROR_HANDLER:只记录异常日志,不抑制异常(会导致任务执行中断)
*/
taskRegistrar.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);
// 也可以使用自定义处理逻辑
taskRegistrar.setErrorHandler(new CustomSchedulerErrorHandler());
}
}
java
// 如果需要自定义更复杂的异常处理逻辑,可以实现 ErrorHandler 接口
@Slf4j
public class CustomSchedulerErrorHandler implements ErrorHandler {
@Override
public void handleError(Throwable t) {
// 1. 记录详细异常日志
log.error("定时任务执行异常", t);
// 2. 发送告警通知
sendAlert("定时任务异常", t.getMessage());
// 3. 其他自定义处理逻辑
}
}
java
@Service
@Slf4j
public class AsyncTaskService {
@Scheduled(cron = "0/5 * * * * ?")
public void scheduledTask() {
System.out.println("定时任务触发...");
// 调用异步任务,这样定时任务可以直接复用异步任务的处理逻辑
asyncTaskService.executeAsyncTask();
System.out.println("定时任务触发完成");
}
@Async
public void executeAsyncTask() {
try {
System.out.println("异步任务开始执行...");
// 模拟业务逻辑
int i = 1 / 0;
System.out.println("异步任务执行完成");
} catch (Exception e) {
// 异步任务中的异常处理
log.error("异步任务执行失败", e);
// 发送告警等其他处理
}
}
}
事务框架
-
Spring对不同平台的事务功能提供了事务处理器:通过
PlatformTransactionManager接口的实现类管理持久层事务- JDBC事务:通过
DataSourceTransactionManager管理 JDBC、MyBatis 连接的JDBC事务 - JPA 事务:通过
JpaTransactionManager管理 Hibernate、EclipseLink、OpenJPA 连接的JDBC事务 - JTA事务:通过
JtaTransactionManager管理Atomikos、Narayana连接的分布式事务
- JDBC事务:通过
-
事务的隔离级别
DEFAULT(默认):使用连接数据库的默认隔离级别(例如MySQL是可重复度,Oracle是读已提交)READ_UNCOMMITTED:读未提交,可能导致脏读READ_COMMITTED:读已提交REPEATABLE_READ:可重复读SERIALIZABLE:最高的隔离级别,完全锁定事务访问的数据,避免脏读、不可重复读和幻读,但性能较差
-
事务的传播行为
REQUIRED(0,默认):外层回滚时,内层全部回滚REQUIRES_NEW(1):内层事务和外层事务完全独立NESTED(6):内层回滚到保存点,不影响外层SUPPORTS(2):遵循外层事务的回滚行为NOT_SUPPORTED(3):当前事务不参与回滚MANDATORY(4):必须在外层事务中调用,否则直接失败NEVER(5):声明这不是事务,如果其中包含了事务会抛异常
JDBC事务
- 描述:JDBC本地事务的解决方案
- 引入依赖
xml
<!-- Spring 事务模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.23</version> <!-- 版本与你的 Spring 框架一致 -->
</dependency>
<!-- JDBC启动器包含了事务模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Mybatis启动器包含了事务模块 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version> <!-- 兼容 Java 8 -->
</dependency>
绑定数据源和事务管理器
- 每个
DataSource都显式绑定一个DataSourceTransactionManager,管理单个数据库的事务(基于数据库本地事务)
java
@Configuration
// @EnableTransactionManagement // 开启事务注解,Spring Boot 2.x+默认以启用,通常可省略
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/mybatis_db?serverTimezone=Asia/Shanghai");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource
}
@Bean // 如果存在多数据源或者存在不同类型的事务,需要显式指定JDBC事务管理器
public PlatformTransactionManager jdbcTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean // 如果存在多数据源或者存在不同类型的事务,需要显式指定TransactionTemplate
public TransactionTemplate jdbcTransactionTemplate(
@Qualifier("jdbcTransactionManager") DataSourceTransactionManager jdbcTransactionManager) {
return new TransactionTemplate(jdbcTransactionManager);
}
}
声明式事务
-
@Transactional:用于方法上(推荐)或者类上 -
声明事务的失效情景
- 声明式事务中有
try-catch异常后,如果需要回滚事务,一定要注意手动回滚事务 - AOP失效场景(例如未开启注解扫描、静态私有方法不能加事务、方法自调用场景)
- 事务传播级别、事务级别设置错误
- 声明式事务中有
java
@Service
public class UserService{
@Autowired
private UserDao userDao;
@override
@Transactional(
transactionManager = "jdbcTransactionManager", // 如果配置了多个事务管理器就需要指定事务管理器
propagation = Propagation.REQUIRED, //设置事务级别
isolation = Isolation.DEFAULT,
timeout = 300)
public void insertUser(UserPo userPo) {
// 整个方法被事务控制
try{
userDao.insertUser(userPo);
}catch{ // 如果catch就不会自动回滚了,就必须手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
编程式事务
- 通过
DataSourceTransactionManager实现事务管理
java
@Service
public class UserService{
@Autowired // 如果项目只有一个数据源,DataSourceTransactionManager会自动配置,直接注入即可
private DataSourceTransactionManager transactionManager;
@Autowired
@Qualifier("jdbcTransactionManager") // 如果存在多个数据源,必须显式指定绑定的事务管理器
private DataSourceTransactionManager jdbcTransactionManager;
@Autowired
private UserDao userDao;
public void dataSourceTransactionManagerMethod() { // 使用DataSourceTransactionManager实现
// 1. 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); // 设置隔离级别
// 2. 开启事务
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 3. 执行业务逻辑
userDao.insertUser(userPo);
// 4. 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 5. 回滚事务
transactionManager.rollback(status);
throw e;
}
}
}
-
TransactionTemplate是对DataSourceTransactionManager的封装,简化了事务流程TransactionTemplate默认会自动注入事务管理器,如果项目中引入了JTA,会优先注入为JTA类型,此时就需要显式注入- 如果项目有多个数据源或事务管理器,也需要显式配置
TransactionTemplate,并手动注入
java
@Service
public class UserService{
@Autowired // 如果项目中只有一个数据源且只有JDBC事务,那么TransactionTemplate会自动配置,直接注入即可
private TransactionTemplate transactionTemplate;
@Autowired
@Qualifier("localTransactionTemplate") // 如果有多种事务,必须显式指定绑定的事务管理器
private TransactionTemplate localTransactionTemplate;
public void transactionTemplateMethod() {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); // 设置事务级别
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为
// 开始编程式事务,有返回值
String result = transactionTemplate.execute(
status -> {
try {
//开始执行事务
} catch (Exception e) {
status.setRollbackOnly(); //有异常就回滚
throw e;
}
return "ok"; // 如果无返回值就return null
}
);
// 开始编程式事务,无返回值,但还是推荐上面的方法
// transactionTemplate.execute(
// new TransactionCallbackWithoutResult() {
// @Override
// protected void doInTransactionWithoutResult(TransactionStatus status) {
// try {
// //...此处开始执行事务
// } catch (Exception e) {
// status.setRollbackOnly(); //有异常就回滚
// throw e;
// }
// }
// }
// );
}
}
JTA事务
-
描述:JTA是主流的XA模型实现框架,用于解决多数据源产生的分布式事务
- 强一致性,适用于金融领域
- 本地事务会全程锁定,不适合远程调用造成的分布式事务场景
-
引入依赖
xml
<!--------------------------- SpringBoot ----------------------------->
<!-- 推荐使用atomikos实现JTA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<!-- SpringBoot3起不再支持Spring官方启动器,可以使用atomikos提供的启动器 -->
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-spring-boot3-starter</artifactId>
<version>6.0.0</version> <!-- 使用最新版本 -->
</dependency>
<!--------------------------- Spring ----------------------------->
<!-- 手动配置 Atomikos -->
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jta</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<!-- 也可以使用bitronix实现JTA,但bitronix已停止更新,适用于兼容老项目的场景 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-bitronix</artifactId> <!-- Spring Boot 2.3.0起不再支持bitronix启动器 -->
</dependency>
<!-- 手动配置bitronix -->
<dependency>
<groupId>org.codehaus.btm</groupId>
<artifactId>btm</artifactId>
<version>2.1.4</version> <!-- 最新版本 -->
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
配置事务管理器
JtaTransactionManager不需要显式绑定单个数据源,所有XA类数据源都会自动适配指定的JTA事务管理器
java
@Configuration
public class JtaTransactionManagerConfig {
// 1. 配置Atomikos UserTransaction
@Bean
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransaction = new UserTransactionImp();
// 设置事务超时(秒)
userTransaction.setTransactionTimeout(300);
return userTransaction;
}
// 2. 配置Atomikos TransactionManager
@Bean
public TransactionManager atomikosTransactionManager() {
UserTransactionManager transactionManager = new UserTransactionManager();
// JVM正常关闭时,优雅地关闭事务管理器
transactionManager.setForceShutdown(false);
return transactionManager;
}
// 3. 配置Spring的JtaTransactionManager
@Bean
public PlatformTransactionManager jtaTransactionManager(
@Qualifier("userTransaction") UserTransaction userTransaction,
@Qualifier("atomikosTransactionManager") TransactionManager atomikosTransactionManager) throws Throwable {
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransaction(userTransaction);
jtaTransactionManager.setTransactionManager(atomikosTransactionManager);
// 允许自定义隔离级别
jtaTransactionManager.setAllowCustomIsolationLevels(true);
return jtaTransactionManager;
}
// 4.编程式事务按需配置TransactionTemplate
@Bean // 如果存在不同类型的事务,需要显式指定TransactionTemplate
public TransactionTemplate jtaTransactionTemplate(
@Qualifier("jtaTransactionManager") JtaTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}
配置数据源
- 所有涉及的数据源必须包装为XA数据源
java
@Configuration
@MapperScan(
basePackages = {"com.wyh.dao.jta.master"},
sqlSessionFactoryRef = "jtaMasterSqlSessionFactory",
sqlSessionTemplateRef = "jtaMasterSqlSessionTemplate")
public class MasterDataSourceConfig {
@Bean
public DataSource jtaMasterDataSource(){
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
DruidXADataSource dds = new DruidXADataSource(); // XA数据源,自动适配指定的JTA事务管理器
ds.setXaDataSource(dds);
dds.setUsername("root");
dds.setPassword("123456");
dds.setUrl("jdbc:mysql://localhost:3306/learn_project?serverTimezone=GMT%2B8");
dds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUniqueResourceName("masterDataSource"); // XA数据源的唯一标识
return ds;
}
@Bean
public SqlSessionFactory jtaMasterSqlSessionFactory(
@Qualifier(value = "jtaMasterDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate jtaMasterSqlSessionTemplate(
@Qualifier("jtaMasterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
声明式事务
java
@Service
public class UserService {
@Autowired
private MasterUserDao masterUserDao;
@Autowired
private SlaveUserDao slaveUserDao;
@Transactional(
transactionManager = "jtaTransactionManager", // 如果配置了多个事务管理器就需要指定事务管理器
propagation = Propagation.REQUIRED, //设置事务级别
isolation = Isolation.DEFAULT,
timeout = 300)
public void insertUserJta(UserPo masterUserPo, UserPo slaveUserPo) {
masterUserDao.insertUser(masterUserPo);
slaveUserDao.insertUser(slaveUserPo);
}
}
编程式事务
- 通过
JtaTransactionManager实现JTA事务
java
@Service
public class DistributedTransactionService {
@Autowired // 项目只有一种事务管理器,直接注入即可
private JtaTransactionManager jtaTransactionManager;
@Autowired
@Qualifier("jtaTransactionManager") // 如果有多个事务管理器则需要显式指定
private JtaTransactionManager jtaTransactionManager;
@Autowired
private MasterUserDao masterUserDao;
@Autowired
private SlaveUserDao slaveUserDao;
public void executeManualTransaction() {
// 定义事务属性(如隔离级别、超时时间)
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setTimeout(10); // 超时时间(秒)
// 开启事务
TransactionStatus status = jtaTransactionManager.getTransaction(def);
try {
// 操作多个数据源
masterUserDao.insertUser(new UserPo(null, "masterJTA", null));
slaveUserDao.insertUser(new UserPo(null, "slaveJTA", null));
// 提交事务
jtaTransactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
jtaTransactionManager.rollback(status);
throw e;
}
}
}
-
TransactionTemplate是对DataSourceTransactionManager的封装,简化了事务流程TransactionTemplate默认会自动注入事务管理器,如果项目中引入了JTA、JDBC等多种事务,此时就需要显式注入- 如果项目有多个事务管理器,也需要显式配置
TransactionTemplate,并手动注入
java
@Service
public class DistributedService {
@Autowired
private MasterUserDao masterUserDao;
@Autowired
private SlaveUserDao slaveUserDao;
@Autowired // 项目只有一种事务管理器,直接注入即可
private TransactionTemplate transactionTemplate;
@Autowired
@Qualifier("jtaTransactionTemplate") // 如果有多个事务管理器则需要显式指定
private TransactionTemplate jtaTransactionTemplate;
public void executeDistributedTransaction() {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); // 设置事务级别
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为
transactionTemplate.execute(status -> { // 开启事务
try {
masterUserDao.insertUser(new UserPo(null, "masterJTA", null));
slaveUserDao.insertUser(new UserPo(null, "slaveJTA", null));
// 模拟异常(测试事务回滚)
// if (true) throw new RuntimeException("Test rollback");
return true; // 事务提交
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
throw e;
}
});
}
}
- atomikos提供了
UserTransaction实现编程式事务,但无法直接通过UserTransaction接口设置事务隔离级别
java
@Service
public class OrderService {
@Autowired
private UserTransaction userTransaction; // Atomikos 提供的 JTA 事务接口
@Autowired
private MasterUserDao masterUserDao;
@Autowired
private SlaveUserDao slaveUserDao;
public void placeOrderWithUserTransaction() throws Exception {
userTransaction.begin(); // 开启事务
try {
masterUserDao.insertUser(new UserPo(null, "masterJTA", null));
slaveUserDao.insertUser(new UserPo(null, "slaveJTA", null));
// 模拟异常(测试回滚)
throw new RuntimeException("Test rollback");
userTransaction.commit(); // 提交事务
} catch (Exception e) {
userTransaction.rollback(); // 回滚事务
throw e;
}
}
}
Spring Retry
- 描述:分布式系统中,网络抖动、服务短暂不可用等场景下,重试机制是推荐的解决方案,Spring提供了功能更强大的重试机制
开启重试机制
xml
<!-- Spring Retry核心模块 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.4</version> <!-- 使用最新稳定版本 -->
</dependency>
<!-- AOP支持(Spring Retry基于AOP实现) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
声明式重试
- 开启注解功能
java
@SpringBootApplication(scanBasePackages = {"com.wyh"})
@ComponentScan(basePackages = "包路径") // 设置注解扫描
@EnableRetry // 开启重试功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@Retryable:标记需要重试的方法
java
@Service
public class RemoteService {
@Retryable(
value = {IOException.class, TimeoutException.class}, // 触发重试的异常类型
maxAttempts = 3, // 最大重试次数(含首次调用)
backoff = @Backoff(delay = 1000, multiplier = 2) // 指数退避:首次1秒,后续每次间隔翻倍
)
public String callRemoteApi(String param) throws IOException {
// 模拟网络波动
if (Math.random() > 0.5) {
throw new IOException("Network error");
}
return "Success: " + param;
}
}
@Recover:定义重试失败后的回调方法
java
javaimport org.springframework.retry.annotation.Recover;
@Service
public class RemoteService {
// ... @Retryable方法 ...
@Recover
public String recover(IOException e, String param) {
System.err.println("Failed after retries: " + e.getMessage());
return "Fallback: " + param; // 返回默认值或执行补偿逻辑
}
}
- 通过
exceptionExpression动态判断是否重试
java
java@Retryable(
value = {Exception.class},
exceptionExpression = "#{#root.message.contains('temporary')}" // 仅当异常消息包含"temporary"时重试
)
public void sensitiveOperation() {
// ...
}
编程式重试
- 配置重试策略
java
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();
// 1. 固定间隔策略(每次间隔 1s)
FixedBackOffPolicy fixedBackOff = new FixedBackOffPolicy();
fixedBackOff.setBackOffPeriod(1000);
template.setBackOffPolicy(fixedBackOff);
// 2. 指数退避策略(初始 500ms,倍数 2,最大 3s)
ExponentialBackOffPolicy expBackOff = new ExponentialBackOffPolicy();
expBackOff.setInitialInterval(500);
expBackOff.setMultiplier(2);
expBackOff.setMaxInterval(3000);
template.setBackOffPolicy(expBackOff);
return template;
}
}
- 编程式使用
java
@Service
public class RedisService {
@Autowired
private RetryTemplate retryTemplate;
public String getDataWithRetry(String key) {
try {
// 执行带重试的逻辑
return retryTemplate.execute(
(RetryCallback<String, RuntimeException>) context -> {
// 业务逻辑(可能抛出需要重试的异常)
System.out.println("执行业务");
},
// 重试耗尽后的回退逻辑
(RecoveryCallback<String>) context -> {
System.err.println("Retry exhausted, return fallback value");
return "fallback_value";
}
);
} catch (RuntimeException e) {
// 处理未捕获的异常(如非重试异常)
throw new RuntimeException("Final failure", e);
}
}
}
三方依赖重试
- 三方依赖可以结合Spring Retry,更方便地实现重试机制,例如redis连接重试机制
java
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 为 RedisTemplate 设置 RetryTemplate
template.setRetryTemplate(redisRetryTemplate());
// 其他序列化配置(可选)
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
// 如果需要 StringRedisTemplate,同样配置
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
template.setRetryTemplate(redisRetryTemplate());
return template;
}
private RetryTemplate redisRetryTemplate() {
RetryTemplate template = new RetryTemplate();
// 1. 设置重试策略(按异常类型)
SimpleRetryPolicy policy = new SimpleRetryPolicy(
3, // 最大重试次数
Map.of(
RedisConnectionFailureException.class, true, // 连接失败重试
RedisSystemException.class, true, // Redis 服务异常重试
RedisCommandTimeoutException.class, true // 命令超时重试
)
);
template.setRetryPolicy(policy);
// 2. 设置退避策略(指数退避)
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(500); // 初始间隔 500ms
backOffPolicy.setMultiplier(2); // 每次间隔翻倍
backOffPolicy.setMaxInterval(3000); // 最大间隔 3s
template.setBackOffPolicy(backOffPolicy);
return template;
}
}
Spring ThreadPool
-
ThreadPoolTaskExecutor本质上就是 Spring 对 Java 原生ThreadPoolExecutor的封装和增强ThreadPoolExecutor需要手动创建和关闭,ThreadPoolTaskExecutor可以加入IOC管理生命周期**ThreadPoolExecutor构造函数参数多,配置繁琐,ThreadPoolTaskExecutor提供 setter 方法,支持链式调用ThreadPoolExecutor仅支持Runnable/Callable,ThreadPoolTaskExecutor还支持ListenableFuture/TaskDecorator
java
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:线程池中长期保持的线程数量,即使它们处于空闲状态
executor.setCorePoolSize(5);
// 最大线程数:线程池在任务激增时可以创建的最大线程数量
executor.setMaxPoolSize(10);
// 队列容量:用于存放等待执行的任务的队列大小
// 如果任务数量超过队列容量,且当前线程数小于最大线程数,则会创建新线程
// 如果队列已满且线程数已达最大值,则会触发拒绝策略
executor.setQueueCapacity(20);
// 线程空闲时间:超过核心线程数的线程,在空闲多长时间后会被销毁
executor.setKeepAliveSeconds(60);
// 线程名称前缀:方便在日志和调试中识别线程
executor.setThreadNamePrefix("MyPool-");
// 拒绝策略:当任务无法被执行时(队列满且线程数达到最大值)的处理方式
// ThreadPoolExecutor.CallerRunsPolicy 是一个很好的选择,它会让提交任务的线程自己来执行这个任务,起到限流的作用
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 一般不需要显式初始化线程池,因为 Spring Bean 默认后置逻辑会自动调用 initialize() 方法来完成初始化
executor.initialize(); // 注意!如果线程池是局部变量就必须手动初始化
return executor;
}
}
快速使用
execute(Runnable task):不阻塞主线程执行,不可接收返回值submit(Runnable task):不阻塞主线程执行,可接收CompletableFuture返回值shutdown():优雅关闭线程池,停止接受新任务,但会执行完已提交的任务,一般不用手动关闭,Spring会自动管理
java
@Service
public class MyTaskService {
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
/**
* 提交一个无返回值的任务
*/
public void executeAsyncTask() {
taskExecutor.execute(() -> {
System.out.println("执行无返回值的异步任务... 线程名: " + Thread.currentThread().getName());
// 在这里编写你的业务逻辑
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println("无返回值的异步任务执行完成");
});
}
/**
* 提交一个有返回值的任务
* @return Future 对象,用于获取任务执行结果
*/
public Future<String> submitAsyncTask() {
return taskExecutor.submit(() -> {
System.out.println("执行有返回值的异步任务... 线程名: " + Thread.currentThread().getName());
// 在这里编写你的业务逻辑
Thread.sleep(1000); // 模拟耗时操作
String result = "任务执行成功: " + System.currentTimeMillis();
System.out.println("有返回值的异步任务执行完成");
return result;
});
}
/**
* 提交一个有返回值的任务,并返回 CompletableFuture
* @return CompletableFuture<String> 对象,用于异步获取结果和编排任务
*/
public CompletableFuture<String> submitAsyncTask() {
// 使用 CompletableFuture.supplyAsync() 提交任务
return CompletableFuture.supplyAsync(() -> {
System.out.println("执行有返回值的异步任务... 线程名: " + Thread.currentThread().getName());
// 在这里编写你的业务逻辑
String result = "任务执行成功: " + System.currentTimeMillis();
System.out.println("有返回值的异步任务执行完成");
return result;
}, taskExecutor); // 指定使用我们的线程池
}
}
任务执行增强
-
ListenableFuture是 Spring 对 Java 原生Future的扩展,它允许你注册回调函数,在任务完成时自动触发,而无需阻塞等待结果- JDK8之前还没有提供
CompletableFuture,因此使用Spring提供的ListenableFuture - JDK8+推荐直接使用
CompletableFuture
- JDK8之前还没有提供
java
public class MyTaskService {
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
/**
* 提交任务并返回 ListenableFuture
*/
public ListenableFuture<String> submitListenableTask() {
Callable<String> task = () -> {
System.out.println("执行任务... 线程名: " + Thread.currentThread().getName());
// 模拟耗时操作
Thread.sleep(1000);
return "任务执行成功";
};
// 使用 taskExecutor.submitListenable() 提交任务
return taskExecutor.submitListenable(task);
}
}
TaskDecorator用于包装Runnable/Callable,允许你在任务执行前后对任务进行装饰,例如添加日志、传递上下文等
java
public class MyTaskDecorator implements TaskDecorator {
// 重载方法,用于包装 Runnable 任务
@Override
public Runnable decorate(Runnable runnable) {
return () -> {
// 任务执行前的逻辑
System.out.println("任务开始执行...");
long startTime = System.currentTimeMillis();
try {
// 执行原始任务
runnable.run();
} finally {
// 任务执行后的逻辑
long endTime = System.currentTimeMillis();
System.out.println("任务执行完成,耗时: " + (endTime - startTime) + "ms");
}
};
}
// 重载方法,用于包装 Callable 任务
@Override
public <T> Callable<T> decorate(Callable<T> callable) {
return () -> {
System.out.println("Callable 任务开始执行...");
long startTime = System.currentTimeMillis();
try {
return callable.call();
} finally {
long endTime = System.currentTimeMillis();
System.out.println("Callable 任务执行完成,耗时: " + (endTime - startTime) + "ms");
}
};
}
}
Spring Util
-
Spring对常见业务场景相关功能封装了工具类
org.springframework.util- 代码瘦身
- 使用工具类有助于避免常见的异常场景,例如避免空指针异常
| 工具类 | 核心功能 | 常用方法示例 | 使用场景 |
|---|---|---|---|
StringUtils |
字符串判空、操作与转换 | isEmpty(), hasText(), trimWhitespace(), tokenizeToStringArray() |
参数校验、字符串处理 |
CollectionUtils |
集合/数组判空、操作与转换 | isEmpty(), intersection(), mergeArrayIntoCollection(), firstElement() |
集合操作、数据合并 |
ObjectUtils |
对象安全比较与转换 | nullSafeEquals(), nullSafeToString(), firstNonNull() |
对象比较、非空处理 |
Assert |
参数校验,失败时抛出异常 | notNull(), isTrue(), hasText() |
输入校验、业务逻辑前置条件检查 |
FileCopyUtils |
文件与流复制,自动关闭资源 | copy(source, target) |
文件操作、资源复制 |
StreamUtils |
流操作封装(字符串、字节数组转换) | copyToString(), copyToByteArray() |
流处理、数据转换 |
ReflectionUtils |
简化反射操作 | findMethod(), invokeMethod() |
动态调用方法、反射场景 |
字符串工具类
-
StringUtilshasText():判断字符串所有空白符后是否有实际内容,取代原来的isEmpty()方法trimWhitespace():去除首尾空白(支持更广泛的 Unicode 空白符)collectionToDelimitedString():将集合(如List、Set等)中的元素拼接成一个字符串,并用指定的分隔符连接tokenizeToStringArray():字符串分割为数组
java
javaboolean isEmpty = StringUtils.isEmpty(""); // true, 注意isEmpty方法已废弃
boolean hasContent = StringUtils.hasText(" hello "); // true
String[] parts = StringUtils.tokenizeToStringArray("a,b,c", ","); // ["a", "b", "c"]
List<String> list = Arrays.asList("Apple", "Banana", "Orange");
String result = StringUtils.collectionToDelimitedString(list, ", "); // "Apple, Banana, Orange"
集合与数组工具类
-
CollectionUtilsisEmpty():判断集合/数组是否为空intersection():求交集mergeArrayIntoCollection():合并数组到集合firstElement():获取第一个元素containsAny():判断是否包含任意指定元素
java
javaList<String> list1 = Arrays.asList("A", "B", "C");
List<String> list2 = Arrays.asList("B", "C", "D");
Collection<String> common = CollectionUtils.intersection(list1, list2); // ["B", "C"]
MultiValueMap:Spring提供的支持键对应多个值的Map(如HTTP请求参数),提供add()、get()方法
java
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("Header", "Content-Type:application/json");
params.add("Header", "Content-Length:1024");
List<String> colors = params.get("Header"); // ["Content-Type:application/json", "Content-Length:1024"]
对象操作工具类
-
ObjectUtilsnullSafeEquals():安全比较对象nullSafeToString():安全转字符串firstNonNull():返回第一个非空对象isEmpty():判空
java
javaboolean equal = ObjectUtils.nullSafeEquals("abc", "abc"); // true
String safeStr = ObjectUtils.nullSafeToString(null); // "null"
-
Assert:参数校验,校验失败时抛出IllegalArgumentExceptionnotNull():非空校验isTrue():条件校验hasText():字符串内容校验
java
javaAssert.notNull(userId, "用户ID不能为空");
Assert.isTrue(age > 0, "年龄必须大于0");
IO流工具类
FileCopyUtils:简化文件复制、流操作,自动处理资源关闭
java
java// 文件复制
FileCopyUtils.copy(sourceFile, targetFile);
// 流到字符串
String content = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
StreamUtils:流操作封装,支持copyToString()、copyToByteArray()等
java
byte[] data = StreamUtils.copyToByteArray(inputStream);
反射工具类
ReflectionUtils:简化反射操作,自动处理权限问题(如setAccessible(true))
java
javaMethod method = ReflectionUtils.findMethod(User.class, "getName");
Object result = ReflectionUtils.invokeMethod(method, user);
其他实用工具类
DigestUtils:提供MD5、SHA-1等哈希算法,用于密码加密或数据校验
java
String md5 = DigestUtils.md5DigestAsHex("password".getBytes());
Spring Listener
-
描述:Spring对观察者模式的功能封装,即当一个对象的状态发生改变时,所有依赖于它的对象都会实时更新并执行指定通知
-
应用场景
ContextStartedEventListener:监听ContextStartedEvent,在应用上下文启动时执行逻辑(如初始化资源)ContextRefreshedEventListener:监听ContextRefreshedEvent,在应用刷新时触发ContextClosedEventListener:监听ContextClosedEvent,在上下文关闭时清理资源ContextLoaderListener:是Spring Web的入口,负责初始化Web应用上下文- 业务事件通知:相当于实现一个轻量级的、进程内的消息队列**
- 监听外部系统事件(如消息队列、数据库变更),一般三方系统都集成了监听功能
-
监听事件:触发监听器执行的对象
ContextRefreshedEvent:ApplicationContext初始化或刷新时初始化资源ContextStartedEvent:ApplicationContext启动服务ContextClosedEvent:ApplicationContext关闭时资源释放ContextStoppedEvent:ApplicationContext停止时暂停服务RequestHandledEvent:HTTP请求处理完成时处理请求日志- 自定义事件:继承
ApplicationEvent类
java
public class CustomEvent extends ApplicationEvent { // 自定义要监听的事件
private String message;
public CustomEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
@Service
public class EventPublisherService { // 自定义事件刷新入口
@Autowired
private ApplicationEventPublisher publisher;
public void publishCustomEvent(String message) { //发布内容
publisher.publishEvent(new CustomEvent(this, message));
}
}
注解式
- 使用
@EventListener注解
java
@Component
public class MyEventListener { // 注解方式
@EventListener
public void handleContextRefresh(ContextRefreshedEvent event) {
System.out.println("使用@EventListener监听容器刷新事件");
}
// 可以监听多个事件
@EventListener({ContextStartedEvent.class, ContextStoppedEvent.class})
public void handleMultipleEvents(SpringContextEvent event) {
System.out.println("监听到容器状态变化: " + event.getClass().getSimpleName());
}
}
编程式
- 实现实现
ApplicationListener接口
java
@Configuration
public class AppConfig {
@Bean
public MyApplicationListener myApplicationListener() {
return new MyApplicationListener();
}
@Bean
public ContextRefreshedEventListener myApplicationListener() {
return new MyApplicationListener();
}
}
public class ContextRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("监听到容器状态变化: " + event.getClass().getSimpleName());
// 这里可以执行数据库初始化、缓存预热等操作
}
}
public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
System.out.println("监听到容器状态变化: " + event.getClass().getSimpleName());
}
}
属性引用
-
目的:可快速访问其他文件中的数据
- 动态属性注入:在Spring配置中动态设置属性值(如
@Value注入动态值) - 条件化配置:根据表达式结果决定是否启用某个Bean或配置
- 数据绑定与验证:在表单验证、数据转换时动态计算值
- AOP与安全控制:在切面逻辑或权限表达式中使用动态条件
- 集成测试:在测试中模拟或验证动态行为
- 动态属性注入:在Spring配置中动态设置属性值(如
属性占位符
- 属性占位符:使用
${} - Spring3.x中,需配合
<context:property-placeholder>或<PropertySourcesPlaceholderConfigurer>,用于加载配置文件
xml
...
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
<property name="url" value="${jdbc.url.dev}" />
<property name="username" value="${jdbc.username.dev}" />
<property name="password" value="${jdbc.password.dev}" />
...
- SpringBoot中,可以通过
@Value注解引用Application.yaml的属性,外部文件可以通过spring.config.import引入
java
@Value("${app.name}") // 从Application.yaml中引用属性
private String appName;
SpEL表达式
- SpEL表达式:使用
#{},用于其他 Bean 的属性、执行逻辑运算、调用方法
java
@Value("#{1 + 1}") // 计算结果为 2
private int result;
@Value("#{user.name}") // 调用名称为 user 的Bean的 getName()方法
private String name;
@Value("#{@applicationContext.getBean('user').name}") // 显式通过 ApplicationContext 获取名称为user的Bean的getName()
private String name;
@Value("#{appConfig.user.username}") // 访问 Bean 嵌套属性
private String username;
@Value("#{T(java.lang.Math).random() * 100}") // 调用方法
private double randomNumber;
@Value("#{'123' matches '\\d+'}") // 匹配正则,true
private boolean isNumber;
@Value("#{{1, 2, 3, 4}.?[#this > 2]}") // 集合操作,[3, 4](过滤大于 2 的元素)
private List<Integer> filteredList;
@Value("#{{'a', 'b', 'c'}![#this.toUpperCase()]}") // 集合操作,["A", "B", "C"](映射转大写)
private List<String> upperCaseList;
SpringBoot
-
版本要求
- SpringBoot稳定版本2.x,目前主流版本正在转向SpringBoot3.x
- Java稳定版本JDK8,目前主流版本正在转向JDK17
- maven版本要求3.6.7及以上
-
标准
pom.xml结构:如果使用阿里云脚手架建议修改为标准结构
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.wyh</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>demo</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动流程
-
大致流程
- 运行启动类:创建
SpringApplication实例 - 加载
ApplicationListener实例:监听应用生命周期的所有事件 - 加载环境:读取配置文件(本地+远程)、命令行参数、系统属性等
- 创建应用上下文:创建并配置
ApplicationContext - Bean 初始化:加载IOC,执行依赖注入(DI)
- 启动内嵌服务器:默认Tomcat
- 回调执行:执行
ApplicationRunner/CommandLineRunner,运行容器启动后的个性化需求
- 运行启动类:创建
-
容器回调:
ApplicationRunner在容器最后阶段执行,早于ApplicationReadyEvent,晚于ContextRefreshedEvent**
java
/**
* 任务简单且无需容器参数解析场景下,推荐使用CommandLineRunner,代码复杂度低
*/
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 执行初始化任务
System.out.println("CommandLineRunner executed with args: " + Arrays.toString(args));
}
}
java
/**
* ApplicationRunner是对CommandLineRunner的原始参数进行进一步封装,提供更灵活的参数解析能力
*/
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 获取命令行参数
System.out.println("ApplicationRunner executed with args: " + args.getSourceArgs());
// 获取选项参数(如--name=value)
if (args.containsOption("name")) {
System.out.println("Option name value: " + args.getOptionValues("name"));
}
}
}
依赖管理机制
- SpringBoot项目会带有一个父项目
Spring-boot-starter-parent,它对绝大部分技术的场景启动器做了版本管理 - 一些三方依赖也适配SpringBoot做了场景启动器,但Spring官方并未对其版本管理,建议在搭建项目前查询官网适配版本
xml
<!--父项目进行版本管理-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- mysql -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope> <!-- SpringBoot自动管理mysql驱动版本 -->
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version> <!-- SpringBoot并未管理druid版本 -->
</dependency>
</dependencies>
自动配置机制
-
场景启动器:场景启动器包含了功能需要的所有依赖
Spring-boot-starter-xxx是Spring官方提供的场景启动器;xxx-Spring-boot-starter是第三方提供的场景启动器- 官方场景启动器会自动管理版本,三方场景启动器需要手动指定版本、
- Spring会在启动时自动注册场景启动器的相关Bean,属性取默认值,可以后续修改\
-
自动配置流程
- 启动 Spring Boot 应用:通过
@SpringBootApplication(scanBasePackages = {"com.wyh"})启动类启动 - 扫描自动配置类: 加载所有自动配置类
- 条件化匹配:根据当前项目的依赖和配置,决定哪些自动配置类生效
- 创建 Bean 并绑定属性:为生效的自动配置类创建 Bean,并绑定
application.yml中的配置(如果有) - 注入到 Spring 容器:将配置好的 Bean 注册到 IoC 容器中
- 启动 Spring Boot 应用:通过
-
全局配置文件和配置类
- 如果场景启动器相关Bean如果修改属性过程较复杂,建议使用配置类(例如配置
rabbitMQ) - 非场景启动器相关Bean,SpringBoot不会自动注册,必须使用配置类注册(例如配置
redisson) - 简单场景或者静态属性可以使用配置文件
- 如果场景启动器相关Bean如果修改属性过程较复杂,建议使用配置类(例如配置
java
@Configuration
public class RedissonConfig {
//redisson没有场景启动器,因此需要使用配置类注册Bean
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.setCodec(new StringCodec())
.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("123456")
.setDatabase(0);
return Redisson.create(config);
}
}
java
@Configuration
public class RabbitMQConfig {
//rabbitMQ虽然有场景启动器,但初始化配置过程很复杂,不适合放在全局文件中
@Bean
public ConnectionFactory rabbitConnectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
...
return factory;
}
}
嵌入式容器
- SpringBoot引入场景启动器后,会自动内置所需的容器,默认使用Tomcat,可以在全局配置文件中设置Tomcat相关参数
yaml
server:
port: 8443 # 监听端口
address: 0.0.0.0 # 监听所有网络接口(限制哪些ip可以访问),或指定IP(如 192.168.1.100)
servlet:
context-path: /api # 所有请求路径前缀(如 IP:port/api/users)
session:
timeout: 30m # 会话持续30分钟(单位:秒或ms,如 30m)
persistent: false # 是否持久化Session到磁盘(默认false)
ssl:
enabled: false # 开启SSL
key-store: classpath:keystore.p12 # 证书文件路径
key-store-password: yourpassword # 证书密码
key-store-type: PKCS12 # 证书类型(PKCS12或JKS)
key-alias: tomcat # 证书别名(可选)
compression:
enabled: true # 启用Gzip压缩
mime-types: text/html,text/css,application/json # 压缩的MIME类型
min-response-size: 1024 # 响应体大于1KB时压缩(默认2048)
tomcat:
threads: # 线程池配置(Tomcat 专用)
max: 200 # 最大线程数(默认200)
min-spare: 10 # 最小空闲线程数(默认10)
accept-count: 100 # 等待队列长度(默认100)
配置类增强功能
-
@ConditionalOnXxx:条件成立时才会通过配置类注册Bean@ConditionalOnClass:如果项目中存在这个类,则触发行为@ConditionalOnMissingClass:如果项目中不存在这个类,则触发指定行为@ConditionalOnBean:如果IOC容器中存在组件,则触发指定行为@ConditionalOnMissingBean:如果IOC容器中不存在组件,则触发指定行为@ConditionalOnProperty:如果配置文件中有对应的值才执行
-
@ConfigurationProperties:由统一配置文件信息注入Bean,较于@Value,避免了繁杂的配置文件,可以集中快速管理- 配置类必须有
getter、setter @ConfigurationProperties(prefix = "xxx")选择配置类中的键,支持多层级匹配- 支持松散匹配(
databaseUrl↔database-url),但建议使用严格匹配 - 非配置类中加载也可以加在属性值,在类上加
@EnableConfigurationProperties即可 @ConfigurationProperties也可以注解于方法上,映射返回值实例
- 配置类必须有
yaml
#application.yaml
db:
druid:
url: jdbc:mysql://localhost:3306/learn_project?serverTimezone=GMT%2B8
username: root
password: 123456
java
@Configuration
@ConfigurationProperties(prefix = "db.druid")
@Data // 注意!必须有getter、setter
public class DataSourceConfiguration {
private String url; // 成员属性值会自动注入
private String username;
private String password;
@Bean
@ConditionalOnClass(DruidDataSource.class) //有DruidDataSource时才加载Bean
public DataSource druidDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
多环境配置文件
-
通过
application.yml文件中指定配置文件application-dev.yml:开发环境application-test.yml:测试环境application-prod.yml:生产环境
yml
# 通过 application.yml 激活
spring:
profiles:
active: dev #使用application-dev.yml
yml
#application-dev.yml
server:
port: 8080
address: 127.0.0.1
bash
java -Dspring.profiles.active=dev -jar myapp.jar
动态注册Bean
-
配置类通过
@Profile("xxx")指定Bean加载条件- 逻辑或表达式:
@Profile({"dev", "test"})、@Profile("dev | test")(Spring 6+ 支持) - 逻辑与表达式:
@Profile("dev & test") - 否定表达式:
@Profile("!prod") - 复杂表达式:
@Profile("dev & !cloud")
- 逻辑或表达式:
java
@Configuration
public class DataSourceConfig {
// 测试环境数据源
@Bean
@Profile("test") // 【spring.profiles.active: test】生效、或者测试类指定@Profile("test")也生效
public DataSource devDataSource() {
return DataSourceBuilder.create()
.url("jdbc:h2:mem:devdb")
.username("sa")
.password("")
.build();
}
@Bean
@Profile("!test") // 【spring.profiles.active: 非test】生效、或者测试类没有指定@Profile("test")就生效
public DataSource prodDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://prod-server:3306/proddb")
.username("admin")
.password("secure123")
.build();
}
}
- 配置文件指定环境时,如果环境名称和
@Profile属性一致,也可以使对应Bean生效
yaml
spring:
profiles:
active: test # @Profile("test")标注的Bean会生效
- 测试类的方法或者类上标注
@ActiveProfiles("xxx"),也可以使对应@Profile("xxx")的Bean生效
java
@Configuration
public class DataSourceConfig {
@Bean
@Profile("test") // yaml文件指定或者@ActiveProfiles("test")指定时,此Bean才会生效
public DataSource devDataSource() {
return DataSourceBuilder.create()
.url("jdbc:h2:mem:testdb")
.username("sa")
.build();
}
@Bean
public DataSource prodDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://prod-server:3306/db")
.username("admin")
.build();
}
}
java
@ActiveProfiles("test") // 标注 @Profile("test") 的Bean会生效,未标注 @Profile 的 Bean 依然会生效
public class DataSourceTest extends BaseTest{
@Autowired
@qualifier("devDataSource") // 注意激活Bean可能会导致Bean类型重复,往往需要按名称匹配
private DataSource dataSource;
@Test
public void testDevDataSource() {
// 验证注入的是 H2 内存数据库(dev 环境)
assertThat(dataSource.getClass()).isEqualTo(HikariDataSource.class);
}
}