我们工作的大部分时间都在写业务代码,如何写好业务代码必然是我们追求的一大目标,在编程方面,简单、易懂、可扩展性是衡量代码质量的通用标准,所以在工作中,我们能用java将产品经理的想法表达出来还不够,我们产出的内容最好还能让其它的工程师一目了然。
本文以创建订单这个业务场景着手,提供一种通用完成创建和编辑业务对象的模式。此业务发起是从前端的一个post请求,就假设为 /api/v1/order
,后端通过 OrderController
控制层接口,然后由 OrderService
处理相关的业务逻辑,再通过OrderRepository
持久层入库,请求处理完之后返回给前端一个orderUuid
,这是一种很常见的业务:
对于Controller
层,大多数情况下只用于构建参数和VO,以及及少量的参数校验(例如非空校验,格式校验等),多数的校验包含业务性的,有时需要与其它的微服务进行交互,我们通常会将这些校验移到service
层,repository
层只用于数据持久化,这里不做过多讨论,变化后的流程图如下:
流程图一完成,业务代码轻车熟路,仅看service层的代码实现,省略其它
@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Transactionalpublic OrderDTO createOrder(OrderCreateRequest request) {// 校验1:用户是否有权限checkoutUserAuthorization(request);// 校验2:产品是否还有库存, 产品状态等checkoutProduct(request);final Order save = orderRepository.save();// 发mq消息给其它系统sendMqMessage(save);// 发邮件// sendEmail();// 记录操作日志// logService.saveLog()...return convert(save);}private void checkoutUserAuthorization(OrderCreateRequest request) {// ....查 userService等等}private void checkoutProduct(OrderCreateRequest request) {// ...查 productService,处理业务}private void sendMqMessage(Order order) {// 调mqService发消息}private OrderDTO convert(Order save) {OrderDTO dto = new OrderDTO();BeanUtils.copyProperties(save, dto);return dto;}
}
可能绝大多数工程师能够熟练并且无误地完成以上的代码编写工作,而这也是我们最常用的设计模式(MVC设计模式),当然也是最简单且和最高效的编码方式。那么到现在停下来想一想,这样写的代码有什么问题?
我能想到以下问题:
- 如果是简单的业务,这段代码没有什么问题,也值得推荐。但订单业务是很复杂的,一是校验可能很多很严格;二与其交互的微服务或第三方系统也不少;三是可能根据订单的种类,有不同的校验方法,那么代码中就会有很多
if ... else
。 - 如果业务非常复杂,这项工作要由多人共同完成,那么大家都会到这个类中编写代码,
OrderService
就成了一个极其臃肿的类,很快代码就超过五六百行(大家可以想想自己平时见到一个类代码有五百行时的感受)。 - 性能问题,假设在校验的时候通过调用
userService
查了当前用户信息,校验完成之后走到后面,记录日志的时候又要用当前用户信息,如果不缓存就又要查一遍,缓存的话就增加了系统的复杂度。 - 一旦代码结构这样设计了,那么使用面向对象的思想就终结了,这也是我觉得最重要的一点。
spring框架给我们提供了很多便利,但是要警惕这些便利的诱惑,我们可以很容易将一个类定义成一个单例bean,并将它交给spring容器进行管理,但是一旦这么做了,在容器启动过程中就创建了这个类的单实例,那么这个对象就必定是一种无状态的,那它本身就成为了一个工具类,处理业务逻辑的工具,它能对外提供一系列方法来处理业务逻辑,我们只需要使用Autowired注解注入这个对象,就能在很多地方随意的使用它提供的方法,仅此而已,这时我们已经坠入了面向过程编辑的陷井。
那么如何解决以上问题呢?
一、校验逻辑又多又杂,需要设计成可插拔的组件。每个组件一个类,这样可以由多人独立开发,并且代码不会冗余到OrderService
类中。
参考spring在生产bean的过程中有很多生命周期,而在每个生命周期的处理逻辑上,都使用了不同的 BeanPostProcessor,每个postProcessor都是可扩展的,对它的扩展不会影响spring创建 bean的主流程。简单点来说,增加或减少一个校验逻辑,OrderService的代码不会有任何改动。
下面是spring初始化bean的核心逻辑:
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {if (System.getSecurityManager() != null) {AccessController.doPrivileged((PrivilegedAction<Object>) () -> {invokeAwareMethods(beanName, bean);return null;}, getAccessControlContext());}else {invokeAwareMethods(beanName, bean);}Object wrappedBean = bean;if (mbd == null || !mbd.isSynthetic()) {wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);}try {invokeInitMethods(beanName, wrappedBean, mbd);}catch (Throwable ex) {throw new BeanCreationException((mbd != null ? mbd.getResourceDescription() : null),beanName, "Invocation of init method failed", ex);}if (mbd == null || !mbd.isSynthetic()) {wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);}return wrappedBean;}
这段代码做了什么事呢?
- 调用所有的
invokeAware
方法,比如正在创建UserService
,而UserService
实现了BeanFactoryAware
接口。那么在此处,spring 就会主动调用UserService
里面的setBeanFactory()
方法。 - 找到容器中所有的
BeanPostProcessors
,并执行postProcessBeforeInitialization()
。 - 执行bean的 init 方法,也就是在方法上加了
@PostConstract
注解,或实现了InitializingBean
重写的afterPropertiesSet()
方法。 - 找到容器中所有的
BeanPostProcessors
,并执行postProcessAfterInitialization()
方法。
主要看以下两个方法:
applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
@Override
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)throws BeansException {Object result = existingBean;for (BeanPostProcessor processor : getBeanPostProcessors()) {Object current = processor.postProcessBeforeInitialization(result, beanName);if (current == null) {return result;}result = current;}return result;
}@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)throws BeansException {Object result = existingBean;for (BeanPostProcessor processor : getBeanPostProcessors()) {Object current = processor.postProcessAfterInitialization(result, beanName);if (current == null) {return result;}result = current;}return result;
}
spring实现AOP功能,实际上就是在启动的过程中往容器里面加了一个 BeanPostProcessor,并在applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName)这个方法中,执行了 aop的这个 BeanPostProcessor的方法,得到的动态代理。而spring aop并没有改变spring ioc的代码。
按照以上的方式,我们可以将 UserService改造一版:
- 定义一个
OrderPostProcessor
接口,将方法加上default是因为有些类不不需要都实现这两个方法public interface OrderPostProcessor {// 调用save()方法前执行default void postProcessBeforeSave(OrderCreateRequest request){}// 调用save()方法后执行,因为save之后就有一个新的 Order了。default void postProcessAfterSave(Order order, OrderCreateRequest request){}}
- 将校验的业务逻辑分成不同的类,并实现
OrderPostProcessor
接口。举两个例子,一个是save之前执行,一个是save之后执行//这个类用于查询product并校验商品库存是否足够,商品的状态是否符合要求等 @Component public class ProductValidatorPostProcessor implements OrderPostProcessor{@Autowiredprivate ProductClient productClient;@Overridepublic void postProcessBeforeSave(OrderCreateRequest request) {// productClient.getProduct()} }// 这个类用于订单入库之后发送消息通知商户或者买家 @Component public class SendMessagePostProcessor implements OrderPostProcessor{@Overridepublic void postProcessAfterSave(Order order, OrderCreateRequest request) {// 发各种消息。。。} }
- 在
OrderService
中完成各个处理类的组装。@Service public class OrderService implements ApplicationContextAware {@Autowiredprivate OrderRepository orderRepository;@Transactionalpublic OrderDTO createOrder(OrderCreateRequest request) {applyOrderPostProcessorsBeforeSave(request);final Order save = orderRepository.save();applyOrderPostProcessorsAfterSave(save, request);return convert(save);}private void applyOrderPostProcessorsBeforeSave(OrderCreateRequest request) {// 从spring容器中取出所有实现了 OrderPostProcessor 接口的类,循环调用它的 postProcessBeforeSave() 方法final Map<String, OrderPostProcessor> beansOfType = applicationContext.getBeansOfType(OrderPostProcessor.class);for (OrderPostProcessor value : beansOfType.values()) {value.postProcessBeforeSave(request);}}private void applyOrderPostProcessorsAfterSave(Order order, OrderCreateRequest request) {final Map<String, OrderPostProcessor> beansOfType = applicationContext.getBeansOfType(OrderPostProcessor.class);for (OrderPostProcessor value : beansOfType.values()) {value.postProcessAfterSave(order, request);}}private OrderDTO convert(Order save) {OrderDTO dto = new OrderDTO();BeanUtils.copyProperties(save, dto);return dto;}ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;} }
代码是简洁了一些,我们将本应放在 OrderService
中的代码转移到各个其它的类中去了,OrderService
变小了。另我大家也发现这个OrderService
有明显的问题: applyOrderPostProcessorsBeforeSave
和 applyOrderPostProcessorsAfterSave
这两个方法与业务无关太突兀了,不应该放在 OrderService
里,并且每次调用都需要从容器中获所有的 OrderPostProcessor
实现类,不太合适,应该在启动的时候就创建好。
修改后如下:
定义了一个 OrderServiceBisProvider
类,将与业务无关的操作放分开
public abstract class OrderServiceBisProvider implements SmartInitializingSingleton, ApplicationContextAware {List<OrderPostProcessor> orderPostProcessors = new ArrayList<>();protected void applyOrderPostProcessorsBeforeSave(OrderCreateRequest request) {for (OrderPostProcessor value : orderPostProcessors) {value.postProcessBeforeSave(request);}}protected void applyOrderPostProcessorsAfterSave(Order order, OrderCreateRequest request) {for (OrderPostProcessor value : orderPostProcessors) {value.postProcessAfterSave(order, request);}}@Overridepublic void afterSingletonsInstantiated() {final Map<String, OrderPostProcessor> beansOfType = applicationContext.getBeansOfType(OrderPostProcessor.class);final Collection<OrderPostProcessor> values = beansOfType.values();// 排序AnnotationAwareOrderComparator.sort(Collections.singletonList(values));this.orderPostProcessors.addAll(values);}ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}
}
OrderServiceBisProvider 只是一个辅助类,目的是将 OrderService 中的部分代码抽出来,此处并无重用代码的想法,但是后面可以改进。
实现
SmartInitializingSingleton
这个接口,那么spring将会在所有的单实例非懒加载的bean创建之后来调用(参考spring bean创建的生命周期)。这时我们可以取出容器中所有实现了OrderPostProcessor
接口的bean。AnnotationAwareOrderComparator.sort(Collections.singletonList(values));
这段代码起排序作用。只要bean使用了 @Order注解或者实现了 order 接口都可以进行排序。
@Service
public class OrderService extends OrderServiceBisProvider {@Autowiredprivate OrderRepository orderRepository;@Transactionalpublic OrderDTO createOrder(OrderCreateRequest request) {applyOrderPostProcessorsBeforeSave(request);final Order save = orderRepository.save();applyOrderPostProcessorsAfterSave(save, request);return convert(save);}private OrderDTO convert(Order save) {OrderDTO dto = new OrderDTO();BeanUtils.copyProperties(save, dto);return dto;}}
这时候再来看 OrderService的代码确实简洁了许多,但是也增加了不少的类。尤其是增加了很多处理器。由于业务的需要,可能有些校验必须先执行,我们可以使用 @Order
注解对它们进行编排,这样就可以保证 OrderServiceBisProvider 中的List<OrderPostProcessor> orderPostProcessors
是按我们的要求排好序的。
//这个类用于查询product并校验商品库存是否足够,商品的状态是否符合要求等
@Order(1)
@Component
public class ProductValidatorPostProcessor implements OrderPostProcessor{@Autowiredprivate ProductClient productClient;@Overridepublic void postProcessBeforeSave(OrderCreateRequest request) {// productClient.getProduct()}
}// 这个类用于订单入库之后发送消息通知商户或者买家
@Order(10)
@Component
public class SendMessagePostProcessor implements OrderPostProcessor{@Overridepublic void postProcessAfterSave(Order order, OrderCreateRequest request) {// 发各种消息。。。}
}
现在的业务流程图:
总结:
通过以上重构,我们就实现了部分的开闭原则和单一职责原则,如果我们要增加一个创建订单前的校验,只需要增加OrderPostProcessor接口的实现类即可,不需要改OrderService的代码,理论上我们只要保证新加的代码进行了充分的测试即可,原先的代码就可以不用回归。另外,由于将各个校验或处理的业务逻辑分为不同的单元分散到各个类中,这也符合类的单一职责原则。
二、将前置处理过程中产生的数据保存到线程中,给后面的处理过程使用,实现性能的提升
假设现在有这么一个业务,在保存订单前校验用户的身份权限,保存订单后要给当前操作人发邮件。那么基于以上的结构,我们可以很快实现这个业务逻辑,增加一个类如下:
@Order(5)
@Component
public class OperatorPostProcessor implements OrderPostProcessor{@Autowiredprivate UserClient userClient;@Overridepublic void postProcessBeforeSave(OrderCreateRequest request) {
// userClient.getUser(request.getOperatorUuid());// 1. 调用 userClient 服务,查询当前操作人的信息// 2. 取出用户权限进行校验}@Overridepublic void postProcessAfterSave(Order order, OrderCreateRequest request) {
// userClient.getUser(request.getOperatorUuid());// 1. 调用 userClient 服务,查询当前操作人的信息// 2. 取出用户的邮箱,发邮件}
}
理论上,加了上面的类,系统是能正常运行的(实际也能正常运行),可是代码有瑕疵:调用了两次查询用户的方法 userClient.getUser(request.getOperatorUuid());
,由于这个对象是单例bean,所以不能用一个成员变量去维护用户的数据(会出现线程安全问题),只能将数据缓存到当前的线程中,这个时候很容易想到使用 ThreadLocal 来保存。
使用 ThreadLocal 是没问题的,但是不能将它放在这个类中,因为不仅仅只有这一个场景需要保存数据,其它的类也有同样的需求。因此将它放置到 OrderServiceBisProvider
中,命名为:ORDER_PROCESSOR_CONTEXT
public abstract class OrderServiceBisProvider implements SmartInitializingSingleton, ApplicationContextAware {List<OrderPostProcessor> orderPostProcessors = new ArrayList<>();public static final ThreadLocal<Map<String, Object>> ORDER_PROCESSOR_CONTEXT = new ThreadLocal<>();protected void applyOrderPostProcessorsBeforeSave(OrderCreateRequest request) {ORDER_PROCESSOR_CONTEXT.remove();ORDER_PROCESSOR_CONTEXT.set(new HashMap<>());for (OrderPostProcessor value : orderPostProcessors) {value.postProcessBeforeSave(request);}}protected void applyOrderPostProcessorsAfterSave(Order order, OrderCreateRequest request) {for (OrderPostProcessor value : orderPostProcessors) {value.postProcessAfterSave(order, request);}}@Overridepublic void afterSingletonsInstantiated() {final Map<String, OrderPostProcessor> beansOfType = applicationContext.getBeansOfType(OrderPostProcessor.class);final Collection<OrderPostProcessor> values = beansOfType.values();// 排序AnnotationAwareOrderComparator.sort(Collections.singletonList(values));this.orderPostProcessors.addAll(values);}ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}}
注意:在 applyOrderPostProcessorsBeforeSave 方法开始前加了两行代码:
ORDER_PROCESSOR_CONTEXT.remove(); ORDER_PROCESSOR_CONTEXT.set(new HashMap<>());
因为TomeCat的线程是基于线程池的,某个新请求的线程可能复用了之前的某个线程,就会带上ThreadLocal里面的数据,为了防止数据错乱,必需在线程开始前它清空重置。
OperatorPostProcessor 改造如下:
@Order(5)
@Component
public class OperatorPostProcessor implements OrderPostProcessor{@Autowiredprivate UserClient userClient;@Overridepublic void postProcessBeforeSave(OrderCreateRequest request) {User operator = userClient.getUser(request.getOperatorUuid());ORDER_PROCESSOR_CONTEXT.get().put("operator", operator); // 将信息存入 context 中// 1. 调用 userClient 服务,查询当前操作人的信息// 2. 取出用户权限进行校验}@Overridepublic void postProcessAfterSave(Order order, OrderCreateRequest request) {
// 直接从 context 中拿数据final User operator = (User) ORDER_PROCESSOR_CONTEXT.get().get("operator");// 1. 调用 userClient 服务,查询当前操作人的信息// 2. 取出用户的邮箱,发邮件}
}
同样的操作适用于其它的处理器,当然为了代码的可读性,可以自定义一个类,用于替换 ThreadLocal中的map,提供一些 getter和 setter方法。
三、使用多态,使增加处理类更易于扩展
按照上述的业务框架,可以对工作中部分业务代码进行重构,只能是部分,为什么呢?经常写业务代码的同学就会发现,业务需求会经常变化,偶尔加一个种类什么的很常见,就以下单为例,一个正常的下单业务流程,可能过一段时间加了一个内购活动,员工自己下单有额外的优惠,也可能订单又分为虚拟物品的订单(例如充值)等等,大家会发现,越往后,代码中加入的 if - else 分支越来越多,有的是根据下单用户的角色进行分类,有的是根据商品的种类进行分类,导致代码中有大量的分支,于是后来的人就不想看这些代码了,而且大家也不敢修改,害怕某一个改动影响了很早之前的一个隐藏的逻辑。
这个时候多态就要上场了,不同的类型用不同的类去实现,这样也可以实现对扩展开放,对修改封闭,不动以前的逻辑,保证代码的可维护性更强。那么怎么将多态整合到我们这个业务框架中呢?
现在的这个业务框架已经将各种业务分为一个一个的单元了,所以我们可以将多态运用到各个单元上,而不必动 OrderSerivce
。以一个具体的场景为例:如果是一个内购订单,就校验用户是否是本公司的员工,否则不校验。这时要对 OperatorPostProcessor
类进行重构。
- 引入一个抽象类,作为主框架,子类通过重写模版方法实现多态
public abstract class AbstractOperatorPostProcessor implements OrderPostProcessor{@Overridepublic void postProcessBeforeSave(OrderCreateRequest request) {User operator = userClient.getUser(request.getOperatorUuid());ORDER_PROCESSOR_CONTEXT.get().put("operator", operator);// 1. 调用 userClient 服务,查询当前操作人的信息if (support(request)) {// 2. 查询当前用户是否是员工,不是则抛出异常...}}@Overridepublic void postProcessAfterSave(Order order, OrderCreateRequest request) {final User operator = (User) ORDER_PROCESSOR_CONTEXT.get().get("operator");// 1. 调用 userClient 服务,查询当前操作人的信息// 2. 取出用户的邮箱,发邮件if (support(request)) {doExtraOperate(request);}}protected void doExtraOperate(OrderCreateRequest request) {// 如果是内部员工,做一些额外的操作,由具体的子类重写}abstract boolean support(OrderCreateRequest request);
- 子类继承
AbstractOperatorPostProcessor
// 普通的订单 @Order(5) @Component public class OperatorPostProcessor extends AbstractOperatorPostProcessor{// 是否是内购的订单@Overrideboolean support(OrderCreateRequest request) {return false;} }// 内购的订单 @Order(5) @Component public class InnerOperatorPostProcessor extends AbstractOperatorPostProcessor{// 是否是内购的订单@Overrideboolean support(OrderCreateRequest request) {return true;}@Overrideprotected void doExtraOperate(OrderCreateRequest request) {// 做一些额外的操作System.out.println("内部员工正在购买商品");} }
以上只是简单地举了一个例子,虽然有点迁强,但是足以体现了将多态整合到这个业务框架中的方法,当然实际工作中要视具体的情况来看到底是定义抽象类还是只定义接口。
4、小结
一提到设计模式,给大家的第一映象就是在框架,中间件中使用,而在我看来恰恰相反,设计模式的初衷就是使系统能有效地应对未来的变化,业务代码的变化是很频繁的,所以我觉得在业务代码中运用一些设计模式,或设计模式的思想是很有必要的。在实现一个功能之前应好好思考业务中不变的部分,只要找到了这些不变的部分,就将它封装到抽象类或接口中,而将变化的部分定义为抽象方法由不同的子类去实现。
本文提供的一个业务框架,仅仅适用于有较多的业务校验场景,如果是简单的业务,直接将它写在 OrderService里面会更好,等后续业务变复杂了,再将它们抽出来不迟。