Spring框架从入门到实战:Java开发者掌握IoC容器、AOP原理和Spring Boot的完整指南,解决企业开发中依赖注入、事务管理和配置简化等真实问题,帮助新手快速上手Spring生态开发。
这篇文章由Agnes撰写,Agnes是Sapiens AI开发的语言模型
写这篇文章的时候,我脑海里浮现的是三年前自己刚接触Spring时的那种抓狂感。那时候看着满屏的XML配置、绕来绕去的bean依赖,还有报错信息里那些看不懂的异常栈,真的想摔键盘。但当你真正摸透了Spring的门道,你会发现,它就像是一个超级贴心的管家,把那些重复、繁琐的事情都帮你处理好了,你只需要专注于业务逻辑本身。
好,咱们今天就从一个实战开发者的角度,把Spring从底层原理到企业级应用,掰开揉碎了讲清楚。
一、IoC容器:你以为你在new对象,其实Spring在替你操心
1.1 什么是IoC,为什么要用
先讲个真实的场景。假设你在开发一个电商系统,有一个OrderService需要调用PaymentService。如果你用传统的做法:
public class OrderService {
private PaymentService paymentService = new PaymentService();
public void placeOrder(Order order) {
paymentService.pay(order);
}
}
看起来没问题对吧?但问题大了:
- 耦合严重:
OrderService硬编码依赖了PaymentService的具体实现。如果哪天要换成支付宝、微信支付,或者测试的时候需要Mock这个服务,你就得改代码。 - 测试困难:想给
OrderService写单元测试?你得真创建一个PaymentService对象,还要处理它的所有依赖。 - 难以复用:
PaymentService被写死在OrderService里,别的地方想用也借不走。
IoC(控制反转)就是来解决这个问题的。你把”创建依赖对象”这个权力,从代码里交出去,交给一个容器来管理。你不是new了一个依赖,而是告诉容器你需要什么,容器会把它送上门。
1.2 依赖注入的三种方式
方式一:构造器注入(推荐)
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
// 构造器注入
public OrderService(PaymentService paymentService, InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public OrderResult placeOrder(Order order) {
// 检查库存
inventoryService.checkStock(order.getItems());
// 支付
paymentService.pay(order);
return new OrderResult(order.getId(), "SUCCESS");
}
}
为什么推荐构造器注入?
- 不可变性:依赖一旦注入,就不会被篡改,线程安全。
- 强制依赖:构造函数必须传参,没有这个依赖你就无法创建对象,避免了”忘记注入”的问题。
- 单元测试友好:直接
new OrderService(mockPayment, mockInventory)就能测试。
方式二:Setter注入
@Service
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
适合可选依赖(可以不注入),但Spring官方更推荐构造器注入。
方式三:字段注入(不推荐,但很常见)
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
public void placeOrder(Order order) {
paymentService.pay(order);
}
}
写起来最省事,但有几个坑:
- 测试麻烦:不借助Spring容器,直接
new OrderService(),paymentService是null。 - 隐含依赖:读代码的人看不出这个类依赖了什么,得找
@Autowired注解。 - 循环依赖风险:Spring能处理字段注入的循环依赖,但构造器注入的循环依赖会直接报错,提前暴露问题。
实际项目建议:优先使用构造器注入,配合Lombok的@RequiredArgsConstructor可以少写很多样板代码:
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
// Lombok自动生成构造器,里面就带着@Autowired的逻辑
}
1.3 Bean的生命周期:从出生到死亡
理解IoC容器,必须搞清楚Bean的生命周期。Spring容器管理Bean的整个过程:
① 实例化Bean → ② 注入属性 → ③ 初始化 → ④ Bean就绪(可以使用) → ⑤ 销毁
关键阶段和对应钩子:
| 阶段 | 注解/接口 | 用途 |
|---|---|---|
| 实例化 | - | Spring用构造器创建对象 |
| 属性注入 | @Autowired |
把依赖注入进来 |
| 初始化前 | BeanPostProcessor.postProcessBeforeInitialization |
预处理 |
| 初始化 | @PostConstruct / InitializingBean |
自定义初始化逻辑 |
| 初始化后 | BeanPostProcessor.postProcessAfterInitialization |
后处理(AOP代理就在这里生成) |
| 销毁前 | @PreDestroy / DisposableBean |
清理资源 |
实战示例:
@Component
public class DataSourceConfig {
private String url;
private String username;
// 构造器注入
public DataSourceConfig(@Value("${db.url}") String url,
@Value("${db.username}") String username) {
this.url = url;
this.username = username;
System.out.println("① Bean实例化,属性还未注入");
}
// 属性注入后
@PostConstruct
public void init() {
System.out.println("③ 初始化:连接数据库 " + url);
// 这里可以打开连接、加载配置等
}
@PreDestroy
public void cleanup() {
System.out.println("⑤ 销毁:关闭数据库连接");
}
}
理解生命周期后,你就能明白为什么有些配置类要加@PostConstruct,有些要实现InitializingBean接口。
二、AOP原理:横切关注点的优雅解法
2.1 AOP是什么,解决什么问题
继续用电商系统的例子。假设你要给所有Service方法加日志、做权限校验、或者处理异常。没有AOP之前,你会怎么写?
public OrderResult placeOrder(Order order) {
log.info("开始下单: {}", order.getId());
try {
// 业务逻辑
return doPlaceOrder(order);
} catch (Exception e) {
log.error("下单失败: {}", e.getMessage());
throw e;
}
}
然后每个Service的每个方法都这么写?代码重复爆炸,而且一旦要改日志格式或者加一个权限检查,你得改几十个文件。
AOP(面向切面编程)就是干这个的。它把”横切关注点”——那些跟业务无关但每个方法都需要的逻辑,比如日志、事务、安全——抽离出来,统一处理。
2.2 核心概念
- 切面(Aspect):横切关注点的模块化,比如
LogAspect、TransactionAspect - 连接点(Join Point):程序执行过程中可以插入切面的点,Spring只支持方法级别的连接点
- 通知(Advice):切面在特定连接点执行的动作,有五种类型
- 切入点(Pointcut):定义哪些连接点需要执行通知,用表达式描述
- 代理(Proxy):AOP的本质是动态代理,Spring默认用JDK动态代理,Bean实现接口时用;没有接口时用CGLIB
通知类型:
├── @Before — 方法执行前
├── @After — 方法执行后(无论是否异常)
├── @AfterReturning — 方法成功返回后
├── @AfterThrowing — 方法抛出异常后
└── @Around — 包围整个方法调用,最强大
2.3 实战:写一个自己的切面
场景:给所有Service方法自动加执行时间日志,超时的标红警告。
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
// 定义切入点:com.example.service包下所有public方法
@Pointcut("execution(public * com.example.service..*.*(..))")
public void serviceLayer() {}
@Around("serviceLayer()")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().toShortString();
log.info("▶ 开始执行: {}", methodName);
Object result;
try {
result = joinPoint.proceed(); // 执行目标方法
} catch (Exception e) {
long cost = System.currentTimeMillis() - startTime;
log.error("✗ 方法异常: {}, 耗时: {}ms, 异常: {}", methodName, cost, e.getMessage());
throw e;
}
long cost = System.currentTimeMillis() - startTime;
if (cost > 1000) {
log.warn("⚠ 方法耗时过长: {}, 耗时: {}ms", methodName, cost);
} else {
log.info("✓ 方法执行完成: {}, 耗时: {}ms", methodName, cost);
}
return result;
}
}
关键点解释:
@Pointcut定义了切入点表达式,execution(public * com.example.service..*.*(..))的意思是:com.example.service包及其子包下,所有public方法的调用。ProceedingJoinPoint.proceed()是核心,它触发目标方法的执行,你可以在前后加入自己的逻辑。- 这个切面是自动生效的,不需要改任何Service代码。
2.4 AOP代理的本质:Spring是怎么做到这一点的
很多人不知道,Spring AOP的本质是动态代理。理解这一点很重要。
当Spring容器启动时,对于被@Aspect标注的Bean,Spring会:
- 创建原始的Target Bean
- 根据切面配置,生成一个Proxy Bean(代理对象)
- 在ApplicationContext中,所有引用这个Bean的地方,拿到的都是Proxy,而不是原始对象
你调用的:proxyService.placeOrder(order)
↓
代理拦截:执行通知逻辑(日志、事务等)
↓
转发调用:targetService.placeOrder(order)
↓
返回结果:代理把结果传回给你
JDK动态代理 vs CGLIB代理:
// JDK动态代理:要求目标类实现接口
public class PaymentServiceImpl implements PaymentService {
public void pay(Order order) { ... }
}
// Spring会生成一个PaymentService接口的代理,重写pay方法
// CGLIB代理:通过继承实现,目标类不需要实现接口
public class OrderServiceImpl { ... }
// Spring会生成OrderServiceImpl的子类,重写方法
Spring Boot 2.x之后默认使用CGLIB,可以通过spring.aop.proxy-target-class=true强制使用CGLIB,或者proxyTargetClass=false配合接口使用JDK代理。
一个常见的坑:@Autowired注入的Bean如果是AOP代理,你不能用instanceof去判断原始类型,因为代理对象不是原始类的实例。要用AopUtils.isCglibProxy()或AopUtils.isJdkDynamicProxy()来判断代理类型。
三、Spring Boot:让Spring开发变得简单
3.1 为什么需要Spring Boot
传统Spring开发有几个痛点:
- 配置地狱:一个项目可能有好几个XML配置文件,几千行
<bean>定义 - 依赖管理复杂:要手动找兼容的jar包版本,容易冲突
- 部署麻烦:需要配置Tomcat、web.xml等
Spring Boot的核心理念是”约定优于配置”。它通过自动配置(Auto-Configuration),在你classpath里找到某个依赖时,自动帮你配置好相关的Bean。
比如你加了spring-boot-starter-web,Spring Boot会自动:
- 配置内嵌Tomcat
- 配置
DispatcherServlet - 配置Jackson序列化
- 配置Spring MVC的默认行为
你几乎不需要写任何XML配置。
3.2 项目结构:一个标准Spring Boot项目
ecommerce-service/
├── src/main/java/com/example/ecommerce/
│ ├── EcommerceApplication.java # 启动类
│ ├── config/ # 配置类
│ │ ├── WebConfig.java
│ │ ├── DataSourceConfig.java
│ │ └── RedisConfig.java
│ ├── controller/ # 控制器层
│ │ ├── OrderController.java
│ │ └── PaymentController.java
│ ├── service/ # 业务层
│ │ ├── OrderService.java
│ │ ├── impl/
│ │ │ ├── OrderServiceImpl.java
│ │ │ └── PaymentServiceImpl.java
│ │ └── PaymentService.java
│ ├── repository/ # 数据访问层
│ │ ├── OrderRepository.java
│ │ └── PaymentRepository.java
│ ├── entity/ # 实体类
│ │ ├── Order.java
│ │ └── Payment.java
│ ├── dto/ # 数据传输对象
│ │ ├── OrderDTO.java
│ │ └── PaymentDTO.java
│ ├── aspect/ # 切面
│ │ ├── LogAspect.java
│ │ └── TransactionAspect.java
│ ├── exception/ # 异常处理
│ │ ├── GlobalExceptionHandler.java
│ │ └── BusinessException.java
│ └── util/ # 工具类
├── src/main/resources/
│ ├── application.yml # 主配置文件
│ ├── application-dev.yml # 开发环境配置
│ ├── application-prod.yml # 生产环境配置
│ └── logback-spring.xml # 日志配置
└── pom.xml
3.3 启动类:@SpringBootApplication的三个含义
@SpringBootApplication
public class EcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(EcommerceApplication.class, args);
}
}
@SpringBootApplication 是三个注解的组合:
@Configuration:标记这个类是配置类,可以定义Bean@EnableAutoConfiguration:开启自动配置,Spring Boot会根据classpath自动配置Bean@ComponentScan:扫描当前包及子包下的组件
3.4 自动配置原理:Spring Boot到底自动配置了什么
@EnableAutoConfiguration 通过@Import(AutoConfigurationImportSelector.class)导入配置。Spring Boot会:
- 读取
spring-boot-autoconfigure依赖下的META-INF/spring.factories文件(Spring Boot 2.7之前) - 或者读取
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 2.7+) - 根据条件注解(
@ConditionalOnClass、@ConditionalOnMissingBean等)决定是否加载某个配置
// 这是一个自动配置类的简化示例
@Configuration
@ConditionalOnClass({DataSource.class, DataSourceProperties.class}) // 类路径下有DataSource才生效
@ConditionalOnMissingBean(DataSource.class) // 用户没有自定义DataSource才生效
@ConditionalOnProperty(prefix = "spring.datasource", name = "url") // 配置文件中配置了url才生效
public class DataSourceAutoConfiguration {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
这个自动配置类会:
- 检查
spring.datasource.url等属性 - 创建一个
DataSourceBean - 你可以覆盖它,只要在配置里加
spring.datasource.url=xxx,然后在@Bean里自定义你的DataSource
实战技巧:想看Spring Boot自动配置了哪些Bean,启动时加参数:
java -jar app.jar --debug
控制台会打印所有自动配置类的匹配和不匹配原因,非常有用。
3.5 配置文件:application.yml的魔法
# application.yml - 主配置
spring:
application:
name: ecommerce-service
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/ecommerce?useSSL=false&serverTimezone=UTC
username: root
password: ${DB_PASSWORD} # 从环境变量读取
driver-class-name: com.mysql.cj.jdbc.Driver
# JPA配置
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
# 热部署(开发环境)
devtools:
restart:
enabled: true
additional-paths: src/main/java
# 服务器配置
server:
port: 8080
servlet:
context-path: /api
# 日志配置
logging:
level:
com.example.ecommerce: DEBUG
org.springframework: INFO
org.hibernate.SQL: DEBUG
# application-dev.yml - 开发环境
spring:
datasource:
url: jdbc:mysql://localhost:3306/ecommerce_dev?useSSL=false&serverTimezone=UTC
username: root
password: dev123
# 开启Actuator所有端点
management:
endpoints:
web:
exposure:
include: "*"
# application-prod.yml - 生产环境
spring:
datasource:
url: jdbc:mysql://prod-db:3306/ecommerce?useSSL=true&serverTimezone=UTC
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
logging:
level:
com.example.ecommerce: WARN
org.springframework: ERROR
多环境切换:
# 启动时指定环境
java -jar app.jar --spring.profiles.active=prod
四、依赖注入的实战问题与解决方案
4.1 循环依赖问题
这是Spring新手最常遇到的问题。两个Bean互相依赖:
@Service
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
@Service
public class PaymentService {
private final OrderService orderService; // 循环依赖!
@Autowired
public PaymentService(OrderService orderService) {
this.orderService = orderService;
}
}
Spring容器启动时会报错:BeanCurrentlyInCreationException: Error creating bean with name 'orderService'
解决方案:
- 重构代码(推荐):把共同依赖提取成第三个Service
@Service
public class OrderPaymentCoordinator {
public void processOrder(Order order) {
// 协调订单和支付
}
}
- 使用
@Lazy注解(临时方案):
@Service
public class PaymentService {
@Lazy
@Autowired
public PaymentService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
}
- 改用字段注入+
@Lazy:
@Service
public class OrderService {
@Lazy
@Autowired
private PaymentService paymentService;
}
4.2 多数据源配置
企业级项目经常需要连接多个数据库:
@Configuration
public class MultiDataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "primaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
@Qualifier("primaryDataSource") DataSource dataSource,
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(dataSource)
.packages("com.example.ecommerce.entity.primary")
.persistenceUnit("primary")
.build();
}
@Bean(name = "secondaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
@Qualifier("secondaryDataSource") DataSource dataSource,
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(dataSource)
.packages("com.example.ecommerce.entity.secondary")
.persistenceUnit("secondary")
.build();
}
}
spring:
datasource:
primary:
url: jdbc:mysql://localhost:3306/ecommerce
username: root
password: root
secondary:
url: jdbc:mysql://localhost:3306/ecommerce_log
username: root
password: root
4.3 条件化Bean:根据环境决定注入什么
@Component
@ConditionalOnProperty(name = "feature.payment.alipay", havingValue = "true")
public class AlipayPaymentService implements PaymentService {
public void pay(Order order) {
// 支付宝支付逻辑
}
}
@Component
@ConditionalOnProperty(name = "feature.payment.wechat", havingValue = "true")
public class WechatPaymentService implements PaymentService {
public void pay(Order order) {
// 微信支付逻辑
}
}
配置文件里:
feature:
payment:
alipay: true
wechat: false
这样你可以不改代码,只改配置,就能切换支付渠道。这在多环境部署中非常有用。
五、事务管理:保证数据一致性
5.1 @Transactional的工作原理
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 30,
rollbackFor = Exception.class
)
public OrderResult placeOrder(Order order) {
// 1. 扣库存
inventoryService.reduceStock(order.getItems());
// 2. 创建订单
Order savedOrder = orderRepository.save(order);
// 3. 支付
paymentService.pay(savedOrder);
return new OrderResult(savedOrder.getId(), "SUCCESS");
}
}
@Transactional的本质:Spring通过AOP代理,在方法执行前后包装了事务逻辑:
调用placeOrder()
↓
代理拦截
↓
开启事务
↓
执行目标方法(扣库存、创建订单、支付)
↓
提交/回滚事务
↓
返回结果
5.2 传播行为详解
public enum Propagation {
REQUIRED, // 默认:有事务就加入,没有就新建
REQUIRES_NEW, // 总是新建事务,挂起当前事务
SUPPORTS, // 有事务就加入,没有就以非事务方式执行
NOT_SUPPORTED, // 以非事务方式执行,挂起当前事务
MANDATORY, // 必须有事务,没有就抛异常
NEVER, // 不能有事务,有就抛异常
NESTED // 嵌套事务(保存点机制)
}
实战场景:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private AuditLogService auditLogService;
@Transactional(propagation = Propagation.REQUIRED)
public OrderResult placeOrder(Order order) {
Order savedOrder = orderRepository.save(order);
// 这条日志记录不应该影响主事务
auditLogService.logOrderCreated(savedOrder);
return new OrderResult(savedOrder.getId(), "SUCCESS");
}
}
@Service
public class AuditLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderCreated(Order order) {
// 即使主事务回滚,日志也会单独提交
auditLogRepository.save(new AuditLog(order.getId(), "ORDER_CREATED"));
}
}
REQUIRES_NEW的典型用法:日志记录、统计、通知等不应该跟着主事务一起回滚的场景。
5.3 事务失效的常见坑
坑1:自调用问题
@Service
public class OrderService {
@Transactional
public OrderResult placeOrder(Order order) {
// 这里调用的是代理方法,事务生效
saveOrder(order);
return new OrderResult(...);
}
@Transactional
public void saveOrder(Order order) {
// 但是下面的调用不会走代理,事务失效!
}
public void callSaveOrder(Order order) {
saveOrder(order); // 同类调用,@Transactional失效!
}
}
原因:@Transactional是通过代理实现的,同类内部调用不会走代理,直接调用原始方法。
解决方案:
- 把事务方法提取到另一个Service
- 注入自身代理:
@Autowired private OrderService orderService;然后orderService.saveOrder(order) - 用
AopContext.currentProxy():((OrderService)AopContext.currentProxy()).saveOrder(order)(需要开启exposeProxy)
@Configuration
public class AopConfig {
@Bean
public AutoProxyCreator autoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setExposeProxy(true);
return creator;
}
}
坑2:异常类型不对
@Transactional(rollbackFor = Exception.class)
public void saveOrder(Order order) {
// 默认只回滚RuntimeException和Error
// 如果抛出的是Checked Exception,默认不回滚!
throw new Exception("自定义异常");
}
解决方案:显式指定rollbackFor:
@Transactional(rollbackFor = Exception.class)
坑3:数据库引擎不支持事务
MySQL的MyISAM引擎不支持事务!务必用InnoDB。
六、Spring Boot实战:企业级项目搭建
6.1 pom.xml依赖管理
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
6.2 全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return ResponseEntity.ok(ApiResponse.fail(e.getCode(), e.getMessage()));
}
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.ok(ApiResponse.fail(400, message));
}
// 通用异常
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.ok(ApiResponse.fail(500, "服务器内部错误"));
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "成功", data);
}
public static <T> ApiResponse<T> fail(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
6.3 统一响应格式
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<ApiResponse<OrderResult>> createOrder(@Valid @RequestBody CreateOrderRequest request) {
OrderResult result = orderService.placeOrder(request.toOrder());
return ResponseEntity.ok(ApiResponse.success(result));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Order>> getOrder(@PathVariable Long id) {
Order order = orderService.getOrder(id);
return ResponseEntity.ok(ApiResponse.success(order));
}
}
6.4 单元测试与集成测试
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private OrderService orderService;
@MockBean
private PaymentService paymentService;
@MockBean
private InventoryService inventoryService;
@Test
void createOrder_success() throws Exception {
// Given
CreateOrderRequest request = new CreateOrderRequest();
request.setUserId(1L);
request.setItems(List.of(new OrderItem(1L, 2)));
OrderResult expectedResult = new OrderResult(1L, "SUCCESS");
when(orderService.placeOrder(any())).thenReturn(expectedResult);
// When & Then
mockMvc.perform(MockMvcRequestBuilders
.post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectToJson(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.orderId").value(1));
}
@Test
void createOrder_invalidRequest() throws Exception {
// 测试参数校验
mockMvc.perform(MockMvcRequestBuilders
.post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(400));
}
}
七、性能优化与生产注意事项
7.1 缓存注解实战
@Service
@CacheConfig(cacheNames = "orders")
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Cacheable(key = "#id")
public Order getOrder(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new BusinessException("订单不存在"));
}
@CacheEvict(key = "#id")
public void deleteOrder(Long id) {
orderRepository.deleteById(id);
}
@CachePut(key = "#result.id")
public Order updateOrder(Long id, Order update) {
Order order = getOrder(id);
// 更新逻辑
return orderRepository.save(order);
}
}
配置文件:
spring:
cache:
type: redis
redis:
time-to-live: 600000 # 10分钟
cache-null-values: false
7.2 Actuator监控
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,beans,mappings
endpoint:
health:
show-details: always
info:
enabled: true
访问http://localhost:8080/actuator/health可以看到应用健康状态。
7.3 日志最佳实践
@Slf4j
@Service
public class OrderService {
public OrderResult placeOrder(Order order) {
// 用占位符,不要用字符串拼接
log.info("开始处理订单: orderId={}", order.getId());
try {
Order saved = orderRepository.save(order);
log.info("订单创建成功: orderId={}, userId={}", saved.getId(), saved.getUserId());
return new OrderResult(saved.getId(), "SUCCESS");
} catch (Exception e) {
// 用参数传入异常,不要拼字符串
log.error("订单创建失败: orderId={}, reason={}", order.getId(), e.getMessage(), e);
throw e;
}
}
}
八、学习路径建议
从入门到精通,我建议这个顺序:
- 第一阶段:手写IoC容器原理,理解Bean的生命周期
- 第二阶段:用纯Spring XML配置做一个小项目
- 第三阶段:迁移到注解配置,理解
@Component、@Service、@Autowired - 第四阶段:学习AOP,自己写一个切面
- 第五阶段:接触Spring Boot,体验自动配置的便利
- 第六阶段:深入学习Spring Boot高级特性(Actuator、Admin、Config)
- 第七阶段:结合Spring Cloud,学习微服务架构
推荐资源:
- 官方文档:https://spring.io/projects/spring-boot
- 《Spring实战》第5版
- Baeldung的Spring教程(英文)
写到这里,我希望你能感受到:Spring框架的学习曲线确实是陡峭的,但一旦你理解了它的核心理念——IoC和AOP——很多看似复杂的东西都会变得清晰。它的设计哲学是”不重新发明轮子”,而是帮你把轮子装好,你只需要开车。
记住,最好的学习方式就是动手写代码。不要只看书,要建一个项目,把@Autowired、@Transactional、@Aspect一个个用上,观察它们的实际效果。遇到问题,打印日志,看源码,Google报错信息——每一步都是成长。
希望这篇文章能成为你Spring学习之路上的一个靠谱的起点。有任何问题,随时来问。
