Java开发框架Spring学习:从零基础入门到企业级项目实战的完整指南与常见问题解决方案
一、先别急着写代码,聊聊Spring到底是什么
我第一次接触Spring的时候,满脑子都是问号:为什么大家都用Spring?它到底神奇在哪里?
说实话,如果你现在打开一个新项目,发现代码里到处是XML配置文件,几百行XML定义各种Bean,那大概率你看到的是Spring早期的版本。那时候开发确实头疼,改个配置要重启服务,配错一个ID整个项目跑不起来。
但现在的Spring完全不是那个样子了。
想象一下,你正在经营一家餐厅。传统的Java开发就像是你自己炒菜、自己服务、自己洗碗——什么都要亲力亲为。而Spring呢?它就像是给你请了一支专业的服务团队:有人负责帮你采购食材(依赖注入),有人负责帮你管理流程(事务管理),还有人负责在你不忙的时候偷偷把菜洗好(AOP切面编程)。
你只需要专注一件事:做菜(写业务逻辑)。
这就是Spring存在的意义——把复杂的事情封装好,让你只关注真正重要的部分。
二、Spring的核心三大件,听懂了你就入门了
Spring的东西看起来多,但核心就三个:IoC(控制反转)、DI(依赖注入)、AOP(面向切面编程)。这三个概念贯穿了整个Spring的生态系统,理解了它们,你就掌握了Spring的”内功心法”。
2.1 IoC:谁控制谁?反转了什么?
这是一个让很多初学者困惑的概念。我直接给你一个生活化的例子:
没有IoC的时候:
你开了一家奶茶店,你想做一杯奶茶,你需要自己买茶叶、自己买牛奶、自己买杯子。每做一杯奶茶,这些原材料的获取全都要你自己搞定。
// 传统方式:自己创建依赖对象
public class BubbleTeaShop {
private TeaLeaf teaLeaf = new TeaLeaf(); // 自己创建茶叶
private Milk milk = new Milk(); // 自己创建牛奶
private Cup cup = new Cup(); // 自己创建杯子
public void makeBubbleTea() {
teaLeaf.brew();
milk.pour();
cup.prepare();
// 制作奶茶...
}
}
问题很明显:你的奶茶店和茶叶店、牛奶店、杯子店耦合在一起了。茶叶换了一家供应商,你的代码就得改;牛奶涨价了,你还是得改。
有了IoC之后:
这时候Spring站出来了,它说:”你别管茶叶牛奶杯子从哪来,我给你准备一个容器,你需要什么跟我说,我给你注入进来。”
// IoC方式:依赖由Spring容器管理
public class BubbleTeaShop {
private TeaLeaf teaLeaf; // 我只声明,不负责创建
private Milk milk; // 我只声明,不负责创建
private Cup cup; // 我只声明,不负责创建
// Spring通过构造函数把依赖注入进来
@Autowired
public BubbleTeaShop(TeaLeaf teaLeaf, Milk milk, Cup cup) {
this.teaLeaf = teaLeaf;
this.milk = milk;
this.cup = cup;
}
public void makeBubbleTea() {
teaLeaf.brew();
milk.pour();
cup.prepare();
// 制作奶茶...
}
}
关键理解:IoC的本质是——你把对象的创建权”反转”给了Spring容器。以前是你控制对象(自己new),现在是Spring容器控制对象(它来创建和管理),你只需要告诉它你需要什么。
这个”反转”就是IoC的核心。
2.2 DI:依赖注入的几种姿势
IoC是思想,DI是具体实现方式。Spring提供了多种注入方式,各有优缺点。
方式一:构造器注入(推荐)
@Component
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
// 构造器注入 - Spring官方推荐的方式
@Autowired
public OrderService(OrderRepository orderRepository,
PaymentService paymentService,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
public Order createOrder(String userId, List<OrderItem> items) {
// 业务逻辑
Order order = new Order(userId, items);
orderRepository.save(order);
paymentService.pay(order);
notificationService.sendOrderConfirm(order);
return order;
}
}
优点:对象创建后就是完整可用的状态,方便单元测试,依赖关系一目了然。
方式二:Setter注入
@Component
public class OrderService {
private OrderRepository orderRepository;
private PaymentService paymentService;
// Setter注入
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
优点:可以选择性地注入,适合可选依赖。 缺点:对象可能处于不完整状态。
方式三:字段注入(不推荐但常见)
@Component
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
}
虽然代码简洁,但很多团队不推荐这种方式,因为依赖关系隐藏在了字段里,看不出来。
2.3 AOP:切面编程,让代码更整洁
AOP可能是Spring中最难理解但也最有用的概念。
想象一下,你的电商系统有100个业务方法,每个方法都需要做日志记录。没有AOP的时候,你每个方法里面都手写日志代码:
public Order createOrder(String userId, List<OrderItem> items) {
logger.info("开始创建订单,用户ID:{}", userId);
long startTime = System.currentTimeMillis();
// 业务逻辑...
long costTime = System.currentTimeMillis() - startTime;
logger.info("订单创建完成,耗时:{}ms", costTime);
return order;
}
100个方法,每个都这么写,代码臃肿不说,将来需求变了(比如日志要改成JSON格式),你得改100个地方。
AOP的做法是:把日志这个”横切关注点”抽出来,写成单独的切面,然后在需要的方法上标记一下就行。
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(com.example.annotation.LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long executionTime = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return result;
}
}
然后在业务方法上加个注解:
@LogExecutionTime
public Order createOrder(String userId, List<OrderItem> items) {
// 纯业务逻辑,没有日志代码
return orderService.create(userId, items);
}
AOP的本质:把那些和业务逻辑无关但每个方法都需要的代码(日志、事务、安全校验等),从业务方法中剥离出来,统一在切面中管理。
三、Spring Boot:Spring的现代化外衣
说完Spring的核心概念,必须聊聊Spring Boot。因为现在实际开发中,你用的基本都是Spring Boot,而不是传统的Spring。
3.1 为什么要用Spring Boot?
传统Spring项目最大的痛点就是配置太繁琐。一个项目下来,XML配置、注解配置、各种.properties文件,光配置就得写几天。
Spring Boot的理念很简单:约定优于配置。
它预设了很多”默认配置”,你不需要写配置文件,它自己就能跑起来。你想改配置?没问题,在application.properties里加几行就行。
3.2 一个Spring Boot项目从零开始
第一步:创建项目
用Spring Initializr(https://start.spring.io)是最快的方式,或者直接看代码结构:
my-shop/
├── src/main/java/
│ └── com/example/myshop/
│ ├── MyShopApplication.java # 启动类
│ ├── config/ # 配置类
│ ├── controller/ # 控制器层
│ ├── service/ # 服务层
│ ├── repository/ # 数据访问层
│ └── model/ # 实体类
├── src/main/resources/
│ ├── application.yml # 配置文件
│ └── static/ # 静态资源
└── pom.xml # Maven依赖
第二步:启动类
package com.example.myshop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // 这个注解包含了@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
public class MyShopApplication {
public static void main(String[] args) {
SpringApplication.run(MyShopApplication.class, args);
}
}
就这么简单,一个Spring Boot应用就启动了。
第三步:核心依赖配置
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring 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>
<!-- Lombok(简化Java代码的神器) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
第四步:实体类
package com.example.myshop.model;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "orders")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String userId;
@Column(nullable = false)
private String status;
@Column(nullable = false)
private Double totalAmount;
@Column(nullable = false)
private LocalDateTime createTime;
@Column(nullable = false)
private LocalDateTime updateTime;
@PrePersist
protected void onCreate() {
createTime = LocalDateTime.now();
updateTime = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updateTime = LocalDateTime.now();
}
}
第五步:数据访问层
package com.example.myshop.repository;
import com.example.myshop.model.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// 自定义查询方法,Spring Data JPA会自动生成实现
List<Order> findByUserId(String userId);
List<Order> findByStatus(String status);
List<Order> findByUserIdAndStatus(String userId, String status);
}
注意看,我定义了一个接口,连实现类都没写!Spring Data JPA会自动帮你生成实现。这就是”约定优于配置”的力量。
第六步:服务层
package com.example.myshop.service;
import com.example.myshop.model.Order;
import com.example.myshop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor // Lombok自动生成构造器注入
public class OrderService {
private final OrderRepository orderRepository;
/**
* 创建订单
*/
@Transactional // 声明式事务管理,Spring自动处理事务提交/回滚
public Order createOrder(String userId, List<OrderItem> items) {
double totalAmount = items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
Order order = new Order();
order.setUserId(userId);
order.setStatus("PENDING");
order.setTotalAmount(totalAmount);
log.info("创建订单,用户:{},金额:{}", userId, totalAmount);
return orderRepository.save(order);
}
/**
* 查询用户订单
*/
@Transactional(readOnly = true)
public List<Order> getOrdersByUserId(String userId) {
return orderRepository.findByUserId(userId);
}
/**
* 更新订单状态
*/
@Transactional
public Order updateOrderStatus(Long orderId, String newStatus) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在: " + orderId));
order.setStatus(newStatus);
return orderRepository.save(order);
}
}
第七步:控制器层
package com.example.myshop.controller;
import com.example.myshop.model.Order;
import com.example.myshop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController // 标注为REST控制器,返回JSON格式数据
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(request.getUserId(), request.getItems());
return ResponseEntity.ok(order);
}
@GetMapping("/user/{userId}")
public ResponseEntity<List<Order>> getOrdersByUser(@PathVariable String userId) {
return ResponseEntity.ok(orderService.getOrdersByUserId(userId));
}
@PutMapping("/{orderId}/status")
public ResponseEntity<Order> updateStatus(
@PathVariable Long orderId,
@RequestBody StatusUpdateRequest request) {
Order order = orderService.updateOrderStatus(orderId, request.getStatus());
return ResponseEntity.ok(order);
}
}
第八步:配置文件
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/my_shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 自动建表,开发环境用update,生产环境用validate
show-sql: true # 打印SQL语句,方便调试
properties:
hibernate:
format_sql: true
server:
port: 8080
第九步:启动并测试
# 启动项目
mvn spring-boot:run
# 测试创建订单
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{
"userId": "user001",
"items": [
{"productId": 1, "price": 99.9, "quantity": 2},
{"productId": 2, "price": 49.9, "quantity": 1}
]
}'
四、企业级项目的核心模块
一个完整的企业级Spring Boot项目,通常包含以下模块,我们逐个来看。
4.1 统一异常处理
生产环境的bug不可避免,关键是异常处理方式要统一。
package com.example.myshop.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
* 统一捕获所有异常,返回标准格式的JSON响应
*/
@Slf4j
@RestControllerAdvice // 全局异常处理注解
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException e) {
log.warn("业务异常:{}", e.getMessage());
Map<String, Object> result = new HashMap<>();
result.put("code", e.getCode());
result.put("message", e.getMessage());
result.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException e) {
Map<String, Object> result = new HashMap<>();
result.put("code", 400);
result.put("message", "参数校验失败");
result.put("errors", e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.toList());
result.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
/**
* 处理未知异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
log.error("系统异常:", e);
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("message", "系统内部错误,请联系管理员");
result.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
4.2 统一响应格式
package com.example.myshop.config;
import lombok.Data;
/**
* 统一API响应格式
*/
@Data
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private long timestamp;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("success");
response.setData(data);
response.setTimestamp(System.currentTimeMillis());
return response;
}
public static <T> ApiResponse<T> error(int code, String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(code);
response.setMessage(message);
response.setTimestamp(System.currentTimeMillis());
return response;
}
}
使用方式:
@GetMapping("/user/{userId}")
public ApiResponse<List<Order>> getOrders(@PathVariable String userId) {
List<Order> orders = orderService.getOrdersByUserId(userId);
return ApiResponse.success(orders);
}
4.3 跨域配置(CORS)
前后端分离项目必备:
package com.example.myshop.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*"); // 允许所有域名
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有HTTP方法
config.setAllowCredentials(true); // 允许携带Cookie
config.setMaxAge(3600L); // 预检请求缓存时间
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
4.4 拦截器:统一权限校验
package com.example.myshop.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
return false;
}
String actualToken = token.substring(7);
if (!tokenService.validateToken(actualToken)) {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\"}");
return false;
}
// 将用户信息存入request属性,后续Controller可以直接获取
String userId = tokenService.getUserIdFromToken(actualToken);
request.setAttribute("userId", userId);
return true;
}
}
注册拦截器:
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截所有API请求
.excludePathPatterns( // 排除不需要认证的接口
"/api/auth/login",
"/api/auth/register",
"/api/public/**"
);
}
}
4.5 日志配置
生产环境日志配置至关重要:
# application.yml
logging:
level:
root: INFO
com.example.myshop: DEBUG
org.springframework: WARN
org.hibernate: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file:
name: logs/myshop.log
max-size: 10MB
max-history: 30
// 在需要的地方使用日志
@Slf4j
@Service
public class OrderService {
public void createOrder(String userId, List<OrderItem> items) {
log.info("创建订单,用户:{},商品数量:{}", userId, items.size());
try {
// 业务逻辑
log.debug("订单创建成功,订单ID:{}", orderId);
} catch (Exception e) {
log.error("订单创建失败,用户:{},原因:{}", userId, e.getMessage(), e);
throw e;
}
}
}
五、常见坑与解决方案
作为过来人,我把开发过程中遇到的坑都整理出来了,希望帮你少走弯路。
5.1 循环依赖问题
Bean 'A' depends on Bean 'B', and Bean 'B' depends on Bean 'A'.
这是Spring中最常见的错误之一。解决方式有三种:
方案一:重构代码(推荐)
根本原因是设计问题,把公共的逻辑抽到一个新的Bean里:
// 原来的代码(循环依赖)
@Component
public class UserService {
@Autowired
private OrderService orderService;
}
@Component
public class OrderService {
@Autowired
private UserService userService;
}
// 重构后
@Component
public class UserService {
@Autowired
private OrderService orderService;
}
@Component
public class OrderService {
// 不依赖UserService,而是依赖一个独立的服务
@Autowired
private NotificationService notificationService;
}
@Component
public class NotificationService {
// 可以注入UserService
}
方案二:使用@Lazy延迟加载
@Component
public class UserService {
@Autowired
@Lazy // 延迟加载,打破循环依赖
private OrderService orderService;
}
方案三:用Setter注入替代构造器注入
@Component
public class UserService {
private OrderService orderService;
@Autowired
public void setOrderService(OrderService orderService) {
this.orderService = orderService;
}
}
5.2 N+1查询问题
这个问题在开发中非常常见,性能问题往往来自这里。
// 错误的做法:N+1查询
public List<Order> getAllOrdersWithUser() {
List<Order> orders = orderRepository.findAll(); // 1次查询,获取所有订单
for (Order order : orders) {
// 每个订单都单独查一次用户信息,共N次查询
User user = userRepository.findById(order.getUserId()).get();
order.setUser(user);
}
return orders;
}
解决方式一:JPQL JOIN查询
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.user")
List<Order> findAllWithUser();
}
解决方式二:Entity Graph
@Entity
@Table(name = "orders")
@EntityGraph(attributePaths = {"user"}) // 指定关联查询
public class Order {
// ...
}
// Repository中直接使用
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findAll(); // 自动JOIN FETCH user
}
解决方式三:批量查询
public List<Order> getAllOrdersWithUser() {
List<Order> orders = orderRepository.findAll();
// 先获取所有用户ID
List<String> userIds = orders.stream()
.map(Order::getUserId)
.distinct()
.toList();
// 一次查询所有用户
Map<String, User> userMap = userRepository.findByIdIn(userIds).stream()
.collect(Collectors.toMap(User::getId, u -> u));
// 填充到订单中
orders.forEach(order -> order.setUser(userMap.get(order.getUserId())));
return orders;
}
5.3 事务失效的坑
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
// 这个注解在某些情况下会失效!
@Transactional
public void createOrder(String userId, List<OrderItem> items) {
orderRepository.save(order);
inventoryService.deductInventory(items);
}
// 坑1:同一个类中方法调用,@Transactional失效
public void processOrder(String userId) {
createOrder(userId, items); // 直接调用,事务不生效!
}
// 坑2:异常被catch住了,事务不回滚
public void riskyOperation() {
try {
doSomething();
} catch (Exception e) {
log.error("出错了", e);
// 异常被吃掉,事务正常提交!
}
}
}
解决方案:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
/**
* 坑1解决:通过Spring容器获取自身的代理对象
*/
@Autowired
private OrderService self; // 注入自己
public void processOrder(String userId) {
self.createOrder(userId, items); // 通过代理调用,事务生效
}
/**
* 坑2解决:catch中重新抛出RuntimeException,确保事务回滚
*/
@Transactional(rollbackFor = Exception.class) // 指定所有异常都回滚
public void riskyOperation() {
try {
doSomething();
} catch (Exception e) {
log.error("出错了", e);
throw new RuntimeException("操作失败", e); // 重新抛出,触发回滚
}
}
/**
* 正确的做法:明确指定回滚策略
*/
@Transactional(
rollbackFor = {Exception.class},
noRollbackFor = {BusinessException.class} // 业务异常不回滚
)
public void createOrder(String userId, List<OrderItem> items) {
orderRepository.save(order);
inventoryService.deductInventory(items);
}
}
5.4 缓存穿透、缓存击穿、缓存雪崩
这是电商系统中最经典的问题:
缓存穿透:查询一个根本不存在的数据,缓存和数据库都查不到,每次请求都打到数据库。
// 解决方案:缓存空值
@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
Product product = productRepository.findById(id).orElse(null);
if (product == null) {
// 缓存空对象,设置较短的过期时间
return null;
}
return product;
}
缓存击穿:一个热点key过期瞬间,大量请求同时打到数据库。
// 解决方案:热点数据永不过期 + 分布式锁
@Cacheable(value = "hotProducts", key = "#id")
public Product getHotProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
// 或使用本地缓存Guava Cache
private final Cache<Long, Product> localCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
缓存雪崩:大量缓存同时过期,请求全部打到数据库。
// 解决方案:过期时间加随机值
@Cacheable(value = "products", key = "#id",
cacheManager = "redisCacheManager",
sync = true) // 防止缓存击穿
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
六、生产环境部署的关键配置
代码写好了,怎么稳定跑在生产环境?这些配置缺一不可。
6.1 多环境配置
# application.yml(公共配置)
spring:
profiles:
active: dev
jackson:
default-property-inclusion: non_null
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
---
# application-dev.yml(开发环境)
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:mysql://dev-db:3306/myshop_dev
username: dev_user
password: dev_password
logging:
level:
root: DEBUG
com.example: DEBUG
---
# application-prod.yml(生产环境)
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-db:3306/myshop_prod?useSSL=true&serverTimezone=Asia/Shanghai
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
jpa:
properties:
hibernate:
ddl-auto: validate # 生产环境用validate,禁止自动建表
show-sql: false
logging:
level:
root: INFO
com.example: INFO
6.2 健康检查与监控
package com.example.myshop.config;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try {
// 尝试获取数据库连接
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(3)) {
return Health.up()
.withDetail("database", "MySQL")
.withDetail("status", "connected")
.build();
}
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.withException(e)
.build();
}
return Health.down().build();
}
}
在application.yml中启用actuator:
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,beans
endpoint:
health:
show-details: always
show-components: always
七、学习路线建议
如果你是从零开始学Spring,我给你一个清晰的路径:
第一阶段:基础入门(1-2周)
- 理解Java基础:集合、泛型、反射、注解
- 理解Spring核心:IoC、DI、Bean生命周期
- 动手写几个小Demo:Hello World、简单CRUD
第二阶段:Spring Boot实战(2-4周)
- 掌握Spring Boot自动配置原理
- 学会使用Spring Data JPA
- 完成一个完整的CRUD项目
第三阶段:企业级开发(1-2个月)
- 学习Spring Security做权限控制
- 学习Redis缓存
- 学习消息队列(RabbitMQ/Kafka)
- 学习Docker部署
第四阶段:进阶优化(持续)
- JVM调优
- Spring源码阅读
- 微服务架构(Spring Cloud)
八、最后的建议
学Spring,最重要的是动手。看十篇文章不如自己写一遍代码,踩一遍坑。
你现在可能觉得概念很多,IoC、DI、AOP、事务、缓存……别急,这些东西你用的多了自然就理解了。我当年学Spring的时候,也是边学边忘,然后边用边记,慢慢地就融会贯通了。
记住这三句话:
- Spring帮你管理对象,你只管写业务
- 配置能少就少,约定优于配置
- 遇到问题先看日志,日志是最好的老师
祝你学习顺利!有任何问题随时来找我。
