Spring自动注入“失效”

探讨了Spring AOP导致的Bean成员变量访问问题及解决方案。由于Spring AOP基于动态代理,直接访问Bean成员变量可能导致null值。文章提供了两种解决方法:通过方法访问成员变量或使用AspectJ。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

spring的bean被织入切面后,直接访问他的成员变量,会得到null值。 最糟糕的是不会有任何报错,很难发现。
这个问题的真正原因是,spring的AOP基于动态代理实现,也就是说我们拿到的bean的class不是他本身的class,而是spring自动生成的一个代理对象。

//比如我们有这样一个类
public final class HelloMessage {
    //我们期望这个num被自动注入
    @Autowired
    public Integer num;

    public String getMessage(){
        return "hello world";
    }
}

它被织入了这样的切面

@Aspect
public class HelloSpyer {

    @Pointcut("execution(* hello.HelloMessage.getMessage(..))")
    public void getMessage(){}

    @Before("getMessage()")
    public void Before(){
        System.out.println("before");
    }
}

我们的bean是这样配置的

@Configuration
@ComponentScan(basePackages = {"hello"})
//这里会scan到HelloMessage,所以下边没有他的@Bean
@EnableAspectJAutoProxy
public class HelloConfig {
    //这里有一个Integer的Bean,按理来说他会被注入HelloClass
    @Bean 
    public Integer num(){
        return new Integer(100);
    }

    //这是切面
    @Bean
    public HelloSpyer helloSpyer(){
        return new HelloSpyer();
    }

}

main方法

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new    AnnotationConfigApplicationContext(HelloConfig.class);

        HelloMessage helloMessage = context.getBean(HelloMessage.class);

        System.out.println(helloMessage.num);
        System.out.println(helloMessage.getClass());
    }
}

运行之后,输出:
null
class hello.HelloMessage$$EnhancerBySpringCGLIB$$16ce769e

结果,没有报错,但是num却是null,而且我们发现helloMessge也变成了奇怪的类。具体来说是 CGLIB生成的动态代理。所以访问代理的变量当然是无效的啦。

解决办法也比较简单:

public final class HelloMessage {
    //我们期望这个num被自动注入
    @Autowired
    public Integer num;

    public String getMessage(){
        return "hello world";
    }
    //增加一个get方法,hh
    public Integer getNum() {
        return num;
    }
}

main改为

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new    AnnotationConfigApplicationContext(HelloConfig.class);

        HelloMessage helloMessage = context.getBean(HelloMessage.class);

        //这里调用方法获得num
        System.out.println(helloMessage.getNum());
    }
}

输出:
100
得到了正确的结果。
这告诉我们:
1,不要随便违反迪米特法则,尽量通过接口访问对象
2,spring提倡针对接口编程是有理由的 (hhh)
3,在spring的AOP中,某些有关类型的操作是不可靠的,如类型检查。比如某个业务逻辑要判断

HelloMessage.class == helloMessage.getClass()

会因为spring AOP而得到意想不到的结果
不过

helloMessage instanceof HelloMessage
HelloMessage.class.isInstance(helloMessage)

这两种判断是安全的,代理类仍然可以安全转型为其原始类。

另一种解决办法是使用 AspectJ 提供的 AOP ,他提供了一种“编译时增强”的AOP框架,也就是他会修改你的字节码,从而避免了代理类可能带来的错误。而spring的AOP是“运行时增强”。然而大多数时候spring的AOP已经能满足我们的需求了。

### Spring 框架中事务失效的具体场景及解决方案 #### 方法可见性不当 在 Spring 中,默认使用 JDK 动态代理实现 AOP,而动态代理仅能拦截公共方法的调用。如果将带有 `@Transactional` 注解的方法设置为非公有访问权限(如 private、protected 或包级私有),则事务管理器无法对该方法应用事务逻辑[^1]。 **解决方案** 确保所有需要事务支持的方法均为 `public` 访问级别。 --- #### 异常未正确传播 Spring 声明式事务默认只会在遇到运行时异常 (`RuntimeException`) 或错误 (`Error`) 时触发回滚操作。如果方法抛出的是受检异常(Checked Exception),除非显式配置 `<tx:method>` 属性或通过注解指定 `rollbackFor` 参数,否则不会自动回滚事务[^2]。 **解决方案** 可以通过如下方式调整事务行为: ```java @Transactional(rollbackFor = {CustomException.class}) public void someServiceMethod() { // 业务逻辑代码 } ``` --- #### 异常被捕获而不重新抛出 当方法内部捕获了异常但并未将其重新抛出时,Spring 的事务管理器认为该方法正常结束,从而不会执行回滚操作。这是因为在事务管理器看来,方法返回即表示成功完成[^2]。 **解决方案** 避免在服务层方法中完全捕获异常,或将捕获后的异常重新抛出以便通知事务管理器进行回滚处理: ```java @Transactional public void someServiceMethod() throws CustomException { try { if (someCondition) { throw new CustomException("Something went wrong"); } } catch (CustomException e) { log.error("An error occurred", e); throw e; // 重新抛出异常以触发事务回滚 } } ``` --- #### 自身方法调用绕过代理 在一个类内部直接调用另一个标注了 `@Transactional` 的方法时,由于调用发生在同一个对象实例内而非经过代理对象,因此事务上下文并不会生效[^1]。 **解决方案** 推荐通过自我注入的方式获取当前 Bean 的代理实例来进行跨方法调用: ```java @Service public class MyService { @Autowired private MyService self; public void outerMethod() { self.innerMethod(); // 调用代理实例上的 innerMethod() } @Transactional public void innerMethod() { // 此处事务已生效 } } ``` --- #### 不恰当的事务传播行为 不同的事务传播行为会影响事务的创建与加入策略。例如,`REQUIRES_NEW` 会强制开启新的独立事务,但如果父事务失败,子事务的结果可能仍然提交;反之,`NESTED` 则允许嵌套事务随外部事务一同回滚。 **解决方案** 根据实际需求合理选择事务传播属性。以下是常见选项及其适用场景: | 传播行为 | 描述 | |----------------|----------------------------------------------------------------------| | REQUIRED | 默认,若有现有事务则加入其中,无则新建事务 | | REQUIRES_NEW | 总是启动新事务,原有事务挂起 | | SUPPORTS | 若存在事务则参与,不存在也不创建 | | NOT_SUPPORTED | 完全不支持事务 | --- #### 数据库连接池配置问题 某些数据库驱动程序或连接池可能存在特殊限制,导致即使事务配置正确也无法按预期工作。例如,部分 JDBC 驱动对隔离级别的支持有限,或者连接池未能正确传递事务上下文信息。 **解决方案** 检查使用的数据库驱动版本是否最新,并验证连接池的相关参数配置是否符合要求。必要时启用调试日志排查潜在问题。 --- #### 缺乏正确的事务同步机制 在分布式环境中,单体系统的本地事务难以覆盖多个资源(如不同数据库或消息队列)。此时需引入全局事务协调工具(如 XA 协议)或其他补偿方案来保障一致性。 **解决方案** 采用分布式事务管理框架(如 Seata、Atomikos)统一管控各参与者之间的交互流程,确保整体事务的一致性。 --- ### 示例代码综合演示 以下示例展示了如何针对上述几种典型失效场景设计健壮的服务方法: ```java @Service public class TransactionalService { @Transactional(propagation = Propagation.REQUIRED, rollbackFor = RuntimeException.class) public void performTransaction() { saveDataToDatabase(); sendMessageToQueue(); } @Transactional protected void saveDataToDatabase() { // 执行保存数据的操作 } private void sendMessageToQueue() { try { // 发送消息至队列 } catch (Exception e) { log.error("Failed to send message", e); throw new RuntimeException(e); // 重新抛出异常以触发回滚 } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值