按照日志显示,发现在项目有依赖使用tk.mybatis二方库中的tk.mybatis.mapper.mapperhelper.MapperInterceptor, 使用的maven依赖如下:
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.3.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
<version>3.1.2version>
<scope>testscope>
dependency>
在MapperHelper这个类中,使用了如下定义:
/**
* 缓存skip结果
*/
private final Map<String, Boolean> msIdSkip = new HashMap<String, Boolean>();
其作用是使用map缓存需要拦截处理的mapper方法,但是在并发情况下,存在并发问题,导致cpu占满的情况。
找到tk.mybatis.mapper.mapperhelper.MapperIntorceptor.intercept()方法,定位到在mapperHelper.isMapperMethod()中 使用HashMap,并发条件下导致HashMap死循环。
参考网上一篇文章,感兴趣的可以阅读理解 https://coolshell.cn/articles/9606.html
github开源代码: https://github.com/abel533/Mapper
存在的问题是限定了例如3.1.2.x这样一个版本,修改版本号之后还存在此问题,同时作者开发的新功能,新版本的功能无法使用,作者的修改对此版本来说是未知的,我们要与作者的新功能保持同步是一件很麻烦的事情。
存在的问题是需要找到使用次框架的范围,代码改造并替换框架的功能,并需要全面测试,成本比较大且风险较高。
Hack是基于开源的程序的基础,对其代码进行增加、删除或者修改、优化,使之在功能上符合新的需求。
/**
* An instance of this class is used to fix tk.mybatis.mapper.mapperhelper.
* MapperInterceptor.mapperHelper.msIdSkip(HashMap threads not threadSafe)
* through reflect to change msIdSkip.HashMap to msIdSkip.ConcurrentHashMap for threadSafe
*
* The purpose of this class is designed to fix the thread unsafe problems through the way of hack,
* framework to modify the source code for as little as possible, and at the same time as little as possible
* to modify the project configuration, better use of the open-closed principle, reduce dependence on tk
*
* if you want to use this function, you need to add an bean instance of this class at your
* spring.xml,{@link }
* and you don't need to dependency tk.mybatis
*
* @author liuyong
* 2017-08-28
*/
此处第一次提交时没有增加注释,后来补上了hack开发增加hack的背景、解决什么问题、原因、方案和使用方法,方便其他人使用和理解,同时增加自己的被知名度。 框架代码使用英文注释,不要出现中英文混用的情况,同时要去掉警告代码
class TkMapperInterceptorHacker extends InstantiationAwareBeanPostProcessorAdapter {
自定义BeanPostProcessor,在bean加载过程中对bean拦截进行操作。 类名使用TkMapperInterceptorHacker比TkMapperInterceptorHack更好一些。
private static final Logger LOGGER = LoggerFactory.getLogger(TkMapperInterceptorHacker.class);
定义Logger,final static变量名需要大写
// interface method is invoked after the bean is instantiated
最开始这里使用的是中文注释,框架代码中不要出现中英文混合的注释
@Override
public Object postProcessAfterInitialization(Object bean, String beanName){
String beanClassName = bean.getClass().getName();
if (beanClassName.contains("tk.mybatis.mapper.mapperhelper.MapperInterceptor")) {
此处判断bean为我们需要拦截处理的bean时,有以下几点收获:
try {
LOGGER.info("TkMapperInterceptorHacker invoke postProcessAfterInitialization method reload bean:" + beanName);
Field mapperHelperFiled = bean.getClass().getDeclaredField("mapperHelper");
//使用setAccessible(true)修改private final不可操作的特性
mapperHelperFiled.setAccessible(true);
Field msIdSkipField = mapperHelperFiled.get(bean).getClass().getDeclaredField("msIdSkip");
//此处使用属性object反射获取类,操作类属性。没有必要现获取到具体的对象在反射操作类属性,这样对具体的类还有依赖。
msIdSkipField.setAccessible(true);
//修改MapperHelper.msIdSkip属性的HashMap为ConcurrentHashMap属性
msIdSkipField.set(mapperHelperFiled.get(bean), new ConcurrentHashMap<>());
} catch (Exception e) {
throw new RuntimeException(e);
//此处应该遵循fast-fail原则,快速失败,但是不能System.exit(),属于野蛮的做法。利用spring的异常捕获,抛出异常,让容器启动失败即可。
}
}
//父类的操作,需要根据父类方法的实现判断是否需要调用,避免父类方法做一些我们未知的操作
return super.postProcessAfterInitialization(bean, beanName);
}
}
/**
* test class methods keys
* every testCase need follow next rules:
* 1、begin check :self check
* 2、 post : do test method
* 3、check again : check result
*
* Created by yehao on 2017/8/25.
*/
public class TkMapperHackTest {
private static final Logger LOGGER = LoggerFactory.getLogger(TkMapperHackTest.class);
//TestCase1: not the bean we won't to deal, it should return the bean no change
@Test
public void testBeanPostProcessorNotTheBean() {
Object bean = new Object();
Field mapperHelper ;
try {
mapperHelper = bean.getClass().getDeclaredField("mapperHelper");
Assert.assertNull(mapperHelper);
} catch (Exception e) {
Assert.assertEquals(NoSuchFieldException.class, e.getClass());
LOGGER.info("bean self check ok, it should not to change");
}
List
针对以上的代码框架,可以解决CPU飙升的问题,然而此版本存在另一个问题是如果tk.MapperHelper是使用的代理类,怎会出现并没有修改msIdSkip的类,为了兼容代理类,修改TkMapperInterceptorHacker类代码如下:
package com.helijia.framework.hack.tk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentHashMap;
/**
* An instance of this class is used to fix tk.mybatis.mapper.mapperhelper.
* MapperInterceptor.mapperHelper.msIdSkip(HashMap threads not threadSafe)
* through reflect to change msIdSkip.HashMap to msIdSkip.ConcurrentHashMap for threadSafe
*
*
The purpose of this class is designed to fix the thread unsafe problems through the way of hack,
* framework to modify the source code for as little as possible, and at the same time as little as possible
* to modify the project configuration, better use of the open-closed principle, reduce dependence on tk
*
*
if you want to use this function, you need to add an bean instance of this class at your
* spring.xml,{@link }
* and you don't need to dependency tk.mybatis
*
*
This class don't fit for {@code JDKDynamic} proxy class, if class is the agent of JDKDynamic, this class won't make effects
*
* @author liuyong
* 2017-08-28
*/
class TkMapperInterceptorHacker extends InstantiationAwareBeanPostProcessorAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(TkMapperInterceptorHacker.class);
/**
* interface method is invoked after the bean is instantiated
*
* @param bean
* @param beanName
* @return
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
String beanClassName = bean.getClass().getName();
if (!StringUtils.isEmpty(beanClassName) && beanClassName.contains("tk.mybatis.mapper.mapperhelper.MapperInterceptor")) {
try {
LOGGER.info("TkMapperInterceptorHacker invoke postProcessAfterInitialization method reload bean:" + beanName);
exceClass(bean, bean.getClass());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return super.postProcessAfterInitialization(bean, beanName);
}
private void exceClass(Object bean, Class clazz) throws Exception {
if (null == clazz || clazz == Object.class) {
return;
}
boolean isMatch = false;
Field[] fields = clazz.getDeclaredFields();
for (final Field field : fields) {
if ("mapperHelper".equals(field.getName())) {
field.setAccessible(true);
Field msIdSkipField = field.get(bean).getClass().getDeclaredField("msIdSkip");
msIdSkipField.setAccessible(true);
msIdSkipField.set(field.get(bean), new ConcurrentHashMap<>());
isMatch = true;
break;
}
}
if (!isMatch) {
exceClass(bean, clazz.getSuperclass());
}
}
}
github地址
https://github.com/yehaoSource/hack