
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以保证可移植性
原理简析 :@Resource由CommonAnnotationBeanPostProcessor处理,它会先解析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();
}
}
原理简析 :@Scope由ScopedProxyMode控制代理行为。默认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 {}
【原理简析】
@Value由AutowiredAnnotationBeanPostProcessor处理,支持SpEL表达式解析@ConfigurationProperties由ConfigurationPropertiesBindingPostProcessor处理,通过JavaBeans规范绑定属性,支持复杂类型转换
常见坑点:
- 配置类忘记加
@Component或@EnableConfigurationProperties,导致配置不生效 - 在
@ConfigurationProperties类中用@Value,两者混用导致混乱 - 配置项改名后,启动不报错但值为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));
}
常见坑点:
@RequestBody只能用POST/PUT/PATCH :GET请求没有body,用了会报错HttpMessageNotReadableException- 多个
@RequestBody无效 :一个方法只能有一个@RequestBody,因为HTTP请求只有一个body - @PathVariable类型转换失败 :URL变量类型不匹配会抛
TypeMismatchException,要用全局异常处理捕获 @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不是代理对象。
生产级建议:
- 事务方法要短小:只包含数据库操作,RPC、HTTP调用放事务外
- 指定rollbackFor:业务异常通常是Exception的子类,必须声明
- 避免大事务:批量操作分批处理,防止锁表
- 只读查询加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指定生成策略。
生产级建议:
- 所有关联必须LAZY :
FetchType.LAZY,避免N+1查询问题 - 用DTO返回数据:不要在Controller直接返回Entity,会触发懒加载
- 关闭open-in-view :
spring.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() {
// 测试公开接口
}
}
原理简析 :@WithMockUser由WithSecurityContextTestExecutionListener处理,创建一个TestingAuthenticationToken并设置到SecurityContextHolder中。
3. 小结
通过本文的系统学习,相信你已经对Spring Boot常用注解有了全面的认识。让我们来总结一下核心要点:
🎯 核心记忆点
- 基础注解 :
@SpringBootApplication是入口,包含三大功能 - 依赖注入:构造器注入是首选,避免字段注入
- 配置绑定 :
@ConfigurationProperties优于@Value - 参数校验:使用Jakarta Bean Validation,支持分组校验
- 异常处理 :
@ControllerAdvice统一处理,分层响应 - 事务管理:注意传播行为,避免同类调用失效
如果对你有帮助,欢迎点赞收藏。有问题的评论区交流,看到必回。
参考链接:
- Spring Boot官方文档:https://spring.io/projects/spring-boot
- Hibernate Validator文档:https://hibernate.org/validator/
- JPA规范:https://jakarta.ee/specifications/persistence/
- 参考学习文章:Spring&SpringBoot常用注解总结 | JavaGuide