从互联网大厂到初创公司Spring框架实战学习指南Java开发入门与进阶全攻略
你好呀,看到这个标题我就知道你在想什么了。是不是刚入职大厂被满屏的Spring配置搞懵过?或者现在在初创公司一个人扛着整个后端,发现Spring根本不够用?别慌,这篇文章就是给你写的。我干了这么多年Java,从阿里、腾讯这种大厂出来,也待过十几人的初创团队,今天把这些年的坑和干货全都摊开来讲。
咱们不整那些教科书式的”第一章、第二章”,就按实际开发中你会遇到的真实场景来聊。
先搞懂一件事:为什么是Spring?
你可能会问,现在微服务满天飞,Spring Cloud都2023年了还在用吗?答案是:还在用,而且用得挺猛。但在此之前,你得先把Spring Boot玩明白。
我见过太多刚毕业的开发者,上来就啃Spring Cloud,结果连依赖注入是什么意思都不知道,配置XML和注解混着用,排查问题能排查到凌晨三点。这就像你还没学会走路就要跑马拉松,摔倒是迟早的事。
先给你看我刚入门时踩过的第一个坑:
// 错误示范:直接在Controller里new对象
public class UserController {
private UserService userService = new UserServiceImpl(); // 这样写是错的
public User getUserById(Long id) {
return userService.findById(id);
}
}
这种写法看着没问题,但一上生产环境就完蛋。为什么呢?因为UserService被new出来之后,你就没法用Spring的依赖注入、事务管理、单例模式这些核心特性了。每次请求都创建一个新实例,浪费资源不说,还可能导致数据不一致。
正确做法是这样的:
@Component
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("用户不存在"));
}
}
看到区别了吗?@Component让Spring管理这个类,@Autowired自动注入依赖,@Transactional开启事务管理。这才是Spring的正确打开方式。
大厂和初创公司用Spring有啥不一样?
这个问题很关键,因为两边的使用习惯和侧重点完全不一样。
在大厂,你看到的是Spring Boot + Spring Cloud + 各种自研框架的组合。项目动辄几百个模块,配置中心用Nacos或Apollo,服务注册发现用Eureka或Consul,网关用Spring Cloud Gateway。代码审查(Code Review)极其严格,每一行代码都要符合团队规范。
我在阿里待那会儿,新接手一个项目光是看Spring配置文件就能看半天。XML配置虽然大部分已经迁移到注解了,但有些老系统还在用,而且团队有自己的starter封装,你得花不少时间理解这些内部框架。
初创公司就完全不一样了。我加入过一个五人的小团队,服务器就两台,Spring Boot项目直接打成jar包部署,连Docker都懒得用。这时候你看不到什么微服务架构,就是一个单体应用,能跑就行。
但初创公司也有自己的坑:技术选型随意、代码规范缺失、没有Code Review、文档基本靠口口相传。这些都会影响你对Spring的理解。
所以我建议的学习路径是:先在个人项目里把Spring Boot用熟,然后有大厂经验的同学可以把微服务那一套也了解清楚,创业型公司的开发者则需要学会如何用最少的基础设施把事情做成。
Spring IoC和DI:这两个概念必须搞透
IoC(控制反转)和DI(依赖注入)是Spring最核心的两个概念,但也是初学者最容易混淆的地方。
先说IoC。什么叫控制反转?简单来说,以前是你自己new对象,现在让Spring来new对象。你不需要关心对象怎么创建、怎么销毁,这些都交给Spring容器来管理了。这个”控制”反转到了Spring手里,所以叫控制反转。
再来说DI。依赖注入是IoC的一种实现方式。你不需要手动去获取依赖的对象,Spring会自动帮你把依赖注入进来。有几种注入方式:
// 方式一:构造器注入(推荐,最适合测试和不可变对象)
@Component
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
@Autowired
public OrderService(PaymentService paymentService,
InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
// 方式二:setter注入(适合可选依赖)
@Component
public class NotificationService {
private EmailService emailService;
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}
// 方式三:字段注入(最简洁,但不推荐在大项目中使用)
@Component
public class ReportService {
@Autowired
private DatabaseService databaseService;
}
大厂一般推荐构造器注入,因为这样依赖关系一目了然,而且对象一旦创建就不能再修改,符合不可变对象的编程思想。小团队里字段注入用得多,因为写起来快,但要注意这种写法会让单元测试变得困难。
Spring MVC:Web开发的基石
不管是大厂还是小公司,Spring MVC都是处理HTTP请求的标配。很多开发者觉得Spring MVC没什么好学的,不就是写几个@Controller和@RequestMapping吗?
但真正用起来的时候,坑还是挺多的。
比如参数绑定,你以为GET请求的参数和POST请求的参数处理方式一样?其实不一样:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// GET请求,参数直接绑到方法上
@GetMapping("/{orderId}")
public Order getOrder(@PathVariable Long orderId,
@RequestParam(defaultValue = "false") boolean includeDetails) {
return orderService.getOrder(orderId, includeDetails);
}
// POST请求,参数绑到对象上
@PostMapping
public Order createOrder(@Valid @RequestBody CreateOrderRequest request,
@RequestHeader("X-Request-Id") String requestId) {
return orderService.createOrder(request, requestId);
}
}
// 参数对象
public class CreateOrderRequest {
@NotNull
private Long productId;
@Min(1)
private Integer quantity;
@NotNull
private String address;
// getter和setter省略
}
很多人忽略了@Valid注解,导致脏数据直接进入业务逻辑。我见过一个大厂的线上故障,就是因为缺少参数校验,一个用户传了quantity = -999999,结果扣了仓库几万件货,库存直接爆掉。
再说说全局异常处理。大厂的规范是统一异常处理,小团队经常各写各的:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ApiResponse<Void> handleUserNotFound(UserNotFoundException e) {
return ApiResponse.error(404, e.getMessage());
}
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e) {
return ApiResponse.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
// 生产环境不要把详细异常信息返回给客户端
log.error("系统异常", e);
return ApiResponse.error(500, "服务器内部错误");
}
}
@Data
@AllArgsConstructor
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> error(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
这种写法的好处是,Controller里不需要到处写try-catch,业务代码保持干净,而且所有异常都能被统一捕获和处理。大厂的标准API响应格式几乎都是这个套路。
Spring Data JPA和MyBatis之争
这是Java后端开发者绕不开的话题。Spring Data JPA和MyBatis都有人用,大厂小厂都有,选哪个?
先说结论:没有绝对的对错,只有适不适合。
Spring Data JPA的好处是开发速度快,写个Repository继承JpaRepository就能用,基本不需要写SQL。适合CRUD为主的业务,比如后台管理系统、内容管理系统这些。
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByStatusAndRole(String status, String role);
// 支持方法名查询,Spring会帮你生成SQL
List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}
但JPA也有明显的缺点:复杂查询很难写,性能优化空间小,数据库方言适配麻烦。我在大厂做用户增长项目时,就遇到过一条SQL优化了半个月,JPA生成的SQL根本没法直接优化。
MyBatis就更灵活,SQL自己写,性能可控,但代码量大。小团队常用MyBatis,因为上手快,改SQL方便。
@Mapper
public interface UserMapper {
User findById(@Param("id") Long id);
List<User> findByCondition(@Param("name") String name,
@Param("status") Integer status);
int insert(User user);
int update(User user);
}
对应的XML:
<mapper namespace="com.example.mapper.UserMapper">
<select id="findById" resultType="com.example.model.User">
SELECT * FROM t_user WHERE id = #{id}
</select>
<select id="findByCondition" resultType="com.example.model.User">
SELECT * FROM t_user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY create_time DESC
</select>
</mapper>
我的建议是:个人项目用JPA快速上手,工作后根据公司技术栈来。如果公司用MyBatis就好好学MyBatis,用JPA就把JPA玩透。不要自己在那边纠结哪个更好,每个框架都有它适用的场景。
Spring Boot自动配置:揭秘Spring Boot的魔法
很多人觉得Spring Boot很神奇,引入一个starter就能用,配置也不用写多少。但你知道这背后是怎么实现的吗?
Spring Boot的自动配置核心就三个东西:@SpringBootApplication、spring.factories(Spring Boot 2.x)和AutoConfiguration.imports(Spring Boot 3.x)。
@SpringBootApplication注解里包含了@EnableAutoConfiguration,这个注解会触发自动配置的加载:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// @SpringBootApplication源码简化版
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@ComponentScan
@EnableAutoConfiguration // 自动配置的核心
public @interface SpringBootApplication {
// ...
}
@EnableAutoConfiguration会读取META-INF/spring.factories(Spring Boot 2.x)或者META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)文件,找到所有要自动配置的类:
# spring.factories (Spring Boot 2.x)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfigure.ExampleAutoConfiguration
Spring Boot会根据@Conditional注解判断是否加载这个自动配置类:
@Configuration
@ConditionalOnClass({DataSource.class}) // 类路径下有DataSource才生效
@ConditionalOnProperty(prefix = "example.datasource", name = "enabled", havingValue = "true")
public class ExampleAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户没有自己定义Bean时才创建
public DataSource dataSource() {
// 创建数据源
}
}
掌握这些原理后,你就能理解为什么有时候Spring Boot的自动配置不生效了。比如你引入了spring-boot-starter-data-jpa,但数据源配置不对,JPA的自动配置就不会生效,因为@ConditionalOnClass和@ConditionalOnBean这些条件没满足。
在实际开发中,你经常会遇到这种情况:某个starter引入了,但相关的Bean没有注入成功。这时候就要学会看Spring Boot的启动日志,里面有一大段CONDITIONS EVALUATION REPORT,告诉你哪些自动配置类被加载了,哪些被跳过了,跳过的原因是什么。
实战:搭建一个完整的Spring Boot项目
光说不练假把式,我给你搭一个实际的项目结构,涵盖大厂和初创公司常用的技术栈:
spring-boot-demo/
├── src/main/java/com/example/demo/
│ ├── DemoApplication.java
│ ├── config/
│ │ ├── WebConfig.java # Web相关配置
│ │ ├── MybatisConfig.java # MyBatis配置
│ │ └── RedisConfig.java # Redis配置
│ ├── controller/
│ │ ├── UserController.java
│ │ └── OrderController.java
│ ├── service/
│ │ ├── UserService.java
│ │ └── OrderService.java
│ ├── dao/
│ │ └── UserRepository.java
│ ├── entity/
│ │ └── User.java
│ ├── dto/
│ │ ├── CreateUserRequest.java
│ │ └── UserResponse.java
│ ├── exception/
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ ├── interceptor/
│ │ └── AuthInterceptor.java
│ └── util/
│ └── ResponseUtil.java
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ ├── logback-spring.xml
│ └── mapper/
│ └── UserMapper.xml
└── pom.xml
application.yml配置:
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: spring-boot-demo
profiles:
active: dev
datasource:
url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
default-property-inclusion: non_null
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
redis:
host: localhost
port: 6379
database: 0
password: ${REDIS_PASSWORD}
核心Controller:
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor # 用构造器注入,替代@Autowired
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public ApiResponse<UserResponse> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ApiResponse.success(UserResponse.from(user));
}
@PostMapping
public ApiResponse<Long> createUser(@Valid @RequestBody CreateUserRequest request) {
Long userId = userService.create(request);
return ApiResponse.success(userId);
}
@PutMapping("/{id}")
public ApiResponse<Void> updateUser(@PathVariable Long id,
@Valid @RequestBody CreateUserRequest request) {
userService.update(id, request);
return ApiResponse.success(null);
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ApiResponse.success(null);
}
}
Service层:
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final RedisTemplate<String, Object> redisTemplate;
@Transactional(rollbackFor = Exception.class)
public Long create(CreateUserRequest request) {
User user = User.builder()
.name(request.getName())
.email(request.getEmail())
.phone(request.getPhone())
.status(1)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
userRepository.insert(user);
// 写入缓存
String cacheKey = "user:" + user.getId();
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
log.info("创建用户成功, userId: {}", user.getId());
return user.getId();
}
public User findById(Long id) {
// 先查缓存
String cacheKey = "user:" + id;
User cachedUser = (User) redisTemplate.opsForValue().get(cacheKey);
if (cachedUser != null) {
return cachedUser;
}
// 缓存未命中,查数据库
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException("用户不存在: " + id);
}
// 写入缓存
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
return user;
}
@Transactional(rollbackFor = Exception.class)
public void update(Long id, CreateUserRequest request) {
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException("用户不存在: " + id);
}
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setUpdatedAt(LocalDateTime.now());
userRepository.update(user);
// 更新缓存
String cacheKey = "user:" + id;
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
log.info("更新用户成功, userId: {}", id);
}
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException("用户不存在: " + id);
}
userRepository.delete(id);
// 删除缓存
String cacheKey = "user:" + id;
redisTemplate.delete(cacheKey);
log.info("删除用户成功, userId: {}", id);
}
}
这个项目结构覆盖了Spring Boot最常用的功能:RESTful API、参数校验、事务管理、缓存、异常处理、日志。你可以在这个基础上继续扩展。
Spring Boot测试:大厂和初创公司的不同侧重
大厂一般会有比较完善的测试体系,单元测试、集成测试、端到端测试都有人负责。初创公司可能就只有一个开发者,测试基本靠手动,这其实挺危险的。
我给你写几个常用的测试案例:
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
// 每个测试前清空数据
userRepository.deleteAll();
}
@Test
void testCreateUser_Success() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setName("张三");
request.setEmail("zhangsan@example.com");
request.setPhone("13800138000");
mockMvc.perform(MockMvcRequestBuilders
.post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isNotEmpty());
}
@Test
void testCreateUser_InvalidEmail() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setName("张三");
request.setEmail("invalid-email"); // 无效的邮箱格式
request.setPhone("13800138000");
mockMvc.perform(MockMvcRequestBuilders
.post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(400));
}
@Test
void testGetUser_NotFound() throws Exception {
mockMvc.perform(MockMvcRequestBuilders
.get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(404));
}
@Test
@Transactional // 测试完成后回滚,不污染数据库
void testIntegration_UserFlow() {
// 创建用户
CreateUserRequest request = new CreateUserRequest();
request.setName("李四");
request.setEmail("lisi@example.com");
request.setPhone("13900139000");
Long userId = userService.create(request);
assertNotNull(userId);
// 查询用户
User user = userService.findById(userId);
assertEquals("李四", user.getName());
// 更新用户
CreateUserRequest updateRequest = new CreateUserRequest();
updateRequest.setName("李四_修改");
updateRequest.setEmail("lisi_new@example.com");
updateRequest.setPhone("13900139000");
userService.update(userId, updateRequest);
User updatedUser = userService.findById(userId);
assertEquals("李四_修改", updatedUser.getName());
// 删除用户
userService.delete(userId);
assertThrows(UserNotFoundException.class, () -> userService.findById(userId));
}
}
大厂的测试会写得更细,包括边界条件测试、并发测试、性能测试等。小公司如果条件允许,至少要写核心业务逻辑的单元测试和集成测试,这是保证代码质量的基本手段。
Spring Boot在生产环境的使用注意事项
不管大厂小厂,项目上线后都会遇到各种问题。我总结了几个常见的坑:
一、日志级别要配好
开发环境可以开DEBUG,生产环境一定要用INFO或WARN。Spring Boot提供了非常灵活的日志配置:
logging:
level:
root: INFO
com.example.demo: DEBUG
org.springframework.web: INFO
org.mybatis: DEBUG
file:
name: logs/app.log
max-size: 100MB
max-history: 30
二、配置文件安全管理
大厂有专门的密钥管理服务(比如阿里的KMS、AWS的Secrets Manager),小公司可能用环境变量或者加密的配置中心。但无论如何,密码不能硬编码在代码里:
# 错误示范
spring:
datasource:
password: 123456 # 绝对不能这样写
# 正确示范
spring:
datasource:
password: ${DB_PASSWORD} # 从环境变量读取
三、JVM参数调优
生产环境的JVM参数需要根据实际内存和并发情况调整:
java -jar \
-Xms512m -Xmx1024m \
-XX:MetaspaceSize=128m \
-XX:MaxMetaspaceSize=256m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=logs/heapdump.hprof \
demo.jar
四、健康检查和监控
Spring Boot Actuator提供了丰富的健康检查端点:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
生产环境记得把这些端点做好权限控制,不要暴露给外部访问。
Spring Cloud:大厂必备,初创公司按需
如果你在大厂工作,Spring Cloud大概率是跑不了的。Spring Cloud提供了一套完整的微服务解决方案:
Spring Cloud Netflix (已停止维护,逐步迁移到Spring Cloud Alibaba)
├── Spring Cloud Gateway (网关)
├── Spring Cloud OpenFeign (服务调用)
├── Spring Cloud Config (配置中心)
├── Spring Cloud CircuitBreaker (熔断降级)
└── Spring Cloud Sleuth (链路追踪)
Spring Cloud Alibaba
├── Nacos (服务注册发现 + 配置中心)
├── Sentinel (流量控制和熔断降级)
├── Seata (分布式事务)
└── RocketMQ (消息队列)
一个简单的微服务调用:
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("/api/users/{id}")
ApiResponse<UserResponse> getUser(@PathVariable("id") Long id);
@PostMapping("/api/users")
ApiResponse<Long> createUser(@RequestBody CreateUserRequest request);
}
@Component
public class UserClientFallback implements UserClient {
@Override
public ApiResponse<UserResponse> getUser(Long id) {
return ApiResponse.error(503, "用户服务暂时不可用");
}
@Override
public ApiResponse<Long> createUser(CreateUserRequest request) {
return ApiResponse.error(503, "用户服务暂时不可用");
}
}
但我要提醒你,Spring Cloud的学习曲线比较陡峭。如果没有大规模分布式系统的实战需求,不必急着上。很多初创公司用单体架构+数据库主从+缓存就足够了,等真正需要微服务的时候再学不迟。
学习建议:如何高效掌握Spring框架
我见过太多开发者学Spring学得很痛苦,原因多半是学习方法不对。给你几条实际可行的建议:
一、先搞懂原理,再追新框架
不要一上来就追Spring Boot 3.x、Spring Cloud 2023.0这些新东西。先把Spring Framework本身的IoC容器、AOP、MVC这些核心原理搞清楚。理解了ApplicationContext是怎么工作的,Spring Boot的自动配置你就很容易理解了。
二、动手写,别只看
Spring是一个框架,不是理论。光看文档不写代码,你永远无法真正掌握。我建议你跟着我上面给的代码示例,亲手搭一个项目跑起来。哪怕只是一个简单的Hello World,也比看十篇文章有用。
三、学会看源码
当你有一定基础后,尝试去读Spring的源码。不用全读,先读你常用的那几个类的实现。比如你用了@Autowired,就去看看AutowiredAnnotationBeanPostProcessor是怎么工作的。读源码不是为了装X,而是为了理解框架的设计思想,这样你遇到问题才能自己排查。
四、了解大厂和小厂的不同场景
如果你在准备大厂面试,重点要把Spring的原理讲清楚,包括Bean的生命周期、循环依赖解决、事务传播机制等。如果你在大厂工作,要注意代码规范和团队协作。如果你在初创公司,要学会快速上手各种技术,同时注意代码的可维护性。
五、不要忽视基础
Spring再强大,底层还是Java。Java的集合、并发、IO、网络这些基础不牢,学Spring会非常吃力。我建议先把《Java核心技术卷一》和《Java并发编程实战》啃一遍,然后再来学Spring会事半功倍。
一些真实的踩坑经历分享
说了这么多理论和代码,最后分享几个我实际工作中踩过的坑,这些都是花钱买来的经验。
坑一:NPE(空指针异常)的大杀器
有一次线上系统突然报大量NPE,排查了半天发现是Spring的@Autowired注入失败。原因是某个Bean在初始化时依赖的另一个Bean还没有创建完成,导致注入了null。解决办法是加上@Lazy注解,让Spring延迟加载:
@Service
public class OrderService {
// 加上@Lazy避免循环依赖问题
@Autowired
@Lazy
private UserService userService;
}
坑二:事务不生效的诡异问题
Spring的@Transactional注解在某些情况下不会生效,最常见的原因是:
- 方法不是public的
- 在同一个类内部调用带
@Transactional的方法(自调用问题) - 异常类型不对(默认只回滚RuntimeException和Error)
解决办法是引入专门的Service层,把事务注解放在Service上,并且用AOP代理的方式调用:
@Service
public class OrderTransactionService {
@Autowired
private OrderService orderService;
@Autowired
private InventoryService inventoryService;
@Transactional(rollbackFor = Exception.class)
public void createOrderWithInventory(Long orderId, Long productId, Integer quantity) {
// 通过代理对象调用,确保事务生效
orderService.createOrder(orderId, productId);
inventoryService.decreaseStock(productId, quantity);
}
}
坑三:Spring Boot热部署失效
开发时用的DevTools热部署,突然有一天不生效了。排查后发现是因为IDE的自动编译关闭了。解决方法是在IDE设置里开启”Build project automatically”,然后按Ctrl+F9(Windows)或Cmd+F9(Mac)触发重新编译。
坑四:JSON序列化日期格式问题
Spring Boot默认用Jackson序列化JSON,日期格式是时间戳。如果前端需要特定格式的日期字符串,需要在配置里设置:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
或者在实体类字段上加注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private LocalDateTime createdAt;
最后的话
学Spring框架这条路,我走了大概七年。从最初看《Spring实战》看得一头雾水,到现在能给别人讲明白Spring的原理,中间踩过无数的坑,也收获了很多成就感。
如果你现在刚入门,不要太焦虑。Spring的生态确实庞大,但核心就那几个东西:IoC容器、AOP、MVC、Data。把这四个吃透了,其他的微服务、云原生、响应式都是在此基础上构建的。
我在大厂见过很多技术很好的同事,他们不一定是Spring用得最熟练的,但一定是基础最扎实的,理解最深刻的。所以不要急于求成,一步步来,把每个概念都搞懂,把每行代码都理解透。
希望这篇文章能帮到你。如果有什么不懂的,欢迎随时来问我。学习路上有人一起交流,会走得更远。
