由 spring 代理机制带来的一些麻烦

最近在写业务代码时,学会“充血模式”,这种模式很“面向对象”,结果发现我们团队尽然没人知道,或者说用过,不得不感慨现在 IT 入门门槛太低了点···

回过头来,为了说明我遇到的问题,先说明一下我是如何实现充血模式的。

1、充血模式的 Java bean

“一般来说”我们写的业务是这么实现的,controller -> service -> mapper ,业务简单的情况下,这么做没有太多问题,这么做实际是面向过程的一种编程方式,虽然我们把很多方法抽象成函数,但仍然改变不了面向过程的事实。

在最近的业务开发中,由于业务比较复杂,或者说我将业务进行了抽象,面向过程的模式已经不足以支持开发了。就有针对性的了解了一下领域驱动设计、充血模式,不得不说,面向对象的方式真香,基本上我的方法没有超过一个屏幕高度的,方法的复用性极强。

下面给出我的一个实例类:

@Component("node")
@Scope("prototype")
public class NodeDO {
	
	private final NodeMapper nodeMapper;

	@Autowired
	public NodeDO(NodeMapper nodeMapper) {
		this.nodeMapper = nodeMapper;
	}

	/**
	 * guava缓存
	 */
	private static final LoadingCache<String, NodeDO> NODE_DO_LOADING_CACHE = CacheBuilder.newBuilder()
			.maximumSize(1000)
			.recordState()
			.build(
					new CacheLoader<String, NodeDO>() {
						@Override
						public NodeDO load(String nodeId) {
							NodeDO node = SpringUtil.getBean(NodeDO.class);
							node.init(nodeId);
							return node;
						}
					}
			)

	public static NodeDO getInstance(String nodeId) {
		return NODE_DO_LOADING_CACHE.getUnchecked(nodeId);
	}

	public void init(String nodeId) {
		xxxx;
		NodeDTO nodeDTO = nodeMapper.selectByPrimaryKey(nodeId);
		xxxx;
	}
	
	public NodeDo copyForNewNode() {
	}

	public void validateConfig() {
	}

......

}

说明:

  • 这个类使用@Scope("prototype")将多例对象的创建交给 spring 管理,方便注入 mapper,service 等,但是对于多例,spring 并不会提供更多的管理。可以看到因为注入了 mapper 这个对象可以自己查询数据库初始化自己,而不是作为一个只存储数据的 pojo;
  • 由于这个类有较大的复用价值,故添加了 guava 作为 jvm 内缓存,将类生成的每个对象放入缓存中,这个缓存定义在类的静态域中,这样 静态方法 getInstance 也能从静态域的缓存中获取到。这个 guava 需要设置好缓存数量、过期策略等,我这因为设置了总量限制,故不设置过期,容量满时的清理策略使用默认的 LRU (最近最少使用)。
  • 有了缓存以后,其他类需要使用这个 Node 时,通过 getInstance(nodeId)来获取,缓存中有的,就直接返回,不需要再次查询数据库来初始化了,特别是这个类的初始化过程还比较复杂的情况下。

充血模式和领域驱动设计我的实践经验还不够,这里先讲到这,以后有更深刻的见解再分享。

2、Spring 事务实现方式

通过查询资料,大家可以知道spring 通过 AOP 使用动态代理实现事务,我们可以简单的通过添加注解@Transactional就可以实现事务,那么,我上面那个类的缓存,缓存的到底是代理对象还是对象本身呢?

通过调试、打断点,可以直观的看到,这个cache 缓存的是代理对象(可以看到 EnhanceBySpringCGLIB):
在这里插入图片描述

好,前面讲了那么多了,再把我的情况讲出来,大家也都能知道原因了。
我在使用缓存的代理对象时,调用了一个 private 方法 A,这个 A 方法中有用到注入的 mapper,见下图。可以看到,在代理对象中,这些实例域都是没有值的,全部初始化为 null,所以方法 A 使用 mapper 就会遇到空指针问题。
在这里插入图片描述
那么如何解决这个问题呢,也很简单,就是将 private 方法改成 public 方法,这样调用代理对象的 public 方法时,代理对象就会将方法委托给它代理的目标对象,方法就不会报错了。

3、再看事务不生效的情况

在使用 @Transactional的注解式事务,下面给出两个类,3 个方法,类 A 中的方法 aa 调用类 B 中的方法 bb,方法 bb 调用方法 cc,这种情况下,事务注解是不会生效的,原理同上面讲的一致。

class A {
	public void aa() {
		B b = new B();
		b.bb();
	}
}

class B {
	public void bb() {
		cc();
	}

	@Transactional
	public void cc() {
		doSomething();
	}
}

所以上面的情况想要事务生效,需要将注解加载方法 bb 上面。

4、小结

问题的本质不是缓存怎么设置、事务怎么配置,而是 spring 的事务是如何实现的,如何通过动态代理,为我们的普通类增强功能。理解原理才能帮助我们更好的解决问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值