流程模版设计

一.背景:

  • 需求:产品迭代,需要对接大量的系统,每个系统的业务流程之间,有些许的差异,这些差异会让代码里有很多的逻辑分支并增加自己独有的业务处理,造成代码耦合度高,难以维护。因此希望将业务代码进行拆解,拆分为一个个原子服务,再将这一个个原子服务,根据渠道编码和业务编码,组装成一个个模版,每一个模版就对应一个系统的对接需求,模版之间互不影响。
  • 系统架构:后台采用了Spring cloud+Spring boot的分布式框架,但没有解决分布式事务的问题,这也为一开始的技术选型失败,埋下了伏笔

二.技术选型:
一开始将上述需求定位为ESB架构,便开始针对这个架构进行选型,对比分析了Spring Integration、Mule ESB 、Apache Camel这3种集成架构

  • Spring Integration:

只提供了技术非常基础的支持,如文件,FTP,JMS,TCP,HTTP或Web服务
缺点:需要编写大量的XML代码,可读性差,不支持DSL编码

<file:inbound-channel-adapter
           id=”incomingOrders”
           directory=”file:incomingOrders”/>

<payload-type-router input-channel=”incomingOrders”>
           <mapping type=”com.kw.DvdOrder” channel=”dvdOrders” />
           <mapping type=”com.kw.VideogameOrder”
                               channel=”videogameOrders” />
           <mapping type=”com.kw.OtherOrder” channel=”otherOrders” />

</payload-type-router>

<file:outbound-channel-adapter
              id=”dvdOrders”
              directory=”dvdOrders”/>

<jms:outbound-channel-adapter
              id=”videogamesOrders”
              destination=”videogameOrdersQueue”
              channel=”videogamesOrders”/>

<logging-channel-adapter id=”otherOrders” level=INFO/>
  • Mule ESB:

提供一些非常有趣的连接器:SAP, Tibco Rendevous, Oracle Siebel CRM, Paypal 或 IBM’s CICS Transaction Gateway
优点:相比Spring Integration,可读性稍好,支持XML格式的DSL风格编码
缺点:相比camel,支持的组件不多,不支持Java代码风格的DSL,需要编写大量的XML

 //DSL风格的XML
<flow name=”muleFlow”>
       <file:inbound-endpoint path=”incomingOrders”/>
       <choice>
           <when expression=”payload instanceof com.kw.DvdOrder”
                        evaluator=”groovy”>
                       <file:outbound-endpoint path=”incoming/dvdOrders”/>
           </when>
           <when expression=”payload instanceof com.kw.DvdOrder”
                         evaluator=”groovy”>
                         <jms:outbound-endpoint
                         queue=”videogameOrdersQueue”/>
           </when>
           <otherwise>
                               <logger level=INFO/>
           </otherwise>
       </choice>
</flow>
  • Apache Camel:

Apache camel提供了用Java,Groovy和Scala编写的DSL
优点:相比Mule ESB,不仅支持XML格式的DSL编码,还支持Java代码的DSL编码

//DSL风格的XML
<route>
       <from uri=”file:incomingOrders”/>
       <choice>
           <when>
               <simple>${in.header.type} is ‘com.kw.DvdOrder’</simple>
                           <to uri=”file:incoming/dvdOrders”/>
           </when>
           <when>
               <simple>${in.header.type} is ‘com.kw.VideogameOrder’
              </simple>
                           <to uri=”jms:videogameOrdersQueue”/>
           </when>
           <otherwise>
               <to uri=”log:OtherOrders”/>
           </otherwise>
       </choice>
   </route>
//DSL风格的Java代码
from(“file:incomingOrders “)
      .choice()
               .when(body().isInstanceOf(com.kw.DvdOrder.class))
                               .to(“file:incoming/dvdOrders”)
               .when(body().isInstanceOf(com.kw.VideogameOrder.class))
                               .to(“jms:videogameOrdersQueue “)
               .otherwise()
                               .to(“mock:OtherOrders “);
  • 初步选型:
    在这几种集成架构中,Apache Camel无疑在功能及便利性方面获得了青睐,于是就拍板定了以Apache camel作为改造架构
  • Camel的缺陷

Camel路由过程中没有处理的异常会被被抛出到路由的发起者,对发生异常的路由停止进行后续步骤的处理
默认情况基本上就是已经做过的步骤没有rollback的操作,如果需要事务控制就更不行了

  • 异常机制
    1)利用Camel提供的DeadLetterChannel将出错的消息路由到"死队列"里,然后停止当前的路由
    2)利用Camel提供的onException功能,当有异常发生的时候,会根据不同的异常类型,跳到和onException里指
    定异常匹配的的步骤进行处理
    在这里插入图片描述
    在编写改造demo的过程中,发现camel无法满足我们对事务的需求,如基于camel调用A、B、C、D4个方法组成的一个模版,当A、B执行完,C发生异常,按照我们的正常流程,其中A、B方法的事务应该回滚,D方法不执行。
    但实际结果是,A、B执行完之后,C发生异常,模版可以停止往下执行(即D方法不执行),但事务无法回滚(A、B方法rollback),如果想解决事务的问题,就必须加入分布式事务,即使这个事务并不是分布式的事务(同一个JVM中)
  • 最终选型:
    弃用camel,根据需求,设计新的流程模版

三.新模版设计思路:
在这里插入图片描述


四.请求转发:对应标题三中的gateWay(网关路由)

  • 实现方式

基于Spring cloud的zuul网关filter,修改url并转发请求

  • zuul filter过滤器介绍
  • filterType:过滤类型(生命周期)

1)pre:路由之前被调用
2)routing:路由之时被调用
3)post:路由完成时处理请求结果
4)error:上述三阶段发生异常时触发,通过post类型的过滤器将最终结果返回给客户段

  • filterOrder:执行顺序

通过int值定义执行顺序,值越小优先级越高

  • shouldFilter: 过滤器执行标志

通过返回boolean值,判断filter是否被执行,用于设置filter的执行范围

  • run:

具体的业务逻辑,如权限、鉴权、请求转发等

  • 代码
public class TemplateRouteFilter extends ZuulFilter {

   @Override
   public String filterType() {
       return FilterConstants.ROUTE_TYPE;
   }

   @Override
   public int filterOrder() {
       return FilterConstant.TEMPLATE_FILTER_ORDER;
   }
       
   @Override
   public boolean shouldFilter() {
       RequestContext ctx = RequestContext.getCurrentContext();
       HttpServletRequest request = ctx.getRequest();
       return null == request.getHeader("templateFlag");
   }

   @Override
   public Object run() {
       RequestContext context = RequestContext.getCurrentContext();
       HttpServletRequest request = context.getRequest();
       int serverPort = request.getServerPort();
       try {
           URI uri1 = new URI("http://localhost:"+serverPort);
           context.setRouteHost(uri1.toURL());
           context.addZuulRequestHeader("templateFlag", "true");
       } catch (Exception e) {
           e.printStackTrace();
       }
       //设置新的请求的uri标识符
       context.put(FilterConstants.REQUEST_URI_KEY, "/api/billfacade/demo/save");
       return null;
   }
}
  • 遇到的问题
  • 请求死循环
  • 原因:
    在zuul网关实现转发功能,必须指定filterType为routing,如上代码,当请求第一次进来之后(请求url:http://localhost:8765/api/billfacade/demo/save1), 被zuul网关的filter过滤器捕捉到,修改并转发的url:http://localhost:8765/api/billfacade/demo/save ,这个修改的新的url又有会被zuul 捕获到,再次处理,以此就形成了死循环
  • 解决:
    当请求的url第一次被filter捕捉到之后,在header中设置请求的url已被修改的标志位,在shouldFilter中根据标志位判断修改url的filter是否应该被执行
  • filterType为pre的filter会被执行2次
  • 原因:
    zuul网关filter的执行顺序是pre->routing->post,假如在zuul中有2个filter,filterType分别为:pre、routing(修改
    url),我们期望的执行结果是pre->routing->业务模块,但实际情况是pre会被执行2次。
    当请求第一次被zuul捕捉到之后,先被pre的filter执行,再被routing的filter执行修改url并设置标志位,然后该请求再次被zuul网关捕捉到,又再次执行了filterType为pre的filter,但请求到filterType为routing的filter时,因header中已设置了修改完成的标志位,该filter不会被执行,故该请求的执行结果是pre->routing-pre->业务模块
  • 解决:
    方法1)同请求死循环的处理方式,根据header中的标志位判断该filter是否需要被执行
    方法2)在处理filterType为pre的filter时,先判断该请求是否需要修改url,如果需要修改,pre对应的filter先不执行,在修改完请求的url后再执行

五.路由:对应标题三中的routeMapping(模版路由)

  • 思路

1)定义Executor接口,内有方法抽象方法execute,用于根据Class类型在IOC容器中,获取所有的模版对象
2)定义RouteMapping注解,设置value(模版的唯一标识),用于匹配模版
3)所有的模版需要实现Executor接口,重写execute方法,注册到IOC容器中(如@Component注解),并添加RouteMapping注解
4)用户发起请求,在拦截器里从header中获取routeMapping,匹配对应的Template
5)在Template调用方法execute()执行流程模版

  • 流程图
    在这里插入图片描述
  • Executor接口
public interface Executor {
   /**
    * 模版执行器
    *
    * @param requestParam request请求入参
    * @return Object request请求的返回的结果,如果不是ObjectRestResponse对象,需进行new ObjectRestResponse<>()封装
    */
   Object execute(Object requestParam) throws Exception;
}
  • RouteMapping注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface RouteMapping {
   String mapping() default "";
}
  • 模版实现类
    实现Executor接口,重写execute方法,注册到IOC容器中,并添加RouteMapping注解
@Component
@RouteMapping(mapping = "consigner")
public class EntrustConsignerHandler implements Executor {

   /**
    * 货主委托单模版执行方法
    *
    * @param requestParam request请求入参
    * @return java.lang.Object
    */
   @Override
   public Object execute(Object requestParam) throws Exception {

       log.info(">>>>> 货主委托单模版执行器<<<<<");
       return TemplateBuilder
               .start()
               .commonTemplate(EntrustTemplate.class)
               .init(requestParam)
               .method("transmitParam")
               .method("validation")
               .method("emptyUserIdValidator")
               .method("emptyGoodsListValidator")
               .method("goodsListInit")
               .method("init")
               .method("setConsignerCompany",EntrustConsignerHandler.class)
               .method("feign")
               .builder()
               .end();
   }
}
  • 根据Class类型在IOC容器中,获取所有的模版对象
public class SpringContextUtil implements ApplicationContextAware {

   private static ApplicationContext context;

   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
       this.context = applicationContext;
   }

   /**
    * 根据Class类型在IOC容器中获取对象
    *
    * @param clazz Class类型
    * @return 对象
    */
   public static <T> List<T> getBeanByType(Class<T> clazz) {
       List<T> list = new ArrayList<T>();

       /* 获取接口的所有实例名 */
       String[] beanNames = context.getBeanNamesForType(clazz);

       if (beanNames == null || beanNames.length == 0) {
           return list;
       }
       T t ;
       for (String beanName : beanNames) {
           t = (T) context.getBean(beanName);
           list.add(t);
       }
       return list;
   }
}
  • 拦截器
public class TemplateInterceptor {

   @Pointcut("execution(* com.sinochem.yunlian.truck.billfacade.camel.controller..*(..))")
   public void controllerMethodPointcut() {
   }
   @Around("controllerMethodPointcut()")
   public Object interceptor(ProceedingJoinPoint pjp) throws Exception {
       RequestContextHolder.currentRequestAttributes();
       RequestAttributes ra = RequestContextHolder.getRequestAttributes();
       ServletRequestAttributes sra = (ServletRequestAttributes) ra;
       HttpServletRequest request = sra.getRequest();
       String routeMapping = request.getHeader("routeMapping");
       log.info(">>>>> 模版路由映射路径,routeMapping:{}<<<<<", routeMapping);
       //获取所有的模版bean
       List<Executor> beans = SpringContextUtil.getBeanByType(Executor.class);
       Object result = null;
       //根据模版list和taskMapping获取对应的执行器
       Executor executor = TemplateUtil.getExecutor(beans, routeMapping);
       if (null == executor) {
           return new ObjectRestResponse<>(CodeStatus.PARAM_INVALID);
       }
       log.info(">>>>> 模版执行器,executor:{}<<<<<", executor.getClass().getSimpleName());
       Object[] args = pjp.getArgs();
       //执行器执行
       if (null != args && args.length > 0 && !(args[0] instanceof HttpServletRequest) && !(args[0] instanceof HttpServletResponse)) {
           result = executor.execute(args[0]);
       }
       log.info(">>>>> 模版执行结果,result:{}<<<<<", JacksonUtils.toJSONString(result));
       return result instanceof ObjectRestResponse ? (ObjectRestResponse) result : new ObjectRestResponse<>(result);
   }
}  
  • 拦截器工具方法-根据模版mapping获取bean对象
	/**
    * 根据模版mapping获取bean对象
    *
    * @param beans   beanList
    * @param mapping TaskMapping注解的值
    * @return org.apache.poi.ss.formula.functions.T
    */
   public static Executor getExecutor(List<Executor> beans, String mapping) {
       if (CollectionUtils.isEmpty(beans) || null == mapping) {
           log.error(">>>> get template executor by mapping,mapping must not be null! <<<<<");
           return null;
       }
       return beans.stream().filter(bean -> mapping.equals(getTaskMapping(bean))).findFirst().orElse(null);
   }

   /**
    * 获取TaskMapping的映射路径
    *
    * @param object bean对象
    * @return RouteMapping 注解
    */
   private static String getTaskMapping(Object object) {
       Class entityClass = null != object ? object.getClass() : null;
       RouteMapping taskMapping = null;
       if (null == entityClass) {
           log.error(">>>> get RouteMapping,entityClasss must not be null! <<<<<");
           return null;
       }
       Annotation[] annotations = entityClass.getAnnotations();
       if (annotations.length == 0) {
           log.warn(">>>> get RouteMappingn class :{},annotations is empty! <<<<<", entityClass.getSimpleName());
           return null;
       }
       for (Annotation annotation : annotations) {
           if (annotation instanceof RouteMapping) {
               taskMapping = (RouteMapping) annotation;
               break;
           }
       }
       return null != taskMapping ? taskMapping.mapping() : null;
   }

六.模版构建:对应标题三中的templateBuilder(流程模版执行)

  • 流程
  • 思路
    1)利用建造者模式,组装由不同方法顺序组成的方法列表
    2)利用反射,遍历顺序执行组装的方法列表
    3)采用InheritableThreadLocal,作为方法传递的载体

  • 主要参数说明

  • Class<?> commTemplateclazz: 通用模版类class
    作用:在method()方法中,如果未指定所属的class,默认从该class中获取方法

  • TemplateEntity:模版entity类
    作用:定义了methodName(方法名)、templateClazz(模版类class),用于根据方法名获取Method

  • Vector < TemplateEntity > methods:方法集合
    作用:用于顺序存放,构建时传递的方法,以便后续遍历执行方法

  • Set< Class > templateClazzs:模版类class set集合
    作用:存放所有的模版类,用于统一校验方法是否在模版类中

  • Map< String, Object > instanceMap:模版类class 实例对象集合
    作用:存放构建的模版类的示例对象,保证同一个模版类在执行模版方法获取的是同一个对象

  • 流程图
    在这里插入图片描述

  • 代码
  • 模版类建造者
public class TemplateBuilder {

   /**
    * 通用模版类class
    */
   private Class<?> commTemplateclazz;

   /**
    * 方法名集合
    */
   private final Vector<TemplateEntity> methods = new Vector<>(16);

   /**
    * 模版类class set集合
    */
   private final Set<Class> templateClazzs = new HashSet<>(16);

   /**
    * 模版类class 实例对象集合
    */
   private final Map<String, Object> instanceMap = new HashMap<>(16);

   /**
    * 模版入口方法
    *
    * @return 模版builder
    */
   public static TemplateBuilder start() {
       return new TemplateBuilder();
   }

   /**
    * 通用模版类class传递
    * 1)通用模版class传递
    * 2)模版类set集合设置
    * 3)模版类实例对象设置
    *
    * @param templateclazz 模版类class
    * @return 模版builder
    */
   public TemplateBuilder commonTemplate(Class<?> templateclazz) {
       //入参校验
       this.commTemplateclazz = templateclazz;
       this.templateClazzs.add(templateclazz);
       TemplateUtil.setInstanceToMap(templateclazz, this.instanceMap);
       return this;
   }

   /**
    * 通用模版方法传递
    *
    * @param methodName 方法名
    * @return 模版builder
    */
   public TemplateBuilder method(String methodName) {
       this.methods.add(new TemplateEntity(methodName, this.commTemplateclazz));
       return this;
   }

   /**
    * 特殊模版方法传递
    *
    * @param methodName    方法名
    * @param templateClazz 模版类class
    * @return 模版builder
    */
   public TemplateBuilder method(String methodName, Class<?> templateClazz) {
       this.methods.add(new TemplateEntity(methodName, templateClazz));
       this.templateClazzs.add(templateClazz);
       TemplateUtil.setInstanceToMap(templateClazz, this.instanceMap);
       return this;
   }

   /**
    * 构建模版
    * 1)校验方法有效性
    * 2)遍历执行方法
    *
    * @return 模版builder
    */
   public TemplateBuilder builder() throws Exception {
       TemplateUtil.isMethodsInClazzs(this.templateClazzs, this.methods);
       for (TemplateEntity method : this.methods) {
           process(method);
       }
       return this;
   }

   /**
    * 参数初始化
    *
    * @param paramObj 请求参数vo
    * @return 模版builder
    */
   public TemplateBuilder init(Object paramObj) {
       TemplateContext.get().put(TemplateConstant.EXCHANGE_FIELD_PARAM, paramObj);
       return this;
   }


   /**
    * 返回结果
    *
    * @return 返回结果
    */
   public ObjectRestResponse end() {
       return null != TemplateContext.get(TemplateConstant.EXCHANGE_FIELD_RESULT) ? (ObjectRestResponse) TemplateContext.get(TemplateConstant.EXCHANGE_FIELD_RESULT) : null;
   }

   /**
    * 处理方法
    * 1)获取方法对象
    * 2)获取方法形参数组
    * 3)获取方法对应的实例对象
    * 4)反射执行方法
    * 5)执行结果通用处理
    *
    * @param entity 方法entity
    */
   private void process(TemplateEntity entity) throws Exception {
       Class clazz = entity.getTemplateClazz();
       Method[] methods = clazz.getDeclaredMethods();
       Method method = TemplateUtil.getMethod(methods, entity.getMethodName());
       if (null == method) {
           throw new IllegalArgumentException(String.format("get method by methodName:【%s】result is null", entity.getMethodName()));
       }
       Object[] args = TemplateUtil.getArgs(method);
       //try {
       Object templateInstance = this.instanceMap.get(clazz.getSimpleName());
       Object invoke = method.invoke(templateInstance, args);
       if (null != invoke && invoke.getClass().isInstance(ObjectRestResponse.class)) {
           TemplateContext.put(TemplateConstant.EXCHANGE_FIELD_RESULT, invoke);
       }
       //} catch (Exception e) {
       //e.printStackTrace();
       // TODO: 2019/4/9 异常不捕捉,需被spring捕获,用于事务回滚
       //throw new RuntimeException("方法处理异常");
       //}
   }

}
  • 方法参数传递载体InheritableThreadLocal
public class TemplateContextNew {

   /**
    * 模版类构建参数传递/交换map
    */
   private static ThreadLocal<Map<String, Object>> exchangeMap = new InheritableThreadLocal<>();

   /**
    * 获取模版类参数传递map
    *
    * @return 模版构建参数传递map
    * 2019/4/4
    * v3.4
    */
   public static Map<String, Object> get() {
       Map<String, Object> map = exchangeMap.get();
       if (map == null) {
           map = new HashMap<>(16);
           exchangeMap.set(map);
       }
       return map;
   }

   /**
    * 模版方法入参-设置
    *
    * @param key 方法入参的参数名称
    * @param value 方法入参的值
    * 2019/4/4
    * v3.4
    */
   public static void put(String key, Object value) {
       Map<String, Object> map = exchangeMap.get();
       if (map == null) {
           map = new HashMap<>(16);
           exchangeMap.set(map);
       }
       map.put(key, value);
   }

   /**
    * 模版方法入参-获取
    *
    * @param key 方法入参的参数名称
    * @return java.lang.Object
    * 2019/4/4
    * v3.4
    */
   public static Object get(String key) {

       Map<String, Object> map = exchangeMap.get();
       if (map == null) {
           map = new HashMap<>(16);
           exchangeMap.set(map);
       }
       return map.get(key);
   }

   /**
    * 删除模版类线程局部变量的当前的值
    *
    */
   public static void remove() {
       exchangeMap.remove();
   }

}
  • 获取指定类指定方法的参数名
   /**
    * 获取指定类指定方法的参数名
    *
    * @param method 方法
    * @return 按参数顺序排列的参数名列表,如果没有参数,则返回null
    */
    private static String[] getMethodParameterNames(final Method method) {
       if (null == method) {
           log.error(">>>> get mehtod param names,method must not be null! <<<<<");
           return null;
       }
       ParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
       String[] parameterNames = null;
       try {
           parameterNames = discoverer.getParameterNames(method);
       } catch (Exception e) {
           log.error(">>>> get mehtod param names error:{} <<<<<", e.getMessage());
       }
       return parameterNames;
   }
  • 获取模版方法对应的入参数组
  /**
    * 获取模版方法对应的入参数组
    * 1)ThreadLocal中获取 exChangeMap
    * 2)获取方法的参数名称数组
    * 3)根据参数名获取方法入参数组
    *
    * @param method 模版方法
    * @return java.lang.Object[]
    */
   public static Object[] getArgs(Method method) {
       Map<String, Object> cacheMap = TemplateContext.get();
       if (null == cacheMap) {
           log.error(">>>> get mehtod param args,cacheMap must not be null! <<<<<");
           return null;
       }
       Class<?>[] types = getParameterTypes(method);
       String[] names = getMethodParameterNames(method);
       if (null == types || null == names) {
           log.error(">>>> get mehtod param args,method must not be null! <<<<<");
           return null;
       }
       if (types.length != names.length) {
           log.error(">>>> get mehtod param args,type.length:【{}】 mismatch names.length:【{}】! <<<<<", types.length, names.length);
           throw new IllegalArgumentException("argument type and name length mismatch");
       }
       Object[] args = new Object[names.length];
       //参数强制转换
       for (int i = 0; i < names.length; i++) {
           Object paramObj = cacheMap.get(names[i]);
           // TODO: 2019/4/4 在方法参数的传递过程中,null值传递的处理(是否允许空值传递?)
           /*if (null == paramObj) {
               throw new IllegalArgumentException(String.format("get value from exchangeMap by name:{ %s } ,result is null!", names[i]));
           }*/
           /*if (!types[i].isInstance(paramObj)) {
               throw new IllegalArgumentException(String.format("argument:{ %s } type:{ %s } mismatch", names[i], types[i]));
           }*/
           args[i] = types[i].cast(paramObj);
       }
       return args;
   }
  • 检查组装的方法是否在模版类中存在
    /**
    * 检查组装的方法是否在模版类中存在
    *
    * @param methodNames   组装方法名set集合
    * @param templateClazz 模版类class
    */
   private static void checkMethodsInClass(Set<String> methodNames, Class<?> templateClazz) {
       //模版方法数量校验
       Method[] methodArray = templateClazz.getDeclaredMethods();
       if (null == methodArray || methodArray.length == 0) {
           throw new IllegalArgumentException(String.format("get method from template:[%s] result is empty", templateClazz.getSimpleName()));
       }
       Set<String> illegalMethod = methodNames.stream().filter(name -> !methodNames.contains(name)).collect(Collectors.toSet());
       if (illegalMethod.size() > 0) {
           throw new IllegalArgumentException(String.format("Illegal method:%s in class: %s  ", illegalMethod.toString(), templateClazz.getSimpleName()));
       }
   }
  • 组装方法有效性检查
   /**
    * 组装方法有效性检查
    * 遍历对比,模版class中是否有对应的方法名
    *
    * @param templateClazzs 模版类class集合
    * @param methods        方法名集合
    */
   public static void isMethodsInClazzs(Set<Class> templateClazzs, Vector<TemplateEntity> methods) {
       if (CollectionUtils.isEmpty(templateClazzs)) {
           log.error(">>>> check method templateClazzs must not empty! <<<<<");
           return;
       }
       for (Class clazz : templateClazzs) {
           Set<String> methodNames = new HashSet<>(16);
           for (TemplateEntity entity : methods) {
               if (clazz.getSimpleName().equals(entity.getClassName())) {
                   methodNames.add(clazz.getSimpleName());
               }
           }
           checkMethodsInClass(methodNames, clazz);
       }
   }
  • 设置模版类实例对象
   /**
    * 设置模版类实例对象
    *
    * @param clazz       模版类class
    * @param instanceMap 模版类class
    */
   public static void setInstanceToMap(Class<?> clazz, Map<String, Object> instanceMap) {
       if (null != clazz && null == instanceMap.get(clazz.getSimpleName())) {
           try {
               instanceMap.put(clazz.getSimpleName(), clazz.newInstance());
               //基于spring代理
               //Object bean = SpringContextUtil.getBean(clazz);
               //instanceMap.put(clazz.getSimpleName(), bean);
           } catch (Exception e) {
               e.printStackTrace();
               // TODO: 2019/4/9 异常处理
           }
       }
   }

七.功能测试:

  • 并发/事务测试:
  • 并发场景:

并发问题的本质是多线程竞争共享资源,在spring框架下,注册到IOC容器的对象默认是单例的,如果我们在模版类中定义了类变量,模版类对象又是单例的,在多线程情况下,类变量就会存在线程安全问题。但在模版类中,使用类变量可以极大的提高我们方法参数传递的便利性。
因此,既想要便利性,又想要规避线程不安全问题,我们需要关注以下两个地方的对象创建/调用
1)每一次调用TemplateBuilder时,需创建一个新的对象(详见TemplateBuilder的start()方法)
2)不同的请求,调用TemplateBuilder,利用反射method.invoke(templateInstance, args),传入的
templateInstance(模版类对象)是不同的对象

1)同一个模版的多次请求,请求A、B同时执行,请求A休眠15s,请求B不休眠,观察日志中,同一模版中类变量A请求的值被B请求覆盖?

public class TestTemplate {

   private TestEntity testEntity;
   /**
    * request 公共入参
    */
   public TestEntity billEntrustVo(){
       this.testEntity = (TestEntity) TemplateContext.get(TemplateConstant.EXCHANGE_FIELD_PARAM);
       return this.testEntity;
   }

   /**
    * 公共参数校验
    */
   public void validation() throws InterruptedException {
       long sleepTime = null != billEntrustVo().getSleepTime() ? billEntrustVo().getSleepTime() : 0L;
       Thread.sleep(sleepTime);
       System.out.println(">>>>> threadName:" + Thread.currentThread().getName() + ", name:" + billEntrustVo().getName() + ", sleepTime:" + sleepTime);
   }
}
public class TestPool {

   public static void main(String[] args){
       ExecutorService pool = Executors.newFixedThreadPool(3);

       pool.submit(() -> {
           TestEntity vo = new TestEntity();
           vo.setName("thread1");
           vo.setSleepTime(5000L);
           try {
               TemplateBuilder
                       .start()
                       .commonTemplate(TestTemplate.class)
                       .init(vo)
                       .method("validation")
                       .builder()
                       .end();
           } catch (Exception e) {
               e.printStackTrace();
           }
       });

       pool.submit(() -> {
           TestEntity vo = new TestEntity();
           vo.setName("thread2");
           vo.setSleepTime(4000L);
           try {
               TemplateBuilder
                       .start()
                       .commonTemplate(TestTemplate.class)
                       .init(vo)
                       .method("validation")
                       .builder()
                       .end();
           } catch (Exception e) {
               e.printStackTrace();
           }
       });
}

输出结果:

>>>>> threadName:pool-1-thread-2, name:thread2, sleepTime:4000
>>>>> threadName:pool-1-thread-1, name:thread1, sleepTime:5000

2)不同模版的多次请求,请求A、B同时执行,请求A休眠15s,请求B不休眠,观察日志中,模版组装方法的list中,2个模版的方法是否有重叠?

已测试过,不会存在不同模版方法的重叠问题,具体测试代码可以类比上一个测试代码(此处略过)

  • 事务场景:
    组装方法A、B先后执行,方法A修改数据成功,方法B抛出异常,观察A方法修改的数据是否被写入数据库?
  • 本流程模版中的事务基于Spring的事务管理, 已测试该流程模版满足我们的事务需求(具体测试代码略)
  • 关于Spring事务失效:
    1)不要在Controller层添加@Transactional注解,(父子容器问题,详见本文末尾)
    2)不要重复扫描@Service层
    3)注解@Transactional注解开启配置,必须放到listener里加载,如果放到DispatcherServlet配置里,事务不起作用(父子容器问题)
    4)@Transactional注解只能应用到public方法,如果在protect、private方法上不会报错,但会失效(跟反射有关)
  • 线程池--线程复用带来的线程变量缓存问题:
    修改tomcat的请求的最大连接数和最大请求数为1,请求A、B同时执行,请求A休眠15s,请求B不休眠,观察日志中,2次请求打印的线程id是否一样?打印的InheritableThreadLocal中获取的同一变量的值A请求被B请求覆盖?
@Component()
public class UpdateTomacat extends TomcatEmbeddedServletContainerFactory
{
   @Override
   public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers)
   {
       //设置端口
       this.setPort(8778);
       return super.getEmbeddedServletContainer(initializers);
   }

   @Override
   protected void customizeConnector(Connector connector)
   {
       super.customizeConnector(connector);
       Http11NioProtocol protocol = (Http11NioProtocol)connector.getProtocolHandler();
       //设置最大连接数
       protocol.setMaxConnections(1);
       //设置最大线程数
       protocol.setMaxThreads(1);
       protocol.setConnectionTimeout(30000);
   }
}

修改tomcat的请求线程池的容量的代码如上
已测试,线程池的线程复用,在并发请求的场景下,没有带来的线程变量缓存问题(针对InheritableThreadLocal)(具体测试代码略)


八.问题:

  1. 流程模版如何中断?
  • 在常规方法中,我们可以根据业务逻辑在方法的任意位置来return(中断并返回结果),但在流程模版中,我们无法通过return的方式,中断流程返回结果,只能通过抛异常的方式中断流程,在InheritableThreadLocal中获取返回值。
  • 抛异常中断流程带来的问题:
    当我们在调用流程模版时,使用了事务(@Transactional注解),有时并不希望这个流程中断的异常被Spring捕捉到执行rollback的操作,因此我们需要定义专用于中断流程返回结果的异常,该异常不能被Spring的Transactional事务管理器捕捉到
  1. 参数传递基于ThreadLocal,异步场景下失效?

一开始的流程模版,我们是使用ThreadLocal作为方法参数传递的载体,但ThreadLocal在异步线程场景下会失效,利用InheritableThreadLocal可以有效解决异步场景下的方法参数传递问题

  1. 不同方法的方法传递相同的参数名,值被覆盖?

在TemplateBuilder中,利用了反射的method.invoke(templateInstance, args)来执行方法,其中的args参数的获取是利用了InheritableThreadLocal中的map来获取值的,当流程模版中的不同方法,使用了相同的参数名,就无法避免不同的方法参数值被覆盖的问题,所以在向InheritableThreadLocal存/取参数时,必须要注意参数名重复值被覆盖,此问题不可避免,只能根据业务逻辑,适当的区分。即使选用Apache Camel作为流程模版的架构,也无法规避这个问题


九.扩展:

  • Controller层添加@Transactional注解导致事务失效解析–本质上是父子容器问题
  1. 什么是父子容器?

这个问题的本质是ContextLoaderListener和DispatcherServlet的区别

  • ContextLoaderListener作用:在web容器启动时加载,/WEB-INF/下的ApplicationContent.xml文件,并创建WebApplicationContext容器
  • DispatcherServlet是我们第一次访问应用时加载,/WEB-INF/下面的< servlet-name >-servlet.xml配置文件,然后也创建一个WebApplicationContext容器
    这个WebApplicationContext容器会将之间ContextLoaderListener创建的容器作为父容器,因此在父容器中配置的所有bean都能够被注入到子容器中

一般情况下,一个SSM 组合的框架中,会存在以下几个配置文件:

web.xml(简化了部分配置)

   <context-param>
   	<param-name>contextConfigLocation</param-name>
   	<param-value>
   		classpath:context/context.xml
   	</param-value>
   </context-param>
   
   <listener>
   	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>
   <listener>
   	<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
   </listener>
   
   <servlet>
   	<servlet-name>springmvc</servlet-name>
   	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
   	<init-param>
   		<param-name>contextConfigLocation</param-name>
   		<param-value>classpath:context/jsp-dispatcher.xml</param-value>
   	</init-param>
   	<load-on-startup>1</load-on-startup>
   </servlet>

   <servlet-mapping>
   	<servlet-name>springmvc</servlet-name>
   	<url-pattern>*.html</url-pattern>
   </servlet-mapping>
</web-app>

applicationContext.xml 文件(简化了部分配置)


   <!-- 配置service层的组件扫描器 -->
   <context:component-scan base-package="com.xxx.service"></context:component-scan>
   <context:component-scan base-package="com.xxx.dao"></context:component-scan>
   //说明:在这个里面存在着一个spring的注解扫描器。用来扫描@Service/@Compont的注解

   <!-- Transaction manager for a single JDBC DataSource -->
   <bean id="transactionManager"
   	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   	<property name="dataSource" ref="dataSource" />
   </bean>
   <!-- 开启注解方式声明事务 -->
   <tx:annotation-driven transaction-manager="transactionManager" />

springmvc.xml 文件(简化了部分配置)


   <mvc:annotation-driven />
   <context:annotation-config />  <!-- 激活Bean中定义的注解 -->

   <!-- 配置Controller扫描器 -->
   <context:component-scan base-package="com.xxx.controller" />
   //说明:这个里面存在着springmvc的注解扫描器,专门用来扫描@Controller的注解

   <!-- 定义视图解析器 -->
   <bean
   	class="org.springframework.web.servlet.view.InternalResourceViewResolver">
   	<property name="prefix">
   		<value>/WEB-INF/jsp/</value>
   	</property>
   	<property name="suffix">
   		<value>.jsp</value>
   	</property>
   </bean>
  1. 父子容器关系?

Spring容器是父容器,SpringMVC容器为子容器
子容器可以引用父容器创建的Bean,父容器不能引用子容器创建的Bean

  1. <tx:annotation-driven/>对事务失效的影响?

我们知道tx:annotation-driven/的作用是开启注解方式的声明式事务
在spring-framework-reference.pdf文档中有这样一段话:
tx:annotation-driven/ only looks for @Transactional on beans in the same application context it is defined in. This means that, if you put tx:annotation-driven/ in a WebApplicationContext for a DispatcherServlet, it only checks for @Transactional beans in your controllers, and not your services.
这句话的意思是,tx:annoation-driven/只会查找和它在相同的应用上下文件中定义的bean上面的@Transactional注解,即如果我们在ApplicationContent.xml中定义了tx:annoation-driven/,那么tx:annoation-driven/只会检查在ContextLoaderListener的上下文中添加@Transactional注解的方法,在Springmvc.xml 中同理,这也就解释了,为什么我们在@Controller中加@Transactional注解事务不生效的原因

  1. 由问题3来看,可以看到@Controller层加@Transactional注解事务未生效的原因是,<tx:annoation-driven/>和@controller不在同一个上下文环境中。那如果我们将@Controller和<tx:annoation-driven/>放在同一个上下文中那么事务是否会生效?
  • 方式1:在ApplicationContent.xml中扫描全部对象(包含@Controller层)

  • 现象:请求对应@Controller接口报404错误

  • 原因:采用这样的方式后,会将所有的对象放到Spring容器中,而SpringMVC容器中不会有对象,当请求到达时,SpringMVC找不到对应的处理映射器,进而报404错误

  • 方式2:在Springmvc.xml中添加tx:annoation-driven/

  • 有待测试,不过官方建议,父子容器各尽其责。SpringMVC应只加载web相关配置(视图配置、Controller注解扫描),由Spring加载数据源、事务配置、Service和Dao注解扫描

  1. 由问题4来看,在@Service层加@Transactional注解是官方推荐的,那如果我们在@Service层加@Transactional注解,但同时在ApplicationContent.xml和Springmvc.xml中扫描@Service注解,此时事务是否会生效?
  • 结果:事务不生效
  • 原因:Spring要使事务生效,需要使用cglib为事务方法所在的类生成代理类对象,当我们在springmvc.xml中又扫描了一遍时,又会为事务方法所在的类生成一个新的对象,使得原来的在spring容器中生成的代理类对象失效,进而导致事务拦截器失效
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值