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项目的入口,但它远不止"启动类标记"这么简单。

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

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

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

核心作用分解:

|----------------------------|--------------------------------|-----------------------|
| 注解 | 作用 | 生产级建议 |
| @SpringBootConfiguration | 声明这是一个配置类,相当于@Configuration | 不要在启动类里写@Bean,保持启动类干净 |
| @EnableAutoConfiguration | 自动配置核心,根据classpath自动配置Bean | 排除不需要的自动配置(见下方代码) |
| @ComponentScan | 组件扫描,默认扫描当前包及子包 | 明确指定扫描路径,避免扫到无关包 |

【生产环境避坑指南】

java 复制代码
// 错误示例:启动类乱放
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亲儿子的注入方式

java 复制代码
@Service
public class OrderService {
    @Autowired
    private UserService userService;  // 按类型注入
  
    @Autowired
    private List<PaymentProcessor> paymentProcessors;  // 注入所有实现类
}

【核心特性深度解析】

1. 注入机制(按类型)

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

2. 必要性和顺序控制

java 复制代码
@Autowired(required = false)  // 找不到Bean也不报错,适合可选依赖
private Optional<CacheService> cacheService;  // Java 8+推荐写法

@Autowired
@Order(1)  // 注入List时排序
private List<Validator> validators;

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

java 复制代码
@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;
    }
}

【生产级最佳实践】

java 复制代码
// 强烈推荐: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。
java 复制代码
// 错误示范:构造器里用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的标准选择

java 复制代码
@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原生优化) | 稍慢(需解析名称) |

【实战选择建议】

java 复制代码
// 场景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 - 解决多实现类的首选方案

java 复制代码
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
}

【进阶用法】

java 复制代码
// 条件化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的生命周期

java 复制代码
@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生命周期 | 全局共享数据 | 几乎不用 |

【生产级解决方案】

java 复制代码
// 正确获取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<PrototypeService> prototypeServiceFactory;
  
    public void processOrder() {
        PrototypeService service = prototypeServiceFactory.getObject();
        service.doWork();
    }
}

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


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

java 复制代码
// 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 | ✅ |

【生产级分层建议】

java 复制代码
// 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<UserDTO> create(@RequestBody @Validated UserCreateDTO dto) {
        // 参数组装、调用service、返回结果
        // Controller层要薄,不做复杂业务逻辑
    }
}

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


2.3 配置注解

2.3.1 @Configuration - 声明配置类

java 复制代码
// 错误示范:用@Component声明配置
@Component
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        // 这样也能用,但不规范,且有代理问题
    }
}

// 正确做法:@Configuration
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> 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 核心区别】

java 复制代码
// 测试类
@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:单个值注入

java 复制代码
@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:批量绑定配置

java 复制代码
# 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
java 复制代码
@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<String, String> 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、嵌套对象 |

【生产级配置规范】

java 复制代码
// 1. 添加依赖获取IDE提示
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
// 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 - 加载自定义配置文件

java 复制代码
@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结合】

java 复制代码
@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 五大请求映射注解

java 复制代码
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
  
    @GetMapping("/{id}")  // GET查询
    public User getUser(@PathVariable Long id) { /* ... */ }
  
    @PostMapping  // POST创建
    public ResponseEntity<User> 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<String, Object> updates) { /* ... */ }
  
    @DeleteMapping("/{id}")  // DELETE删除
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) { /* ... */ }
  
    // @RequestMapping是它们的老祖宗
    @RequestMapping(value = "/search", method = {RequestMethod.GET, RequestMethod.POST})
    public List<User> searchUsers(@RequestParam String keyword) { /* ... */ }
}

【RESTful API规范建议】

java 复制代码
// 完整资源路径,包含版本号
@RequestMapping("/api/v1/users")

// 幂等性设计
@GetMapping    // 查询,安全且幂等
@PutMapping    // 全量更新,幂等
@DeleteMapping // 删除,幂等
@PostMapping   // 创建,非幂等
@PatchMapping  // 部分更新,非幂等

// 状态码返回规范
@PostMapping
public ResponseEntity<User> 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高级特性】

java 复制代码
//  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 - 路径变量
java 复制代码
@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 - 查询参数
java 复制代码
@GetMapping("/search")
public List<User> searchUsers(
    @RequestParam String keyword,  // 必填
    @RequestParam(required = false, defaultValue = "1") int page,  // 可选+默认值
    @RequestParam Map<String, String> params  // 接收所有参数
) {
    // URL: /search?keyword=jack&page=2&sort=name
    // keyword="jack", page=2, params包含所有键值对
}
@RequestBody - 请求体(JSON)
java 复制代码
@PostMapping
public ResponseEntity<User> create(
    @RequestBody @Validated UserCreateDTO dto,  // 自动JSON反序列化+校验
    @RequestHeader("X-Request-ID") String requestId  // 额外:header获取
) {
    // 请求头:Content-Type: application/json
    // 请求体:{"username":"jack","email":"jack@example.com"}
}

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

java 复制代码
// 一律用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<Page<UserVO>> 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 空值检查三兄弟

java 复制代码
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 细碎但重要的校验注解

java 复制代码
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<String> tags;
  
    // 布尔值断言
    @AssertTrue(message = "必须同意协议")
    private boolean agreementAccepted;
}

【自定义校验注解】

java 复制代码
// 1. 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. 实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
    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 - 开启校验的钥匙

java 复制代码
@RestController
@RequestMapping("/api/users")
@Validated  // 在类级别开启方法参数校验
public class UserController {
  
    // 方法参数校验(路径变量)
    @GetMapping("/{id}")
    public User getUser(@PathVariable @Min(1) Long id) { /* ... */ }
  
    // 请求体校验
    @PostMapping
    public ResponseEntity<User> 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

java 复制代码
// 统一返回值结构
@Data
public class Result<T> {
    private int code;
    private String message;
    private T data;
  
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setData(data);
        return result;
    }
  
    public static <T> Result<T> error(int code, String message) {
        Result<T> 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<Void> 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<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
  
    // 处理所有未捕获的异常
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
  
    // 处理404
    @ExceptionHandler(NoHandlerFoundException.class)
    public Result<Void> handleNotFound(NoHandlerFoundException e) {
        return Result.error(404, "接口不存在: " + e.getRequestURL());
    }
  
    // 处理ConstraintViolationException(@Validated方法参数校验)
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<Void> handleConstraintViolation(ConstraintViolationException e) {
        String message = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, "参数校验失败: " + message);
    }
}

【进阶技巧】

java 复制代码
// 按异常状态码返回不同HTTP状态
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleException(Exception e) {
    Result<Void> 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 - 数据一致性的守护者

java 复制代码
@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非事务运行 | 绝对不能在事务中调用 |

【实战场景代码】

java 复制代码
@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<ItemDTO> 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));
    }
}

【常见失效场景大集合】

java 复制代码
@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

java 复制代码
@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;
}

【关联关系注解】

java 复制代码
@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<OrderItem> 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 - 集成测试

java 复制代码
@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));
    }
}

【测试分层策略】

java 复制代码
// 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 - 安全测试

java 复制代码
@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统一处理,分层响应
  • 事务管理:注意传播行为,避免同类调用失效

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

参考链接

相关推荐
猫豆~2 小时前
ceph分布式存储——1day
java·linux·数据库·sql·云计算
有味道的男人2 小时前
淘宝图片搜索(拍立淘)+ 店铺全商品爬虫 深度实战指南(Python)
开发语言·爬虫·python
尘诞辰2 小时前
【C语言】数据在内存中的储存
c语言·开发语言·数据结构·c++
JPX-NO2 小时前
Rust + Rocket + Diesel构建的RESTful API示例(CRUD)
开发语言·rust·restful
running up2 小时前
Spring IOC与DI核心注解速查表
java·后端·spring
YDS8292 小时前
SpringCloud —— Sentinel详解
java·spring cloud·sentinel
无敌最俊朗@2 小时前
STL-关联容器(面试复习4)
开发语言·c++
洛阳泰山2 小时前
快速上手 MaxKB4J:开源企业级 Agentic 工作流系统在 Sealos 上的完整部署指南
java·人工智能·后端
bybitq2 小时前
string,byte,rune,character?详解Golang编码-UTF-8
开发语言·后端·golang