Springframework

  • 应用场景:Spring是一个开源设计层面框架,实现了业务逻辑层和其他层的解耦,它的核心思想是面向接口编程
模块核心功能典型场景
Core ContainerIOC/DI、Bean 管理基础对象创建
Data AccessJDBC、ORM、事务数据库操作
WebMVC、WebFluxWeb 应用开发
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 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 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:导入其他配置类
@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("包路径")开启注解扫描
@SpringBootApplication(scanBasePackages = {"com.wyh"}) // 设置注解扫描
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Service
//@Component("userService") // 可以手动指定Bean名称,默认Bean名称是类名首字母小写
public class UserService implements IUserService {
//TODO
}

Bean注入(DI)

  • 描述:根据类型或者名称从IOC容器中取出对应Bean实例

  • 注入方法应用场景

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

手动加载

  • ClassPathXmlApplicationContext:获取xml定义的Bean
  • AnnotationConfigApplicationContext:获取注解/配置类定义的Bean
// 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名称匹配
@Service
class UserService {
@Autowired // 基于类型注入
private UserServiceImpl1 userServiceImpl1;
@Resource // 基于名称注入
private UserServiceImpl2 userServiceImpl2;
@Autowired
@Qualifier("impl2") // 存在同类型Bean,再使用名称匹配
private UserService userService;
}

构造器注入

  • 循环依赖:构造器注入可以防止自调用的循环依赖问题
  • 数据安全:构造器注入的成员是静态属性,不会被篡改
@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. 原理:通过 AopContext.currentProxy() 获取当前代理对象,从而在内部调用时绕过 this 引用,直接调用代理对象的方法
    2. 实现方法:@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)

开启AOP

  • Spring3.x
<!-- 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>
<!-- 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 功能
<!-- Spring Boot Starter AOP(核心依赖) -->
  <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
@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

  • 选择要增强的核心业务类
@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
@Component
@Aspect
@Slf4j
public class UserServiceAspect {
// 定义切入点:匹配 UserService所有public方法,*是单通配符,..是多通配符
@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(); //获取方法
DataSource dataSource = method.getAnnotation(DataSource.class); // 获取方法上的 @DataSource 注解
}
// 返回通知
@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自动生效
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

自定义组合注解

  • @Aspect + @Component是固定格式,可以自定义组合注解
/**
* 自定义组合注解:替代 @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 "";
}

MVC

  • Spring MVC 支持多种网络传输,其中处理 HTTP 请求是核心功能,也兼容其他连接类型(例如Websocket)

    1. MVC通过封装原生Servlet对HTTP请求的API,极大地简化了控制器层的开发
    2. 每一个HTTP请求由Servlet 容器的线程池为其分配一个线程,请求结束后线程自动回收
    3. HTTP和TCP/IP协议的区别:TPC/IP协议是传输层协议,HTTP协议是应用层协议,我们主要关注HTTP协议下的请求处理
  • SpringBoot引入web启动器即可

<!--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,其他常见的有文件类型等

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重定向目标 URLLocation: https://example.com/new-path
    Header自定义响应头X-Version:1.0
  • 响应体:数据类型和字符集由响应头Content-Type指定,一般响应体中的数据类型是json,文件类型见下方文件传输

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快速开发
@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,这样可以快速区分业务场景异常和服务器异常
@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反序列化,默认相同名称映射,也可以自定义映射规则
@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特殊场景,请求体是文件数据即文件上传功能,响应体是文件数据即文件下载功能
# SpringBoot默认已配置MultipartResolver的Bean,可以使用统一配置文件或者配置类修改属性
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
file-size-threshold: 1MB
enabled: true
location: /tmp/uploads
//也可以使用配置类,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;
}
}

文件上传

  • 前端一般通过表单传输文件,一个键代表一种类型的文件,可以是一个,也可以是多个文件
<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指定文件(组)
@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);
}
}
}

文件下载

@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对象之间的转换
@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:更复杂的权限控制
//拦截器
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
}
}
@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);
}
}
  • 举例:动态参数的路径跳转
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. 服务/网关设置跨域

全局统一配置(推荐)

@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)的缓存时间(秒)
}
}

局部跨域设置

//作用于控制器方法
@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
@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
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version> <!-- 或兼容版本 -->
</dependency>
<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
<!-- 引入依赖后会自动注册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
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
@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,才能生效对应的校验功能
@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;
}
}
@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支持分组校验功能(例如添加功能和更新功能对参数的校验不同)
  • 定义分组接口(空接口),仅用于标识校验规则的适用场景,不建议使用已有类直接耦合
public interface CreateGroup {}
public interface UpdateGroup {}
  • 在实体类中指定分组
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 中指定分组
@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
<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
@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():优雅关闭线程池,停止接受新任务,但会执行完已提交的任务
@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 指定线程池
@Service
public class MyService {
@Async("customTaskExecutor") // 指定使用线程池
public void asyncMethodWithConfiguredExecutor() {
...
}
}

使用异步任务

  • 原理:调用带有 @Async 的方法时,由AOP拦截调用,并提交到线程池异步执行
  • 同类中 @Async 方法调用另一个 @Async 方法时,可能会失效,因为是AOP自调用失效
@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. 实际上,异步任务中就应该处理异常,而不是抛出
@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());
};
}
}
@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
<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
@EnableScheduling // 开启定时任务
@SpringBootApplication(scanBasePackages = {"com.wyh"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
  • 配置ThreadPoolTaskScheduler:既是定时任务的调度器,又是执行器,用于实现定时任务,默认会自动配置
@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指定调度器
@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 :在定时规则基础上,指定任务第一延迟执行(毫秒)

// @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开启多线程
@Component
public class CombinedTask {
@Async
@Scheduled(fixedRate = 1000)
public void run() {
System.out.println("Running in thread: " + Thread.currentThread().getName());
}
}

异常处理机制

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

    1. 可以使用SchedulingConfigurer配置统一异常处理
    2. 可以将定时任务以异步任务形式处理,套用异步任务的处理逻辑
    3. 实际上,异步任务中就应该处理异常,而不是抛出
@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());
}
}
// 如果需要自定义更复杂的异常处理逻辑,可以实现 ErrorHandler 接口
@Slf4j
public class CustomSchedulerErrorHandler implements ErrorHandler {
@Override
public void handleError(Throwable t) {
// 1. 记录详细异常日志
log.error("定时任务执行异常", t);
// 2. 发送告警通知
sendAlert("定时任务异常", t.getMessage());
// 3. 其他自定义处理逻辑
}
}
@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本地事务的解决方案
  • 引入依赖
<!-- 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,管理单个数据库的事务(基于数据库本地事务)
@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. 事务传播级别、事务级别设置错误
@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 实现事务管理
@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,并手动注入
@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. 本地事务会全程锁定,不适合远程调用造成的分布式事务场景
  • 引入依赖

<!--------------------------- 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事务管理器
@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数据源
@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);
}
}

声明式事务

@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事务
@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,并手动注入
@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接口设置事务隔离级别
@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;
}
}
}

日志框架

  • 目的

    1. 问题排查与调试
    2. 性能监控与优化
    3. 运维监控

SLF4J门面

  • 描述:【强制】开发者必须使用日志框架SLF4J 中的 通用API,如果更换具体日志类型只需要更换依赖即可,无需更改代码

  • 引入依赖

<!--------------------------- Spring3.x ----------------------------->
  <dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>3.2.18.RELEASE</version> <!-- 示例版本 -->
    <exclusions>
      <!-- 排除 commons-logging -->
        <exclusion>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <!-- JCL 到 SLF4J 的桥接(替代 commons-logging) -->
      <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>1.7.32</version>
      </dependency>
      <!-- Logback 核心实现,包含了slf4j-api -->
        <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.11</version>
        </dependency>
        <!--------------------------- SpringBoot ----------------------------->
          <!-- spring-boot-starter-web自带配置了SLF4J和logback -->
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
  • Spring3.x配置
<?xml version="1.0" encoding="UTF-8"?>
  <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-->
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/applicationContext.xml</param-value>
    </context-param>
    <!-- 显示指定配置文件 -->
      <context-param>
      <param-name>logbackConfigLocation</param-name>
      <param-value>classpath:logback.xml</param-value>
      </context-param>
      <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
      <listener>
      <listener-class>ch.qos.logback.classic.servlet.LogbackServletContextListener</listener-class>
      </listener>
      ...
    </web-app>
  • SpringBoot配置:SpringBoot中的logbck配置文件命名要求如下(优先级自上而下)

    1. application.yaml中的日志配置
    2. logback-spring.groovylog4j-spring.groovylog4j2-spring.groovy
    3. logback-spring.xmllog4j-spring.xmllog4j2-spring.xml
    4. logback.xmllog4j.xmllog4j2.xml
    5. logback.groovylog4j.groovylog4j2.groovy
logging:
config: classpath:logback-spring.xml # 指定配置文件,不指定SpringBoot也会自动查找日志配置文件
#  level:
#    com.wyh.*: warn # com.wyh包下所有类只打印warn等级以上的日志
#    com.wyh.UserUtils: info # 如果有冲突,以越详细的路径下的日志等级为准
#  file:
#    name: log/myLog.log # 在项目跟目录上log/myLog中生成日志,如果要使用绝对路径可以用logging.file.path

打印日志

  • SLF4J提供了日志工厂,用于获取日志对象

    1. lombok封装了获取Logger对象的过程:在类上加注解@SLF4J,自动获取对象log
    2. 如果使用配置文件中<logger>配置的对象,就需要显式创建Logger:LoggerFactory.getLogger("logName")
    3. 原生获取Logger:Logger logger = LoggerFactory.getLogger(UserService.clss);
  • 【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式

@Slf4j // 推荐使用lombok,自动创建实例log
public class UserService {
public void test(){
if (log.isDebugEnabled()) { // 日志级别的开关判断
log.debug("姓名: {},年龄: {}", "wyh", 25); // 字符串变量之间的拼接使用占位符的方式
}
}
}

日志配置文件

  • 日志级别说明

    1. trace —> debug(开发环境) —> info —> warn(生产环境) —> error
    2. 【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断
    3. logback是独立的日志框架,Spring 不参与 Logback 配置加载,在src/main/resources下创建logback.xml即可
  • 常见功能

    1. 控制输出内容的格式
    2. 按时间格式迭代生成日志文件,例如每日生成一个日志文件
    3. 控制日志输出的级别,不同对象打印不用等级日志
    4. 实时监控日志,例如出现error信息就发邮件预警
    5. 异步生成日志,例如业务较为复杂时异步执行
    6. 自动删除过期日志,例如自动删除一年前的日志
<configuration debug="true"> <!--debug="true"表示启动应用后,控制台会输出初始化日志,检查是否加载了你的配置文件-->
  <!--logback-spring.xml-->
    <property name="LOG_PATH" value="./logs" />
    <!-- 定义Appender -->
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
        <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
      </appender>
      <!-- RollingFileAppender表示开启日志滚动功能 -->
          <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
          <!--文件路径-->
          <file>${LOG_PATH}/app.log</file>
            <!-- 输出格式: %d{...}-时间 [%thread]-线程  %-5level-等级 %logger{36}-打印类名 %msg%n-匹配开发者输入内容 -->
              <encoder>
              <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
              </encoder>
              <!--按天滚动-->
                  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>${DEV_LOG_DIR}/${APP_NAME}-dev-%d{yyyy-MM-dd}.log</fileNamePattern>
                <maxHistory>7</maxHistory>
                <cleanHistoryOnStart>false</cleanHistoryOnStart> <!--不管理启动前的历史日志-->
                </rollingPolicy>
                <!--日志等级过滤-->
                    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                  <level>INFO</level>
                  </filter>
                </appender>
                <!-- FileAppender表示不支持滚动,即将日志全部放在一个文件中 -->
                    <appender name="MYSTYLE" class="ch.qos.logback.core.FileAppender">
                  <file>${LOG_PATH}/dao.log</file>
                    <encoder>
                    <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
                    </encoder>
                  </appender>
                  <!-- 根日志配置,配置logger以外的类 -->
                      <root level="WARN"> <!-- 只打印WARN以上级别的日志,默认所有包下都打印日志 -->
                      <!-- 多个appender表示日志会重复输出 -->
                        <appender-ref ref="FILE" /> <!-- 使用上述FILE-appender格式输出 -->
                        <appender-ref ref="CONSOLE" /> <!-- 使用上述CONSOLE-appender格式输出 -->
                      </root>
                      <!-- logger可以覆盖root做更精细化配置,name表示指定要匹配的日志记录器(Logger)的名称,additivity防止日志传递 -->
                          <logger name="com.wyh.service" level="WARN" additivity="false">
                          <appender-ref ref="MYSTYLE" /> <!-- 使用上述MYSTYLE-appender格式输出 -->
                        </logger>
                      </configuration>

企业级日志

  • 每天生成一个文件,以日期格式命名
  • 面对开发者打印INFO级别,面对运维者打印WARN级别,控制台打印DEBUG级别
  • 日志保存15天
  • 运维文件日志异步生成
  • 监控日志,发生ERROR警告就异步发送邮件通知
./logs/
   ├── dev/
   │   ├── app-dev.log          # 当前开发日志文件
   │   └── app-dev-2023-10-01.log # 历史开发日志
   └── ops/
       ├── app-ops.log         # 当前运维日志文件
       └── app-ops-2023-10-01.log # 历史运维日志
<!--邮件发送依赖-->
  <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mail</artifactId>
  </dependency>
<!--logback-spring.xml-->
  <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
      <property name="LOG_DIR" value="./logs" /> <!-- 定义日志目录 -->
      <property name="DEV_LOG_DIR" value="${LOG_DIR}/dev" />  <!-- 开发日志目录 -->
      <property name="OPS_LOG_DIR" value="${LOG_DIR}/ops" />  <!-- 运维日志目录 -->
      <!-- 开发日志 Appender(按天滚动,INFO 级别) -->
          <appender name="FILE_DEV" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${DEV_LOG_DIR}/dev.log</file>
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
          <level>INFO</level>
          </filter>
          <!--按天滚动-->
              <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${DEV_LOG_DIR}/%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
            <cleanHistoryOnStart>false</cleanHistoryOnStart> <!--不管理启动前的历史日志-->
            </rollingPolicy>
            <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
          </appender>
          <!--  运维日志 Appender(异步,按天滚动) -->
              <appender name="ASYNC_FILE_OPS" class="ch.qos.logback.classic.AsyncAppender">
            <queueSize>512</queueSize>
            <discardingThreshold>10</discardingThreshold> <!-- 缓存10条日志才持久化 -->
            <includeCallerData>false</includeCallerData>
            <neverBlock>false</neverBlock>
              <!--按天滚动-->
                  <appender class="ch.qos.logback.core.rolling.RollingFileAppender">
                <file>${OPS_LOG_DIR}/ops.log</file>
                    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                  <level>WARN</level>
                  </filter>
                    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                  <fileNamePattern>${OPS_LOG_DIR}/%d{yyyy-MM-dd}.log</fileNamePattern>
                  <maxHistory>7</maxHistory>
                  <cleanHistoryOnStart>false</cleanHistoryOnStart>
                  </rollingPolicy>
                  <encoder>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                  </encoder>
                </appender>
              </appender>
              <!-- 控制台输出 -->
                  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
                  <encoder>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                  <charset>UTF-8</charset>
                  </encoder>
                </appender>
                <!-- 异步邮件 Appender(包装 SMTPAppender) -->
                    <appender name="ASYNC_EMAIL" class="ch.qos.logback.classic.AsyncAppender">
                  <queueSize>256</queueSize> <!-- 队列大小 -->
                  <discardingThreshold>0</discardingThreshold> <!-- 队列剩余 0 时丢弃低级别日志 -->
                  <neverBlock>true</neverBlock> <!-- 队列满时丢弃新日志(避免阻塞) -->
                  <includeCallerData>false</includeCallerData> <!-- 禁用调用方信息(提升性能) -->
                      <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
                      <!-- SMTP 服务器配置 -->
                      <smtpHost>smtp.example.com</smtpHost> <!--例如126邮箱:smtp.126.com-->
                      <smtpPort>465</smtpPort> <!--例如465(SSL)或 587(TLS)-->
                      <username>your-email@example.com</username> <!--例如wyh_cn@126.com-->
                      <password>your-email-password</password> <!--这里要使用授权码-->
                      <SSL>true</SSL> <!-- 启用 SSL -->
                      <STARTTLS>true</STARTTLS> <!-- 启用 TLS -->
                        <!-- 邮件收发配置 -->
                        <from>your-email@example.com</from>
                        <to>admin@example.com</to>
                        <subject>ERROR: %logger{20} - %m</subject>
                          <!-- 触发邮件的条件(ERROR 级别) -->
                              <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                            <level>ERROR</level>
                            </filter>
                            <!-- 日志格式 -->
                                <layout class="ch.qos.logback.classic.PatternLayout">
                              <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
                              </layout>
                              <!-- 可选:缓冲日志,避免频繁发送邮件 -->
                                  <cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTracker">
                                <bufferSize>10</bufferSize> <!-- 缓存 10 条日志后发送 -->
                                </cyclicBufferTracker>
                              </appender>
                            </appender>
                            <!-- 根日志 -->
                                <root level="INFO">
                                <appender-ref ref="ASYNC_EMAIL" />
                                <appender-ref ref="ASYNC_FILE_OPS" />
                                <appender-ref ref="FILE_DEV" />
                              </root>
                            </configuration>

Spring Retry

  • 描述:分布式系统中,网络抖动、服务短暂不可用等场景下,重试机制是推荐的解决方案,Spring提供了功能更强大的重试机制

开启重试机制

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

声明式重试

  • 开启注解功能
@SpringBootApplication(scanBasePackages = {"com.wyh"})
@ComponentScan(basePackages = "包路径") // 设置注解扫描
@EnableRetry // 开启重试功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
  • @Retryable:标记需要重试的方法
@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:定义重试失败后的回调方法
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@Retryable(
value = {Exception.class},
exceptionExpression = "#{#root.message.contains('temporary')}" // 仅当异常消息包含"temporary"时重试
)
public void sensitiveOperation() {
// ...
}

编程式重试

  • 配置重试策略
@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;
}
}
  • 编程式使用
@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连接重试机制
@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
@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会自动管理
@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
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,允许你在任务执行前后对任务进行装饰,例如添加日志、传递上下文等
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():字符串分割为数组
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():判断是否包含任意指定元素
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()方法
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():判空
javaboolean equal = ObjectUtils.nullSafeEquals("abc", "abc"); // true
String safeStr = ObjectUtils.nullSafeToString(null); // "null"
  • Assert:参数校验,校验失败时抛出IllegalArgumentException

    1. notNull():非空校验
    2. isTrue():条件校验
    3. hasText():字符串内容校验
javaAssert.notNull(userId, "用户ID不能为空");
Assert.isTrue(age > 0, "年龄必须大于0");

IO流工具类

  • FileCopyUtils:简化文件复制、流操作,自动处理资源关闭
java// 文件复制
FileCopyUtils.copy(sourceFile, targetFile);
// 流到字符串
String content = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
  • StreamUtils:流操作封装,支持copyToString()copyToByteArray()
byte[] data = StreamUtils.copyToByteArray(inputStream);

反射工具类

  • ReflectionUtils:简化反射操作,自动处理权限问题(如setAccessible(true)
javaMethod method = ReflectionUtils.findMethod(User.class, "getName");
Object result = ReflectionUtils.invokeMethod(method, user);

其他实用工具类

  • DigestUtils:提供MD5、SHA-1等哈希算法,用于密码加密或数据校验
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
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注解
@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接口
@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>,用于加载配置文件
...
<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引入
@Value("${app.name}") // 从Application.yaml中引用属性
private String appName;

SpEL表达式

  • SpEL表达式:使用#{},用于其他 Bean 的属性、执行逻辑运算、调用方法
@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 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**

/**
* 任务简单且无需容器参数解析场景下,推荐使用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));
}
}
/**
* 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官方并未对其版本管理,建议在搭建项目前查询官网适配版本
<!--父项目进行版本管理-->
  <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. 简单场景或者静态属性可以使用配置文件
@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);
}
}
@Configuration
public class RabbitMQConfig {
//rabbitMQ虽然有场景启动器,但初始化配置过程很复杂,不适合放在全局文件中
@Bean
public ConnectionFactory rabbitConnectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
...
return factory;
}
}

嵌入式容器

  • SpringBoot引入场景启动器后,会自动内置所需的容器,默认使用Tomcat,可以在全局配置文件中设置Tomcat相关参数
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也可以注解于方法上,映射返回值实例
#application.yaml
db:
druid:
url: jdbc:mysql://localhost:3306/learn_project?serverTimezone=GMT%2B8
username: root
password: 123456
@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:生产环境
# 通过 application.yml 激活
spring:
profiles:
active: dev #使用application-dev.yml    
#application-dev.yml
server:
port: 8080
address: 127.0.0.1
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")
@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生效
spring:
profiles:
active: test # @Profile("test")标注的Bean会生效
  • 测试类的方法或者类上标注@ActiveProfiles("xxx"),也可以使对应@Profile("xxx")的Bean生效
@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();
}
}
@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);
}
}