最近在写业务代码时,学会“充血模式”,这种模式很“面向对象”,结果发现我们团队尽然没人知道,或者说用过,不得不感慨现在 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 的事务是如何实现的,如何通过动态代理,为我们的普通类增强功能。理解原理才能帮助我们更好的解决问题。