如何在Spring项目中践行《Effective Java》的建议?
嘿,我太懂这种拉扯感了!刚把《Effective Java》啃完,满脑子都是“不可变”“final字段”“禁用公开构造”的最佳实践,结果转头扎进Spring项目,就被一堆“必须要有默认构造”“类不能是final”的框架要求泼冷水,甚至怀疑是不是书里的内容在实际项目里根本不适用?别慌,这俩不是对立的,咱们一步步拆解平衡方案。
Bloch的所有建议核心都是减少意外副作用、提升代码稳定性、降低长期维护成本,而Spring的要求是框架运行时的技术约束——两者的出发点不同,但完全可以找到适配方式,不用非此即彼。
1. JPA实体类:不可变性 vs 默认构造要求
JPA需要默认构造方法是为了通过反射实例化对象,但它不要求这个构造方法是public,我们可以用以下方式兼顾:
- 给实体类的业务字段加上
final修饰,用带参构造方法初始化核心状态 - 保留一个protected/private级别的无参构造方法,可以用Lombok简化:
@NoArgsConstructor(access = AccessLevel.PROTECTED)+@AllArgsConstructor - 尽量避免公开setter方法,如果需要更新实体状态,用建造者模式或者仅提供业务语义明确的更新方法(比如
updateUserEmail(String newEmail)而非通用的setEmail) - 如果用Spring Data,还可以给实体类加
@Immutable注解,从框架层面约束不可变性(部分JPA实现也支持)
示例代码:
@Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Getter // 只提供getter,无setter public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // JPA主键通常需要可变,这是合理的例外 private final String username; private final String email; // 业务语义明确的更新方法,而非通用setter public User updateEmail(String newEmail) { return new User(this.id, this.username, newEmail); } }
2. 服务类:final类/方法 vs Mock、AOP要求
这里的核心思路是区分业务核心逻辑和框架适配层:
方案一:用接口+JDK动态代理
为服务类定义接口,实现类可以设为final——因为Spring的JDK动态代理是基于接口的,代理对象会实现接口,完全不影响AOP增强和Mock(直接Mock接口即可)。这样既遵循了Bloch“用final防止继承滥用”的建议,又满足框架需求。
示例:
// 接口定义服务契约 public interface UserService { void createUser(User user); } // 业务实现类,完全遵循Effective Java建议 @Service public final class UserServiceImpl implements UserService { private final UserRepository userRepository; // 依赖注入构造方法,无默认构造 public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public void createUser(User user) { // 核心业务逻辑 userRepository.save(user); } }
方案二:升级Mock工具,支持final类
如果你不想用接口,Mockito 2.1+已经支持Mock final类和方法了,只需要做个简单配置:
在测试资源目录下创建src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker,文件内容写mock-maker-inline,之后就可以直接Mock final类,不用为了测试把类改成非final。
方案三:核心逻辑封装成final类,外层用非final适配类
把业务核心逻辑放到一个final的“核心类”里,然后用一个非final的Spring Bean类包装它,适配AOP和框架需求:
// 核心业务逻辑,完全遵循Effective Java建议 public final class UserServiceCore { private final UserRepository userRepository; public UserServiceCore(UserRepository userRepository) { this.userRepository = userRepository; } public void createUser(User user) { // 核心逻辑 userRepository.save(user); } } // Spring Bean适配类,非final,用于AOP和框架集成 @Service public class UserService { private final UserServiceCore core; public UserService(UserServiceCore core) { this.core = core; } @Transactional // AOP注解加在适配层 public void createUser(User user) { core.createUser(user); } }
方案四:用AspectJ编译期织入
如果你的项目用AspectJ而不是Spring默认的动态代理,编译期织入可以直接修改字节码增强类,即使类是final也能生效——这样既能保留final类的稳定性,又能实现切面增强。
最后想强调:不要把《Effective Java》的建议当成“必须100%遵守的铁律”,它教你的是如何思考代码设计的合理性——比如“降低可变性”是为了避免多线程下的意外状态修改,“用final类”是为了防止子类随意破坏父类的约定。
而Spring的要求是特定技术栈下的妥协,我们要做的是:
- 在业务核心逻辑层尽量遵循这些原则,因为这部分是代码的灵魂,稳定性最重要
- 在框架适配层(比如Spring Bean包装类、接口实现)灵活调整,满足框架需求,这部分是基础设施,牺牲一点原则换框架的便利性是值得的
内容的提问来源于stack exchange,提问作者Pasha

