Spring框架教程

Springframework

  • 应用场景:Spring是一个开源设计层面框架,实现了业务逻辑层和其他层的解耦,它的核心思想是面向接口编程
模块 核心功能 典型场景
Core Container IOC/DI、Bean 管理 基础对象创建
Data Access JDBC、ORM、事务 数据库操作
Web MVC、WebFlux Web 应用开发
AOP 切面编程 日志、事务
Test 测试支持 单元/集成测试
Security 认证、授权 安全控制
Integration 消息、批量任务 企业集成
  • Spring发展过程

    1. Spring3.x:通过xml文件加载IOC
    2. Spring4.x:废弃xml文件,推荐使用配置类+配置文件方式加载IOC
    3. Spring Boot:基于Spring4+的Web项目框架,极大地简化了Web应用的配置

IOC

  • 描述:是对工厂模式和单例模式的综合实现,开发者不需要再临时手动创建配置、获取对象

    1. 代码解耦,提高代码可读性,和可维护性
    2. IOC默认实现了单例模式,提高了性能

Bean生命周期

阶段 触发方式
Bean实例化(Instantiation) 构造方法注入
依赖注入(Dependency Injection) 字段注入、Setter注入
BeanPostProcessor 前置处理 实现BeanPostProcessor接口的postProcessBeforeInitialization()方法
初始化前(Initializing) @PostConstruct
BeanPostProcessor 后置处理 实现BeanPostProcessor接口的postProcessAfterInitialization()方法
使用 调用业务方法
销毁,容器关闭时调用 @PreDestroy

Bean注册(IOC)

  • 描述:将创建的类实例加入IOC容器中

  • 注册方式的应用场景

    1. 命名空间注册方式只用于Spring旧项目,不建议使用
    2. 业务类注解通过构造方法注册IOC,初始化过程简单快速,适合加载业务组件快速使用其方法
    3. 配置类通过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

    1. 动态代理:配置类会生成一个代理类,代理类会拦截所有 @Bean 方法的调用
    2. 调用拦截:拦截所有 @Bean 方法后,代理类会先检查 Bean 是否单例,防止重复创建对象
    3. 注册IOC:如果容器中已有单例则直接返回,否则创建新实例并放入容器
  • 配置类相关注解

    1. @Configuration:加在类上,表示这个类是配置类,Spring会默认加载所有配置类
    2. @Bean:将返回值注册Bean,必须用于Spring管理的类中(一般是配置类),否则注册的Bean无法自动注入
    3. @Value:引用全局配置文件(application.yaml),注入属性值
    4. @PropertySource:配合@Value,可以加载属性文件中的值( .properties.yml
    5. @ImportResource:将文件中定义的 Bean 注册到 Spring 容器中,用于新项目兼容老项目对 XML 的支持
    6. @scope:Bean 的作用域(如单例、原型、请求、会话等)
    7. @Lazy:Bean懒加载,即使用时才注册
    8. @ComponentScan:指定路径下开启注解扫描,否则注解不生效
    9. @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名称是类名首字母小写

    1. @Component:普通类
    2. @Service:业务层
    3. @Controller:控制器层
    4. @Repository:持久化层
  • 原理

    1. 开启扫描:Spring启动默认不加载通用注解注册,需要再开启注解扫描
    2. 注册IOC:开启注解扫描后,会反射构造器(默认空参),创建Bean加入IOC
    3. 注入Bean:注册完毕后,其他Bean也可以作为成员注入(详见下文)
  • 开启注解扫描方法

    1. Spring3:在xml文件中<context:component-scan base-package="包路径"/>开启
    2. SpringBoot:启动类默认开启了同路径注解扫描,也可以在配置类中使用@ComponentScan("包路径")开启注解扫描
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实例

  • 注入方法应用场景

    1. 不要手动加载Bean,否则会破坏单例模式
    2. 一般业务类中首选字段注入
    3. 如果存在循环依赖风险,应该使用构造器方法显式注入

手动加载

  • ClassPathXmlApplicationContext:获取xml定义的Bean
  • AnnotationConfigApplicationContext:获取注解/配置类定义的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动态代理)

    1. 将业务核心和增强部分解耦,实现代码复用
    2. 统一管理增强类,提升可维护性
  • 应用场景

    1. 事务处理(@transactional
    2. 权限校验(Spring Security
    3. 异步任务(@Async
    4. 参数校验(@Validated
    5. Sentinel流控异常处理(@SentinelResource
    6. 打印日志
    7. AOP是运行时注解处理器的最佳解决方案
  • AOP失效场景

    1. 方法自调用场景
    2. privatefinalstatic代理会失效
    3. 没有加入IOC
    4. 切面类互相干扰
  • AOP自调用失效的解决方案

    1. @EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true):绕过 this 引用,直接调用代理对象的方法
    2. 业务类注入自身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 定义切面类,编写通知逻辑应用到指定业务类

    1. 前置通知: @Before
    2. 返回通知: @AfterReturning
    3. 后置通知: @After
    4. 异常通知: @AfterThrowing
    5. 环绕通知: @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是运行时注解处理器的最佳解决方案

    1. 运行时实现:运行时通过反射动态执行逻辑,首选 AOP
    2. 编译时实现:在类加载或编译时修改字节码,可以使用 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)

    1. MVC通过封装原生Servlet对HTTP请求的API,极大地简化了控制器层的开发
    2. 每一个HTTP请求由Servlet 容器的线程池为其分配一个线程,请求结束后线程自动回收
    3. 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请求

    1. HTTP请求是无状态短连接(三次握手四次挥手),WebSocket请求是双向实时通信的长连接
    2. HTTP请求适用于 请求-响应式交互场景,WebSocket请求适用于需要状态保持的场景
  • HTTP请求和HTTPS请求

    1. HTTP 是超文本明文传输,HTTPS在【TPC/IP传输层】 和 【HTTP 应用层】之间加入了 SSL/TLS 安全协议,实现加密传输

    2. HTTP 的端口号是 80,HTTPS 的端口号是 443

    3. HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的

请求结构

  • 请求结构:标准的HTTP请求包含了三部分,请求行、请求头、请求体

  • 请求行结构:<Method(请求方法)> <Request-URI(请求资源)> <HTTP-Version(请求协议)>

    1. Method:设置请求方法,常见有GETPUTDELETEPOST
    2. Request-URI:请求资源路径,查询参数用?/连接
    3. 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/jsonmultipart/form-data
    Content-Length 请求体长度 1024
    Accept 接收的响应类型 text/htmlapplication/jsonimage/png
    X- 自定义请求头约定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请求映射

  • 注解匹配

    1. 请求方法:@getMapping@postMapping@putMapping@deleteMapping
    2. 请求参数:@RequestParam(入参可为空),@PathVariable(入参不为空)、 @DateTimeFormat(pattern = "yyyy-MM-dd")
    3. 请求头:@RequestHeader@CookieValue
    4. 请求体:@requestBody会将请求体反Json序列化为指定实例
  • HttpServletRequest API

    1. 请求头:getHeadergetCookies
    2. 请求体:getReader()
  • 最佳实践

    1. 请求方法通过注解形式映射
    2. 请求参数通过注解形式映射
    3. 请求体通过注解形式映射
    4. 请求头通过HttpServletRequest API形式映射

响应结构

  • 一个标准的响应包括三部分:响应状态码、响应头、响应体

  • 响应行:协议版本 + 响应状态码

  • 响应头:以键值对形式存储数据

    响应头key 说明 举例
    Content-Type 响应体的数据类型 application/jsonapplication/pdfimage/png
    Content-Length 响应体的数据长度 1024
    Content-Disposition 浏览器处理响应体方式 attachment; filename="example.pdf"(不直接显式下载的文件)
    Location 重定向目标 URL Location: https://example.com/new-path
    Header 自定义响应头 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返回响应

  • 注解匹配

    1. 响应码:@ResponseStatus
    2. 响应体:@ResponseBody会将方法返回值自动序列化为Json字符串写入响应体
  • HttpServletRequest API

    1. 响应码:setStatus()
    2. 响应头:setHeader()setContentType()setCharacterEncoding()
    3. 响应体:getWriter().write()ImageIO.write()
  • ResponseEntity

    1. 响应码:.status()
    2. 响应头:.header().contentType()
    3. 响应体:.body()
  • 最佳实践

    1. 响应头通过HttpServletRequest API形式映射
    2. 响应体通过注解形式映射
    3. 简单场景推荐使用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
    }
}

统一返回体

  • 描述:为了精细化业务异常,建议将响应体统一格式

    1. 强制所有接口返回统一格式,避免因开发者习惯不同导致响应结构混乱,前端也便于统一处理响应
    2. 业务逻辑上的异常返回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配置类

  • 使用方法

    1. 实现组件接口,编写逻辑内容
    2. WebMvcConfigurer配置类中的重写方法中引用组件接口
  • 常用组件方法

    1. addInterceptors拦截器
    2. addCorsMappings跨域设置
    3. configureContentNegotiation:内容协商设置
    4. addResourceHandlers:静态资源处理器
    5. resourceViewResolver:视图解析器(前后端分离项目中已不再使用)
    6. configureMessageConverters/extendMessageConverters:信息转换器,处理HTTP请求/响应体与Java对象之间的转换
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转换
    }
}

拦截器

  • 目的:在控制器接收请求前,拦截请求,先进行指定逻辑语句

  • 替代方案

    1. 过滤器:在请求到达servlet前就拦截请求
    2. AOP:指定控制器方法前执行指定语句
    3. 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; // 终止后续处理
    }
}

跨域处理

  • 描述:前端访问后端时,因为同源规则导致后端禁止前端访问

  • 原理

    1. 同源指的是:用户访问前端的请求 和 前端发送给后端的请求 之间的域名、端口、协议必须一样
    2. 跨域访问本质上不是错误,而是浏览器的限制行为(例如React访问Tomcat,但二者部署在不同的IP上)
  • 解决方法

    1. 前端直接设位置服务器代理
    2. 前端由Nginx托管,一定是同源的
    3. 服务/网关设置跨域

全局统一配置(推荐)

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项目的全局异常处理机制的兜底

  • 注解说明

    1. @ControllerAdvice:指定异常扫描路径
    2. @ExceptionHandler:用于指定异常类型
    3. @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);
        }
    }
}

数据校验

  • 目的:校验入参的数据格式是否正确

  • 原理

    1. @Valid 通过 ModelAttributeMethodProcessorRequestResponseBodyMethodProcessor 直接调用验证器完成校验
    2. @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>

校验规则注解

  • 空值检查

    1. @NotNull:元素不能为 null
    2. @NotEmpty:元素不能为 null 且不能为空(适用于字符串、集合、数组、Map)
    3. @NotBlank:字符串不能为 null,且去除首尾空格后不能为空(仅适用于字符串)
  • 数值范围检查

    1. @Min(value):数值必须大于或等于 value
    2. @Max(value):数值必须小于或等于 value
    3. @DecimalMin(value):数值(包括小数)必须大于或等于 value
    4. @DecimalMax(value):数值(包括小数)必须小于或等于 value
    5. @Positive:数值必须是正数
    6. @PositiveOrZero:数值必须是正数或零
    7. @Negative:数值必须是负数
    8. @NegativeOrZero:数值必须是负数或零
  • 字符串检查

    1. @Size(min, max):字符串、集合、数组的大小必须在 minmax 之间
    2. @Length(min, max):字符串的长度必须在 minmax 之间(功能与 @Size 类似,@Size 更通用)
    3. @Pattern(regexp):字符串必须匹配指定的正则表达式
  • 日期检查

    1. @Past:日期必须是过去的时间
    2. @PastOrPresent:日期必须是过去的时间或当前时间
    3. @Future:日期必须是未来的时间
    4. @FutureOrPresent:日期必须是未来的时间或当前时间
  • 其他检查

    1. @Email:字符串必须是一个合法的电子邮件地址
    2. @AssertTrue:布尔值必须为 true
    3. @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原理

    1. @Async 标记的方法通过AOP提交到配置的 ThreadPoolTaskExecutor线程池,异步执行
    2. 方法可以返回 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提供的核心线程池,用于实现异步任务,默认会自动配置

    1. execute(Runnable task):不阻塞主线程执行,不可接收返回值
    2. submit(Runnable task):不阻塞主线程执行,可接收Future返回值
    3. 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("异步任务");
    }
}

异常处理机制

  • 异步方法执行后,调用方无法感知异常

    1. 可以使用AsyncConfigurer配置统一异常处理
    2. 可以将异步任务返回CompletableFuture,响应式处理
    3. 实际上,异步任务中就应该处理异常,而不是抛出
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任务调度器异步执行,不会阻塞主线程

  • 应用场景

    1. 用户下单后30分钟未付款,每隔固定时间(如 1 分钟)执行一次扫描,取消超时订单
    2. 保证数据安全性,可以定时对数据库中的数据进行备份
    3. 保证应用正常运行,可以定时清理过期数据以节省性能
    4. 利用定时任务实现定时通知的功能
  • 替代方案

    1. Quartz
    2. 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执行时间计算 - 在线工具

    1. zone参数:时区,例如指定zone = "Asia/Shanghai",默认使用JVM 默认时区,JVM未指定时取系统时区
    2. cron参数:整数类型,可以省略年字段,可以使用通配符
    3. *:表示所有值,表示 "每单位"(如每分钟、每小时等),例如在秒字段上*表示每秒执行一次
    4. ?:任意值,即定时任务不关心此字段,用于解决冲突,例如星期和日可能冲突,此时在星期上加?
    5. -:区间,例如在小时字段上5-7表示5、6、7点触发
    6. /:递增触发,例如在秒字段上5/10表示从第5秒开始每隔10秒触发一次
    7. #:序号,例如在周字段上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());
    }
}

异常处理机制

  • 定时任务本质是异步任务

    1. 可以使用SchedulingConfigurer配置统一异常处理
    2. 可以将定时任务以异步任务形式处理,套用异步任务的处理逻辑
    3. 实际上,异步任务中就应该处理异常,而不是抛出
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接口的实现类管理持久层事务

    1. JDBC事务:通过 DataSourceTransactionManager 管理 JDBC、MyBatis 连接的JDBC事务
    2. JPA 事务:通过 JpaTransactionManager 管理 Hibernate、EclipseLink、OpenJPA 连接的JDBC事务
    3. JTA事务:通过JtaTransactionManager管理Atomikos、Narayana连接的分布式事务
  • 事务的隔离级别

    1. DEFAULT(默认):使用连接数据库的默认隔离级别(例如MySQL是可重复度,Oracle是读已提交)
    2. READ_UNCOMMITTED:读未提交,可能导致脏读
    3. READ_COMMITTED:读已提交
    4. REPEATABLE_READ:可重复读
    5. SERIALIZABLE:最高的隔离级别,完全锁定事务访问的数据,避免脏读、不可重复读和幻读,但性能较差
  • 事务的传播行为

    1. REQUIRED(0,默认):外层回滚时,内层全部回滚
    2. REQUIRES_NEW(1):内层事务和外层事务完全独立
    3. NESTED(6):内层回滚到保存点,不影响外层
    4. SUPPORTS(2):遵循外层事务的回滚行为
    5. NOT_SUPPORTED(3):当前事务不参与回滚
    6. MANDATORY(4):必须在外层事务中调用,否则直接失败
    7. 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:用于方法上(推荐)或者类上

  • 声明事务的失效情景

    1. 声明式事务中有 try-catch 异常后,如果需要回滚事务,一定要注意手动回滚事务
    2. AOP失效场景(例如未开启注解扫描、静态私有方法不能加事务、方法自调用场景)
    3. 事务传播级别、事务级别设置错误
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的封装,简化了事务流程

    1. TransactionTemplate默认会自动注入事务管理器,如果项目中引入了JTA,会优先注入为JTA类型,此时就需要显式注入
    2. 如果项目有多个数据源或事务管理器,也需要显式配置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模型实现框架,用于解决多数据源产生的分布式事务

    1. 强一致性,适用于金融领域
    2. 本地事务会全程锁定,不适合远程调用造成的分布式事务场景
  • 引入依赖

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的封装,简化了事务流程

    1. TransactionTemplate默认会自动注入事务管理器,如果项目中引入了JTA、JDBC等多种事务,此时就需要显式注入
    2. 如果项目有多个事务管理器,也需要显式配置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 的封装和增强

    1. ThreadPoolExecutor需要手动创建和关闭,ThreadPoolTaskExecutor可以加入IOC管理生命周期**
    2. ThreadPoolExecutor构造函数参数多,配置繁琐,ThreadPoolTaskExecutor提供 setter 方法,支持链式调用
    3. ThreadPoolExecutor仅支持 Runnable/CallableThreadPoolTaskExecutor还支持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 的扩展,它允许你注册回调函数,在任务完成时自动触发,而无需阻塞等待结果

    1. JDK8之前还没有提供CompletableFuture ,因此使用Spring提供的ListenableFuture
    2. JDK8+推荐直接使用CompletableFuture
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

    1. 代码瘦身
    2. 使用工具类有助于避免常见的异常场景,例如避免空指针异常
工具类 核心功能 常用方法示例 使用场景
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() 动态调用方法、反射场景

字符串工具类

  • StringUtils

    1. hasText():判断字符串所有空白符后是否有实际内容,取代原来的isEmpty()方法
    2. trimWhitespace():去除首尾空白(支持更广泛的 Unicode 空白符)
    3. collectionToDelimitedString():将集合(如 ListSet 等)中的元素拼接成一个字符串,并用指定的分隔符连接
    4. 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"

集合与数组工具类

  • CollectionUtils

    1. isEmpty():判断集合/数组是否为空
    2. intersection():求交集
    3. mergeArrayIntoCollection():合并数组到集合
    4. firstElement():获取第一个元素
    5. 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"]

对象操作工具类

  • ObjectUtils

    1. nullSafeEquals():安全比较对象
    2. nullSafeToString():安全转字符串
    3. firstNonNull():返回第一个非空对象
    4. isEmpty():判空
java 复制代码
javaboolean equal = ObjectUtils.nullSafeEquals("abc", "abc"); // true
String safeStr = ObjectUtils.nullSafeToString(null); // "null"
  • Assert:参数校验,校验失败时抛出IllegalArgumentException

    1. notNull():非空校验
    2. isTrue():条件校验
    3. 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对观察者模式的功能封装,即当一个对象的状态发生改变时,所有依赖于它的对象都会实时更新并执行指定通知

  • 应用场景

    1. ContextStartedEventListener:监听ContextStartedEvent,在应用上下文启动时执行逻辑(如初始化资源)
    2. ContextRefreshedEventListener:监听ContextRefreshedEvent,在应用刷新时触发
    3. ContextClosedEventListener:监听ContextClosedEvent,在上下文关闭时清理资源
    4. ContextLoaderListener:是Spring Web的入口,负责初始化Web应用上下文
    5. 业务事件通知:相当于实现一个轻量级的、进程内的消息队列**
    6. 监听外部系统事件(如消息队列、数据库变更),一般三方系统都集成了监听功能
  • 监听事件:触发监听器执行的对象

    1. ContextRefreshedEvent:ApplicationContext初始化或刷新时初始化资源
    2. ContextStartedEvent:ApplicationContext启动服务
    3. ContextClosedEvent:ApplicationContext关闭时资源释放
    4. ContextStoppedEvent:ApplicationContext停止时暂停服务
    5. RequestHandledEvent:HTTP请求处理完成时处理请求日志
    6. 自定义事件:继承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());
    }
}

属性引用

  • 目的:可快速访问其他文件中的数据

    1. 动态属性注入:在Spring配置中动态设置属性值(如@Value注入动态值)
    2. 条件化配置:根据表达式结果决定是否启用某个Bean或配置
    3. 数据绑定与验证:在表单验证、数据转换时动态计算值
    4. AOP与安全控制:在切面逻辑或权限表达式中使用动态条件
    5. 集成测试:在测试中模拟或验证动态行为

属性占位符

  • 属性占位符:使用${}
  • 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

  • 版本要求

    1. SpringBoot稳定版本2.x,目前主流版本正在转向SpringBoot3.x
    2. Java稳定版本JDK8,目前主流版本正在转向JDK17
    3. 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>

启动流程

  • 大致流程

    1. 运行启动类:创建 SpringApplication 实例
    2. 加载 ApplicationListener 实例:监听应用生命周期的所有事件
    3. 加载环境:读取配置文件(本地+远程)、命令行参数、系统属性等
    4. 创建应用上下文:创建并配置 ApplicationContext
    5. Bean 初始化:加载IOC,执行依赖注入(DI)
    6. 启动内嵌服务器:默认Tomcat
    7. 回调执行:执行 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>

自动配置机制

  • 场景启动器:场景启动器包含了功能需要的所有依赖

    1. Spring-boot-starter-xxx是Spring官方提供的场景启动器;xxx-Spring-boot-starter是第三方提供的场景启动器
    2. 官方场景启动器会自动管理版本,三方场景启动器需要手动指定版本
    3. Spring会在启动时自动注册场景启动器的相关Bean,属性取默认值,可以后续修改\
  • 自动配置流程

    1. 启动 Spring Boot 应用:通过 @SpringBootApplication(scanBasePackages = {"com.wyh"})启动类启动
    2. 扫描自动配置类: 加载所有自动配置类
    3. 条件化匹配:根据当前项目的依赖和配置,决定哪些自动配置类生效
    4. 创建 Bean 并绑定属性:为生效的自动配置类创建 Bean,并绑定 application.yml 中的配置(如果有)
    5. 注入到 Spring 容器:将配置好的 Bean 注册到 IoC 容器中
  • 全局配置文件和配置类

    1. 如果场景启动器相关Bean如果修改属性过程较复杂,建议使用配置类(例如配置rabbitMQ
    2. 非场景启动器相关Bean,SpringBoot不会自动注册,必须使用配置类注册(例如配置redisson
    3. 简单场景或者静态属性可以使用配置文件
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

    1. @ConditionalOnClass:如果项目中存在这个类,则触发行为
    2. @ConditionalOnMissingClass:如果项目中不存在这个类,则触发指定行为
    3. @ConditionalOnBean:如果IOC容器中存在组件,则触发指定行为
    4. @ConditionalOnMissingBean:如果IOC容器中不存在组件,则触发指定行为
    5. @ConditionalOnProperty:如果配置文件中有对应的值才执行
  • @ConfigurationProperties:由统一配置文件信息注入Bean,较于@Value,避免了繁杂的配置文件,可以集中快速管理

    1. 配置类必须有gettersetter
    2. @ConfigurationProperties(prefix = "xxx")选择配置类中的键,支持多层级匹配
    3. 支持松散匹配(databaseUrldatabase-url),但建议使用严格匹配
    4. 非配置类中加载也可以加在属性值,在类上加@EnableConfigurationProperties即可
    5. @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文件中指定配置文件

    1. application-dev.yml:开发环境
    2. application-test.yml:测试环境
    3. 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加载条件

    1. 逻辑或表达式:@Profile({"dev", "test"})@Profile("dev | test")(Spring 6+ 支持)
    2. 逻辑与表达式:@Profile("dev & test")
    3. 否定表达式:@Profile("!prod")
    4. 复杂表达式:@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);
    }
}
相关推荐
V***u4531 小时前
【学术会议论文投稿】Spring Boot实战:零基础打造你的Web应用新纪元
前端·spring boot·后端
喵叔哟1 小时前
6.配置管理详解
后端·python·flask
yuuki2332331 小时前
【C++】类和对象(上)
c++·后端·算法
曾经的三心草1 小时前
基于正倒排索引的Java文档搜索引擎3-实现Index类-实现搜索模块-实现DocSearcher类
java·python·搜索引擎
韩数1 小时前
小白也能看懂! 今年爆火的 MCP 协议究竟是什么?写给普通人的 MCP 指南
后端·aigc·mcp
l***46681 小时前
SSM与Springboot是什么关系? -----区别与联系
java·spring boot·后端
稚辉君.MCA_P8_Java1 小时前
Gemini永久会员 快速排序(Quick Sort) 基于分治思想的高效排序算法
java·linux·数据结构·spring·排序算法
I***t7161 小时前
【MyBatis】spring整合mybatis教程(详细易懂)
java·spring·mybatis
YA3331 小时前
mcp-grafana mcp 使用stdio报错
java·开发语言