一.背景:
-
需求:
产品迭代,需要对接大量的系统,每个系统的业务流程之间,有些许的差异,这些差异会让代码里有很多的逻辑分支并增加自己独有的业务处理,造成代码耦合度高,难以维护。因此希望将业务代码进行拆解,拆分为一个个原子服务,再将这一个个原子服务,根据渠道编码和业务编码,组装成一个个模版,每一个模版就对应一个系统的对接需求,模版之间互不影响。 -
系统架构:
后台采用了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并转发请求
-
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)(具体测试代码略)
八.问题:
流程模版如何中断?
- 在常规方法中,我们可以根据业务逻辑在方法的任意位置来return(中断并返回结果),但在流程模版中,我们
无法通过return的方式,中断流程返回结果,只能通过抛异常的方式中断流程,
在InheritableThreadLocal中获取返回值。- 抛异常中断流程带来的问题:
当我们在调用流程模版时,使用了事务(@Transactional注解),有时并不希望这个流程中断的异常被Spring捕捉到执行rollback的操作
,因此我们需要定义专用于中断流程返回结果的异常,该异常不能被Spring的Transactional事务管理器捕捉到
参数传递基于ThreadLocal,异步场景下失效?
一开始的流程模版,我们是使用ThreadLocal作为方法参数传递的载体,但
ThreadLocal在异步线程场景下会失效
,利用InheritableThreadLocal可以有效解决异步场景下的方法参数传递问题
不同方法的方法传递相同的参数名,值被覆盖?
在TemplateBuilder中,利用了反射的method.invoke(templateInstance, args)来执行方法,其中的args参数的获取是利用了InheritableThreadLocal中的map来获取值的,当流程模版中的不同方法,使用了相同的参数名,就无法避免不同的方法参数值被覆盖的问题,所以在向InheritableThreadLocal存/取参数时,必须要注意
参数名重复值被覆盖,此问题不可避免,只能根据业务逻辑,适当的区分。即使选用Apache Camel作为流程模版的架构,也无法规避这个问题
九.扩展:
- Controller层添加@Transactional注解导致事务失效解析–
本质上是父子容器问题
什么是父子容器?
这个问题的本质是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>
父子容器关系?
Spring容器是父容器,SpringMVC容器为子容器
子容器可以引用父容器创建的Bean,父容器不能引用子容器创建的Bean
<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注解事务不生效的原因
由问题3来看,可以看到@Controller层加@Transactional注解事务未生效的原因是,<tx:annoation-driven/>和@controller不在同一个上下文环境中。那如果我们将@Controller和<tx:annoation-driven/>放在同一个上下文中那么事务是否会生效?
- 现象:请求对应@Controller接口报404错误
- 原因:采用这样的方式后,会将所有的对象放到Spring容器中,而SpringMVC容器中不会有对象,当请求到达时,SpringMVC找不到对应的处理映射器,进而报404错误
- 有待测试,不过官方建议,父子容器各尽其责。SpringMVC应只加载web相关配置(视图配置、Controller注解扫描),由Spring加载数据源、事务配置、Service和Dao注解扫描
由问题4来看,在@Service层加@Transactional注解是官方推荐的,那如果我们在@Service层加@Transactional注解,但同时在ApplicationContent.xml和Springmvc.xml中扫描@Service注解,此时事务是否会生效?
- 结果:事务不生效
- 原因:Spring要使事务生效,需要使用cglib为事务方法所在的类生成代理类对象,当我们在springmvc.xml中又扫描了一遍时,又会为事务方法所在的类生成一个新的对象,使得原来的在spring容器中生成的代理类对象失效,进而导致事务拦截器失效