You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Spring项目中践行《Effective Java》的建议?

嘿,我太懂这种拉扯感了!刚把《Effective Java》啃完,满脑子都是“不可变”“final字段”“禁用公开构造”的最佳实践,结果转头扎进Spring项目,就被一堆“必须要有默认构造”“类不能是final”的框架要求泼冷水,甚至怀疑是不是书里的内容在实际项目里根本不适用?别慌,这俩不是对立的,咱们一步步拆解平衡方案。

首先明确:《Effective Java》绝不是无用,它给的是底层原则,而非教条

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:14:02