实用指南:Spring Boot 注解全栈指南:涵盖 Bean 注册、配置加载、请求映射、事务控制、数据校验等一网打尽


1.前言

哈喽大家好~我发现很多同学对注解的使用停留在“照猫画虎”的阶段:知道@Autowired能注入,但说不清它和@Resource的本质区别;知道@Transactional能开事务,但遇到不回滚的情况就抓瞎。

注解的本质是元数据,它是写给框架看的“说明书”。在Spring Boot“约定优于配置”的哲学下,注解是我们与框架沟通的核心语言。掌握不好这门语言,就会导致:

  • 知道@Autowired能注入,但不知道它按类型注入的机制和@Primary的优先级
  • 会写@RequestMapping,但不清楚@GetMapping和它的细微差别
  • 滥用@Transactional,根本不知道事务传播机制和失效场景
  • 参数校验就写一个@NotNull,结果前端传空字符串照样通过

今天我们就来把这些注解彻底吃透。


插播一条消息~

十年经验淬炼 · 系统化AI学习平台推荐

系统化学习AI平台https://www.captainbed.cn/scy/

  • 完整知识体系:从数学基础 → 工业级项目(人脸识别/自动驾驶/GANs),内容由浅入深
  • 实战为王:每小节配套可运行代码案例(提供完整源码)
  • 零基础友好:用生活案例讲解算法,无需担心数学/编程基础

特别适合

  • 想系统补强AI知识的开发者
  • 转型人工智能领域的从业者
  • 需要项目经验的学生

2.1 Spring Boot基础注解

2.1.1 @SpringBootApplication - 一切的开始

这个注解几乎是每个Spring Boot项目的入口,但它远不止"启动类标记"这么简单。

@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

点进去看源码,它是三个注解的组合:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
    // ... 省略属性
}

核心作用分解:

注解

作用

生产级建议

@SpringBootConfiguration

声明这是一个配置类,相当于@Configuration

不要在启动类里写@Bean,保持启动类干净

@EnableAutoConfiguration

自动配置核心,根据classpath自动配置Bean

排除不需要的自动配置(见下方代码)

@ComponentScan

组件扫描,默认扫描当前包及子包

明确指定扫描路径,避免扫到无关包

【生产环境避坑指南】

// 错误示例:启动类乱放
package com.company;  // 放在根包,会导致扫描整个项目所有模块
@SpringBootApplication
public class Application {}
// 正确做法:按模块划分扫描范围
package com.company.userservice;  // 只在当前模块内
@SpringBootApplication(scanBasePackages = "com.company.userservice")
@EnableAutoConfiguration(exclude = {
    DataSourceAutoConfiguration.class,  // 如果没配数据源就排除
    RedisAutoConfiguration.class        // 暂时不用Redis也排除
})
public class UserServiceApplication {
    // 启动类保持干净,不要写业务逻辑
}

原理简析@EnableAutoConfiguration通过spring.factories文件(Spring Boot 2.7+改用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)加载自动配置类,条件注解@ConditionalOnClass判断classpath中是否存在某个类才生效。

常见坑点:启动类不能放在Java默认包下(即没有package声明),否则会导致@ComponentScan扫描整个classpath,启动极慢且可能加载无关Bean。


2.2 Bean相关注解

这一部分是Spring IoC(控制反转)的核心体现。我们如何创建Bean,如何将它们组装在一起?

2.2.1 @Autowired - Spring亲儿子的注入方式

@Service
public class OrderService {
    @Autowired
    private UserService userService;  // 按类型注入
    @Autowired
    private List paymentProcessors;  // 注入所有实现类
}

【核心特性深度解析】

1. 注入机制(按类型)
Spring容器启动时,通过反射找到@Autowired标注的字段/方法,然后根据类型去Bean工厂找唯一的Bean实例。如果找到多个同类型的Bean,就按变量名作为@Qualifier去匹配。

2. 必要性和顺序控制

@Autowired(required = false)  // 找不到Bean也不报错,适合可选依赖
private Optional cacheService;  // Java 8+推荐写法
@Autowired
@Order(1)  // 注入List时排序
private List validators;

3. 构造器注入(Spring官方推荐)

@Service
public class ProductService {
    private final ProductRepository repository;
    private final StockService stockService;
    // Spring 4.3+:单个构造器可省略@Autowired
    public ProductService(ProductRepository repository, StockService stockService) {
        this.repository = repository;
        this.stockService = stockService;
    }
}

【生产级最佳实践】

// 强烈推荐:final + 构造器注入,避免循环依赖和NPE
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)  // Lombok简化
public class OrderService {
    private final UserService userService;  // 必须final
    private final PaymentService paymentService;
    // 方法内使用userService,肯定不为null
}

常见坑点

  • 循环依赖:A依赖B,B依赖A。Spring默认支持单例的setter循环依赖,但构造器循环依赖会直接报错。解决方案:@Lazy延迟注入。
  • 注入null:字段注入在构造函数执行完之前,field是null,如果在构造函数里使用会NPE。
// 错误示范:构造器里用Autowired字段
@Service
public class BadExample {
    @Autowired
    private Dependency dependency;
    public BadExample() {
        dependency.doSomething();  // 这里dependency还是null!
    }
}
// 正确做法:构造器注入
@Service
public class GoodExample {
    private final Dependency dependency;
    public GoodExample(Dependency dependency) {
        this.dependency = dependency;
        dependency.doSomething();  // 安全
    }
}

2.2.2 @Resource - JSR-250的标准选择

@Service
public class UserService {
    @Resource(name = "userCache")  // 按名称注入,找不到再按类型
    private Cache cache;
    @Resource  // 默认按字段名作为Bean名称
    private UserRepository userRepository;
}

与@Autowired的核心区别对比:

对比维度

@Autowired

@Resource

来源

Spring框架

JSR-250标准(Java扩展包)

注入方式

先按类型,再按名称(@Qualifier)

先按名称,再按类型

支持@Primary

✅ 支持

❌ 不支持

支持@Qualifier

✅ 支持

✅ 支持(通过name属性)

可选依赖

required=false

不提供,找不到就报错

性能

略快(Spring原生优化)

稍慢(需解析名称)

【实战选择建议】

// 场景1:多实现类时,@Resource更直观
public interface PayService {}
@Service("alipayService") public class AlipayService implements PayService {}
@Service("wechatPayService") public class WechatPayService implements PayService {}
@Service
public class OrderService {
    @Resource(name = "alipayService")  // 一眼看出用的是哪个实现
    private PayService payService;
    // 等价于@Autowired + @Qualifier("alipayService")
    @Autowired
    @Qualifier("alipayPayService")
    private PayService payService2;
}
// 场景2:Spring项目用@Autowired,非Spring框架用@Resource
// 如果是纯Java EE项目,建议@Resource以保证可移植性

原理简析@ResourceCommonAnnotationBeanPostProcessor处理,它会先解析name属性,通过BeanFactory.getBean(name)查找,找不到再回退到类型匹配。


2.2.3 @Primary - 解决多实现类的首选方案

public interface MessageSender {
    void send(String msg);
}
@Service
@Primary  // 告诉Spring:有多个Bean时,优先选我
public class EmailSender implements MessageSender {
    public void send(String msg) { /* ... */ }
}
@Service
public class SmsSender implements MessageSender {
    public void send(String msg) { /* ... */ }
}
@Service
public class NotificationService {
    @Autowired
    private MessageSender sender;  // 注入的是EmailSender,因为有@Primary
}

【进阶用法】

// 条件化Primary:不同环境用不同实现
@Configuration
public class SenderConfig {
    @Bean
    @Primary
    @Profile("prod")  // 生产环境用邮件
    public MessageSender prodSender() {
        return new EmailSender();
    }
    @Bean
    @Profile("dev")  // 开发环境用控制台
    public MessageSender devSender() {
        return new ConsoleSender();
    }
}

常见坑点@Primary@Qualifier同时存在时,@Qualifier优先级更高。如果@Resource指定了name,@Primary也会失效。


2.2.4 @Scope - 控制Bean的生命周期

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)  // 多例
@Service
public class PrototypeService {
    private final AtomicInteger counter = new AtomicInteger(0);
    public int getCount() {
        return counter.incrementAndGet();
    }
}
@RestController
public class ScopeDemoController {
    @Autowired
    private PrototypeService prototypeService;
    @GetMapping("/demo")
    public String demo() {
        // 注意:这里每次返回都是1!因为Controller是单例,只注入一次
        return "count: " + prototypeService.getCount();
    }
}

【作用域类型全解析】

作用域

说明

适用场景

注意点

singleton

默认,容器中唯一实例

无状态Service、DAO

避免在单例Bean中持有可变状态

prototype

每次注入都创建新实例

有状态Bean、线程不安全类

在单例Bean中注入prototype需要用代理

request

HTTP请求生命周期

Web应用中每个请求一个实例

仅在web环境有效

session

HTTP Session生命周期

用户会话数据

仅在web环境有效,小心内存泄漏

application

ServletContext生命周期

全局共享数据

几乎不用

【生产级解决方案】

// 正确获取prototype Bean的方式:注入ApplicationContext
@Service
public class OrderService {
    @Autowired
    private ApplicationContext context;
    public void processOrder() {
        // 每次调用都获取新的prototype实例
        PrototypeService service = context.getBean(PrototypeService.class);
        service.doWork();
    }
}
// 或使用ObjectFactory(Spring推荐)
@Service
public class OrderService {
    @Autowired
    private ObjectFactory prototypeServiceFactory;
    public void processOrder() {
        PrototypeService service = prototypeServiceFactory.getObject();
        service.doWork();
    }
}

原理简析@ScopeScopedProxyMode控制代理行为。默认NO表示不代理,如果单例Bean依赖多例Bean,需要用ScopedProxyMode.TARGET_CLASS创建CGLIB代理。


2.2.5 组件注册四天王:@Component及衍生注解

// 1. @Component - 通用组件
@Component
public class EmailValidator {
    public boolean isValid(String email) { /* ... */ }
}
// 2. @Repository - DAO层,自动转换异常
@Repository
public class UserRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    // 会将SQLException转为DataAccessException
}
// 3. @Service - 业务层
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}
// 4. @Controller - MVC控制器(返回视图)
@Controller
public class PageController {
    @GetMapping("/index")
    public String index(Model model) {
        return "index";  // 返回页面名称
    }
}
// 5. @RestController = @Controller + @ResponseBody
@RestController
@RequestMapping("/api/users")
public class UserApiController {
    @Autowired
    private UserService userService;
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);  // 直接返回JSON
    }
}

【深度对比表格】

注解

作用

特殊功能

是否被@ComponentScan扫描

@Component

通用组件

@Repository

数据访问层

自动异常转换

@Service

业务逻辑层

无(语义化)

@Controller

MVC控制器

返回视图

@RestController

REST控制器

自动@ResponseBody

【生产级分层建议】

// dao层
@Repository
public class UserDao {
    // 只放SQL/JPQL,不做业务逻辑
}
// service层
@Service
@Transactional(readOnly = true)  // 类级别统一设置
public class UserService {
    @Autowired
    private UserDao userDao;
    @Transactional  // 写操作单独标记
    public void createUser(User user) {
        // 业务逻辑:校验、计算、调用多个DAO
    }
}
// controller层
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    private final UserService userService;
    // 构造器注入
    public UserController(UserService userService) {
        this.userService = userService;
    }
    @PostMapping
    public ResponseEntity create(@RequestBody @Validated UserCreateDTO dto) {
        // 参数组装、调用service、返回结果
        // Controller层要薄,不做复杂业务逻辑
    }
}

原理简析:这些注解都是@Component的元注解,Spring通过ClassPathBeanDefinitionScanner扫描带这些注解的类,生成BeanDefinition注册到容器。@Repository额外注册了PersistenceExceptionTranslationPostProcessor进行异常转换。


2.3 配置注解

2.3.1 @Configuration - 声明配置类

// 错误示范:用@Component声明配置
@Component
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate() {
        // 这样也能用,但不规范,且有代理问题
    }
}
// 正确做法:@Configuration
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // 生产环境必须配置序列化
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.builder(factory)
                .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                        .entryTtl(Duration.ofHours(1)))
                .build();
    }
}

【@Configuration vs @Component 核心区别】

// 测试类
@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService(myDao());  // 第一次调用
    }
    @Bean
    public MyDao myDao() {
        return new MyDao();  // 第二次调用
    }
}
// Spring会代理@Configuration类,myService()里调用的myDao()实际是容器中的单例
// 如果换成@Component,myDao()会被调用两次,产生两个实例!

原理简析@Configuration类会被CGLIB代理,内部方法调用会被拦截,确保@Bean方法返回的是容器管理的单例。而@Component没有代理,方法调用就是普通Java调用。

生产建议:所有配置类都用@Configuration,且配置类中不要依赖注入其他Bean,保持配置纯净。


2.3.2 @Value vs @ConfigurationProperties - 注入配置

@Value:单个值注入

@Component
public class AliyunSmsSender {
    @Value("${aliyun.sms.access-key-id}")  // 直接注入
    private String accessKeyId;
    @Value("${aliyun.sms.timeout:5000}")  // 带默认值
    private int timeout;
    @Value("${server.port}")  // 注入系统配置
    private String port;
    @Value("classpath:certificates/aliyun.pem")  // 注入资源
    private Resource certificate;
    @Value("#{systemProperties['user.dir']}")  // SpEL表达式
    private String projectPath;
}

@ConfigurationProperties:批量绑定配置

# application.yml
app:
  mail:
    host: smtp.gmail.com
    port: 587
    username: admin@example.com
    password: secret
    properties:
      mail.smtp.auth: true
      mail.smtp.starttls.enable: true
  security:
    jwt:
      secret: mySecretKey
      expiration: 86400
@Component
@ConfigurationProperties(prefix = "app")
@Getter @Setter  // Lombok
public class AppProperties {
    private Mail mail;
    private Security security;
    @Getter @Setter
    public static class Mail {
        private String host;
        private int port;
        private String username;
        private String password;
        private Map properties;
    }
    @Getter @Setter
    public static class Security {
        private Jwt jwt;
    }
    @Getter @Setter
    public static class Jwt {
        private String secret;
        private long expiration;
    }
}

【全面对比表格】

特性

@Value

@ConfigurationProperties

适用场景

单个值注入

结构化配置(推荐)

松散绑定

❌ 不支持(必须完全匹配)

✅ 支持(kebab-case/snake_case)

类型安全

❌ 运行时转换失败

✅ 启动时校验

IDE提示

❌ 无

✅ 有(配合spring-boot-configuration-processor)

SpEL表达式

✅ 支持

❌ 不支持

默认值

${key:default}

✅ 在字段初始化时设置

复杂类型

❌ 难(需自定义转换)

✅ 支持Map、List、嵌套对象

【生产级配置规范】

// 1. 添加依赖获取IDE提示

    org.springframework.boot
    spring-boot-configuration-processor
    true

// 2. 配置类单独放config包,加@Validated
@Component
@ConfigurationProperties(prefix = "app")
@Validated  // JSR-303校验
public class AppProperties {
    @NotBlank
    private String name;
    @Min(1024)
    @Max(65535)
    private int port;
    // ...
}
// 3. 启用配置类
@SpringBootApplication
@EnableConfigurationProperties({AppProperties.class})  // 显式注册
public class Application {}

【原理简析】

  • @ValueAutowiredAnnotationBeanPostProcessor处理,支持SpEL表达式解析
  • @ConfigurationPropertiesConfigurationPropertiesBindingPostProcessor处理,通过JavaBeans规范绑定属性,支持复杂类型转换

常见坑点

  1. 配置类忘记加@Component@EnableConfigurationProperties,导致配置不生效
  2. @ConfigurationProperties类中用@Value,两者混用导致混乱
  3. 配置项改名后,启动不报错但值为null,线上爆炸(解决方案:加@Validated

2.3.3 @PropertySource - 加载自定义配置文件

@Configuration
@PropertySource(value = "classpath:custom.properties", encoding = "UTF-8")
public class CustomConfig {
    @Value("${custom.key}")
    private String customKey;
}
// 加载多个文件
@PropertySource({
    "classpath:common.properties",
    "classpath:${spring.profiles.active}/env.properties"
})
// 忽略文件不存在
@PropertySource(value = "optional:classpath:optional.properties", ignoreResourceNotFound = true)

【与@ConfigurationProperties结合】

@Component
@PropertySource("classpath:mail.properties")
@ConfigurationProperties(prefix = "mail")
public class MailProperties {
    private String host;
    private int port;
}

生产建议:Spring Boot推荐用application-{profile}.yml管理环境配置,少用@PropertySource。只有在需要加载非标准位置的配置文件时才使用。


2.4 MVC相关注解

2.4.1 五大请求映射注解

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    @GetMapping("/{id}")  // GET查询
    public User getUser(@PathVariable Long id) { /* ... */ }
    @PostMapping  // POST创建
    public ResponseEntity createUser(@RequestBody @Validated UserDTO dto) { /* ... */ }
    @PutMapping("/{id}")  // PUT全量更新
    public User updateUser(@PathVariable Long id, @RequestBody UserDTO dto) { /* ... */ }
    @PatchMapping("/{id}")  // PATCH部分更新
    public User patchUser(@PathVariable Long id, @RequestBody Map updates) { /* ... */ }
    @DeleteMapping("/{id}")  // DELETE删除
    public ResponseEntity deleteUser(@PathVariable Long id) { /* ... */ }
    // @RequestMapping是它们的老祖宗
    @RequestMapping(value = "/search", method = {RequestMethod.GET, RequestMethod.POST})
    public List searchUsers(@RequestParam String keyword) { /* ... */ }
}

【RESTful API规范建议】

// 完整资源路径,包含版本号
@RequestMapping("/api/v1/users")
// 幂等性设计
@GetMapping    // 查询,安全且幂等
@PutMapping    // 全量更新,幂等
@DeleteMapping // 删除,幂等
@PostMapping   // 创建,非幂等
@PatchMapping  // 部分更新,非幂等
// 状态码返回规范
@PostMapping
public ResponseEntity create(@RequestBody UserDTO dto) {
    User user = userService.create(dto);
    return ResponseEntity
        .status(HttpStatus.CREATED)  // 201
        .header("Location", "/api/v1/users/" + user.getId())
        .body(user);
}

【@RequestMapping高级特性】

//  consumes/produces 限定Content-Type
@PostMapping(
    value = "/upload",
    consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
    produces = MediaType.APPLICATION_JSON_VALUE
)
public Result uploadFile(@RequestParam("file") MultipartFile file) { /* ... */ }
// headers/params 条件匹配
@GetMapping(
    value = "/special",
    headers = "X-API-VERSION=2.0",  // 必须带特定header
    params = "debug=true"           // 必须带debug参数
)
public Result specialEndpoint() { /* ... */ }

原理简析RequestMappingHandlerMapping负责扫描所有@RequestMapping注解,构建HandlerMethod与URL的映射关系,存储在MappingRegistry中。请求到来时,DispatcherServlet通过HandlerMapping找到对应的HandlerExecutionChain


2.4.2 参数绑定三剑客

@PathVariable - 路径变量
@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(
    @PathVariable Long userId,
    @PathVariable("orderId") String orderNumber  // 名称不匹配时指定
) {
    // URL: /users/123/orders/2024001
    // userId=123, orderNumber="2024001"
}
@RequestParam - 查询参数
@GetMapping("/search")
public List searchUsers(
    @RequestParam String keyword,  // 必填
    @RequestParam(required = false, defaultValue = "1") int page,  // 可选+默认值
    @RequestParam Map params  // 接收所有参数
) {
    // URL: /search?keyword=jack&page=2&sort=name
    // keyword="jack", page=2, params包含所有键值对
}
@RequestBody - 请求体(JSON)
@PostMapping
public ResponseEntity create(
    @RequestBody @Validated UserCreateDTO dto,  // 自动JSON反序列化+校验
    @RequestHeader("X-Request-ID") String requestId  // 额外:header获取
) {
    // 请求头:Content-Type: application/json
    // 请求体:{"username":"jack","email":"jack@example.com"}
}

【生产级参数接收最佳实践】

// 一律用DTO接收,不直接暴露实体类
public class UserQueryDTO {
    @NotBlank(message = "关键词不能为空")
    private String keyword;
    @Min(value = 1, message = "页码从1开始")
    private Integer page = 1;
    @Min(value = 1, message = "每页至少1条")
    @Max(value = 100, message = "每页最多100条")
    private Integer size = 10;
}
@GetMapping("/search")
public Result> search(@Validated UserQueryDTO queryDTO) {
    // Spring自动将请求参数绑定到DTO属性
    // URL参数:keyword=jack&page=2&size=20
    return Result.success(userService.search(queryDTO));
}

常见坑点

  1. @RequestBody只能用POST/PUT/PATCH :GET请求没有body,用了会报错HttpMessageNotReadableException
  2. 多个@RequestBody无效:一个方法只能有一个@RequestBody,因为HTTP请求只有一个body
  3. @PathVariable类型转换失败:URL变量类型不匹配会抛TypeMismatchException,要用全局异常处理捕获
  4. @RequestParam接收数组@RequestParam List<Long> ids对应URL:?ids=1&ids=2&ids=3

2.5 参数校验注解

2.5.1 空值检查三兄弟

public class UserDTO {
    @NotNull(message = "用户ID不能为null")      // 不能是null
    private Long id;
    @NotEmpty(message = "用户名不能为空字符串") // 不能是null且length>0
    private String username;
    @NotBlank(message = "手机号不能为空白")     // 不能是null且trim()后length>0
    private String phone;
    private String address;
}

区别对比:

注解

null

"" (空字符串)

" " (空白)

适用类型

@NotNull

❌ 报错

✅ 通过

✅ 通过

任何对象

@NotEmpty

❌ 报错

❌ 报错

✅ 通过

Collection, Map, String

@NotBlank

❌ 报错

❌ 报错

❌ 报错

仅限String

生产建议:String类型统一用@NotBlank,集合用@NotEmpty,对象用@NotNull


2.5.3 细碎但重要的校验注解

public class ProductDTO {
    // 数值范围
    @Min(value = 0, message = "价格不能为负")
    @Max(value = 999999, message = "价格超出上限")
    @Digits(integer = 6, fraction = 2, message = "价格格式错误") // 整数6位,小数2位
    private BigDecimal price;
    // 字符串长度
    @Size(min = 2, max = 20, message = "商品名称长度2-20")
    private String name;
    // 正则表达式
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "编码只能包含字母数字下划线")
    private String code;
    // Email格式
    @Email(message = "邮箱格式不正确")
    private String contactEmail;
    // 日期范围
    @Future(message = "过期日期必须是未来时间")
    private LocalDate expiryDate;
    @PastOrPresent(message = "生产日期不能是将来")
    private LocalDate productionDate;
    // 集合大小
    @Size(min = 1, max = 5, message = "标签数量1-5个")
    private List tags;
    // 布尔值断言
    @AssertTrue(message = "必须同意协议")
    private boolean agreementAccepted;
}

【自定义校验注解】

// 1. 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "手机号格式不正确";
    Class[] groups() default {};
    Class[] payload() default {};
}
// 2. 实现校验器
public class PhoneValidator implements ConstraintValidator {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;  // null由@NotNull处理
        return PHONE_PATTERN.matcher(value).matches();
    }
}
// 3. 使用
public class UserDTO {
    @Phone
    private String phone;
}

2.5.4 @Validated - 开启校验的钥匙

@RestController
@RequestMapping("/api/users")
@Validated  // 在类级别开启方法参数校验
public class UserController {
    // 方法参数校验(路径变量)
    @GetMapping("/{id}")
    public User getUser(@PathVariable @Min(1) Long id) { /* ... */ }
    // 请求体校验
    @PostMapping
    public ResponseEntity create(@RequestBody @Validated UserCreateDTO dto) { /* ... */ }
    // 分组校验:创建和更新用同一DTO但校验规则不同
    @PutMapping("/{id}")
    public User update(@PathVariable Long id,
                      @RequestBody @Validated(UpdateGroup.class) UserDTO dto) { /* ... */ }
}
// Service层也能用
@Service
@Validated
public class UserService {
    public void updateStatus(@Min(1) Long userId, @NotBlank String status) {
        // 参数校验失败会抛ConstraintViolationException
    }
}

原理简析@Validated是Spring对JSR-303的扩展,支持分组校验和方法参数校验。MethodValidationPostProcessor会为@Validated类创建代理,拦截方法调用进行校验。

常见坑点@Validated必须和@RequestBody一起用,否则不生效。Controller类上要加@Validated才能校验路径变量和请求参数。


2.6 全局异常处理

2.6.1 @ControllerAdvice + @ExceptionHandler

// 统一返回值结构
@Data
public class Result {
    private int code;
    private String message;
    private T data;
    public static  Result success(T data) {
        Result result = new Result<>();
        result.setCode(200);
        result.setData(data);
        return result;
    }
    public static  Result error(int code, String message) {
        Result result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
}
// 全局异常处理器
@RestControllerAdvice(basePackages = "com.company.userservice.controller")  // 只处理指定包
public class GlobalExceptionHandler {
    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult()
                .getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, "参数校验失败: " + message);
    }
    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
    // 处理所有未捕获的异常
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
    // 处理404
    @ExceptionHandler(NoHandlerFoundException.class)
    public Result handleNotFound(NoHandlerFoundException e) {
        return Result.error(404, "接口不存在: " + e.getRequestURL());
    }
    // 处理ConstraintViolationException(@Validated方法参数校验)
    @ExceptionHandler(ConstraintViolationException.class)
    public Result handleConstraintViolation(ConstraintViolationException e) {
        String message = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, "参数校验失败: " + message);
    }
}

【进阶技巧】

// 按异常状态码返回不同HTTP状态
@ExceptionHandler(Exception.class)
public ResponseEntity> handleException(Exception e) {
    Result result;
    HttpStatus status;
    if (e instanceof BusinessException) {
        result = Result.error(((BusinessException) e).getCode(), e.getMessage());
        status = HttpStatus.BAD_REQUEST;
    } else if (e instanceof AccessDeniedException) {
        result = Result.error(403, "无权限");
        status = HttpStatus.FORBIDDEN;
    } else {
        result = Result.error(500, "系统异常");
        status = HttpStatus.INTERNAL_SERVER_ERROR;
    }
    return ResponseEntity.status(status).body(result);
}
// 处理特定Controller的异常
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
public class SpecificExceptionHandler {
    // 只处理这两个Controller的异常
}

原理简析@ControllerAdvice会被ControllerAdviceBean扫描并包装成ExceptionHandlerExceptionResolver。当Controller抛出异常时,DispatcherServlet会遍历所有HandlerExceptionResolver找到匹配的处理器。


2.7 事务管理

2.7.1 @Transactional - 数据一致性的守护者

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private AccountService accountService;
    @Autowired
    private StockService stockService;
    // 基础用法:方法执行成功则提交,异常则回滚
    @Transactional
    public void createOrder(OrderDTO dto) {
        // 1. 扣减库存
        stockService.decrease(dto.getProductId(), dto.getQuantity());
        // 2. 扣减账户余额
        accountService.deduct(dto.getUserId(), dto.getAmount());
        // 3. 创建订单
        Order order = new Order();
        BeanUtils.copyProperties(dto, order);
        orderRepository.save(order);
        // 4. 发送消息(如果失败,前面操作会回滚)
        sendOrderMessage(order);
    }
    // 只读事务:优化查询性能
    @Transactional(readOnly = true)
    public Order getOrderWithDetails(Long orderId) {
        Order order = orderRepository.findById(orderId);
        // 触发懒加载
        order.getItems().size();
        order.getLogs().size();
        return order;
    }
    // 指定隔离级别和传播行为
    @Transactional(
        isolation = Isolation.READ_COMMITTED,  // 读已提交,避免脏读
        propagation = Propagation.REQUIRED,    // 默认:有事务加入,无则新建
        timeout = 5,                           // 5秒超时
        rollbackFor = {BusinessException.class, SQLException.class},  // 指定回滚异常
        noRollbackFor = {IllegalArgumentException.class} // 指定不回滚
    )
    public void complexTransaction() {
        // ...
    }
}

【传播机制详解】

假设方法A调用方法B:

传播行为

A有事务

A无事务

使用场景

REQUIRED (默认)

B加入A的事务

B新建事务

主流程业务方法

REQUIRES_NEW

B挂起A,新建独立事务

B新建事务

日志记录(必须成功)

NESTED

B在A的事务内创建savepoint

B新建事务

部分回滚(如批量处理)

SUPPORTS

B加入A的事务

B非事务运行

查询方法

NOT_SUPPORTED

B挂起A,非事务运行

B非事务运行

只读操作

MANDATORY

B加入A的事务

抛异常

必须被事务调用

NEVER

抛异常

B非事务运行

绝对不能在事务中调用

【实战场景代码】

@Service
public class LoggingService {
    @Autowired
    private LogRepository logRepository;
    // 日志必须保存,即使主事务回滚
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(String content) {
        Log log = new Log();
        log.setContent(content);
        log.setCreateTime(LocalDateTime.now());
        logRepository.save(log);
    }
}
@Service
public class BatchImportService {
    @Autowired
    private ItemRepository itemRepository;
    // 批量导入,单条失败不影响其他
    @Transactional
    public void importData(List items) {
        for (ItemDTO dto : items) {
            try {
                processItem(dto);
            } catch (Exception e) {
                log.error("处理单条数据失败: {}", dto, e);
                // 继续处理下一条
            }
        }
    }
    @Transactional(propagation = Propagation.NESTED)  // 创建savepoint
    public void processItem(ItemDTO dto) {
        // 单条数据处理,失败只回滚这里
        itemRepository.save(convertToEntity(dto));
    }
}

【常见失效场景大集合】

@Service
public class TransactionalPitfalls {
    // 1. 非public方法不会生效(Spring AOP代理机制)
    @Transactional
    private void privateMethod() {}  // ❌ 失效
    // 2. 自调用不会生效(绕过代理)
    @Transactional
    public void methodA() {
        this.methodB();  // ❌ 直接调用,不走代理
    }
    @Transactional
    public void methodB() {}
    // 3. 异常被catch不抛出
    @Transactional
    public void methodC() {
        try {
            saveToDb();
        } catch (Exception e) {
            log.error(e);  // ❌ 异常被吃掉了,事务不会回滚
        }
    }
    // 4. 错误异常类型
    @Transactional(rollbackFor = Exception.class)  // ✅ 默认只回滚RuntimeException
    public void methodD() throws Exception {
        throw new Exception("检查异常");  // 必须声明rollbackFor=Exception.class
    }
    // 5. 数据库引擎不支持(如MyISAM)
    // CREATE TABLE my_table (...) ENGINE=InnoDB;  // ✅ 必须InnoDB
}

原理简析@Transactional通过Spring AOP代理实现,TransactionInterceptor拦截方法调用,创建TransactionInfo,在try-catch中执行业务逻辑,根据异常决定提交或回滚。自调用失效是因为this不是代理对象。

生产级建议

  1. 事务方法要短小:只包含数据库操作,RPC、HTTP调用放事务外
  2. 指定rollbackFor:业务异常通常是Exception的子类,必须声明
  3. 避免大事务:批量操作分批处理,防止锁表
  4. 只读查询加readOnly=true:优化性能,防止误写

2.8 JPA相关注解

2.8.1 @Entity + @Id + @Column

@Entity
@Table(name = "t_user", indexes = {
    @Index(name = "idx_username", columnList = "username"),
    @Index(name = "idx_email", columnList = "email", unique = true)
})
@EntityListeners(AuditingEntityListener.class)  // 自动填充创建时间
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // MySQL自增
    private Long id;
    @Column(nullable = false, length = 50, unique = true)
    private String username;
    @Column(name = "email_address", nullable = false, length = 100)
    private String email;
    @Column(columnDefinition = "TEXT")
    private String profile;
    @Enumerated(EnumType.STRING)  // 枚举存储为字符串
    @Column(length = 20)
    private UserStatus status;
    @Column(nullable = false, updatable = false)
    @CreatedDate
    private LocalDateTime createTime;
    @Column(nullable = false)
    @LastModifiedDate
    private LocalDateTime updateTime;
    @Version  // 乐观锁
    private Integer version;
}

【关联关系注解】

@Entity
public class Order extends BaseEntity {
    // 多对一
    @ManyToOne(fetch = FetchType.LAZY)  // 必须LAZY,避免N+1查询
    @JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "fk_order_user"))
    private User user;
    // 一对多
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List items = new ArrayList<>();
    // 一对一
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "shipping_id")
    private ShippingInfo shippingInfo;
}

原理简析@Entity由JPA实现(如Hibernate)扫描,SessionFactory创建时解析注解生成元数据,映射到数据库表。@Id定义主键,@GeneratedValue指定生成策略。

生产级建议

  • 所有关联必须LAZYFetchType.LAZY,避免N+1查询问题
  • 用DTO返回数据:不要在Controller直接返回Entity,会触发懒加载
  • 关闭open-in-viewspring.jpa.open-in-view=false,避免性能问题

2.9 测试相关注解

2.9.1 @SpringBootTest - 集成测试

@SpringBootTest  // 加载完整ApplicationContext
@AutoConfigureMockMvc  // 自动配置MockMvc
@ActiveProfiles("test")  // 激活测试配置
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)  // 指定执行顺序
public class UserControllerIntegrationTest {
    @Autowired
    private MockMvc mockMvc;  // 模拟MVC请求
    @Autowired
    private UserRepository userRepository;
    @BeforeEach
    void setUp() {
        // 每个测试前清理数据
        userRepository.deleteAll();
    }
    @Test
    @Order(1)
    @DisplayName("创建用户成功")
    void createUser_success() throws Exception {
        String json = "{\"username\":\"test\",\"email\":\"test@example.com\"}";
        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.data.username").value("test"))
                .andDo(print());  // 打印请求响应详情
    }
    @Test
    @Order(2)
    @DisplayName("查询用户列表")
    void getUsers_success() throws Exception {
        // 准备数据
        User user = new User();
        user.setUsername("test");
        userRepository.save(user);
        mockMvc.perform(get("/api/v1/users?page=1&size=10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.content").isArray())
                .andExpect(jsonPath("$.data.content.length()").value(1));
    }
}

【测试分层策略】

// 1. Unit Test(单元测试):Mock所有依赖
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    @InjectMocks
    private UserService userService;
    @Test
    public void testCreateUser() {
        when(userRepository.save(any())).thenReturn(new User());
        userService.createUser(new UserDTO());
        verify(userRepository, times(1)).save(any());
    }
}
// 2. Integration Test(集成测试):测试真实交互
@DataJpaTest  // 只加载JPA相关
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)  // 用真实数据库
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    public void testQuery() {
        // 测试真实SQL
    }
}
// 3. Slice Test(切片测试):只测某一层
@WebMvcTest(UserController.class)  // 只启动Web层,Mock Service
public class UserControllerSliceTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private UserService userService;  // Mock Service层
}

2.9.2 @Test + @WithMockUser - 安全测试

@SpringBootTest
@WithMockUser(roles = {"ADMIN"})  // 模拟管理员用户
public class AdminServiceTest {
    @Autowired
    private AdminService adminService;
    @Test
    @WithMockUser(authorities = "user:delete")  // 覆盖类级别配置
    public void testDeleteUser() {
        // 测试需要user:delete权限的方法
    }
    @Test
    @WithAnonymousUser  // 匿名用户
    public void testPublicEndpoint() {
        // 测试公开接口
    }
}

原理简析@WithMockUserWithSecurityContextTestExecutionListener处理,创建一个TestingAuthenticationToken并设置到SecurityContextHolder中。


3. 小结

通过本文的系统学习,相信你已经对Spring Boot常用注解有了全面的认识。让我们来总结一下核心要点:

核心记忆点

  • 基础注解@SpringBootApplication是入口,包含三大功能
  • 依赖注入:构造器注入是首选,避免字段注入
  • 配置绑定@ConfigurationProperties优于@Value
  • 参数校验:使用Jakarta Bean Validation,支持分组校验
  • 异常处理@ControllerAdvice统一处理,分层响应
  • 事务管理:注意传播行为,避免同类调用失效

如果对你有帮助,欢迎点赞收藏。有问题的评论区交流,看到必回。

参考链接

posted @ 2026-01-31 13:12  gccbuaa  阅读(6)  评论(0)    收藏  举报