Java中几种代理实现的方式
一、问题来源
最近在做项目的过程中,遇到一个问题,随着项目的日益庞大,组件间关系依赖复杂,项目的运行日志在多线程中杂乱无章,问题的定位与排查越来越困难;因此,团队讨论后决定使用日志聚合工具,对同一业务的单个流程的日志进行聚合,为了方便日志聚合,团队决定对项目日志的输出进行增强,对每个运行流程添加相同的traceId输出。
我们的项目采用了目前最流程的日志框架log4j进行日志管理,所以首先想到了log4j的MDC类,看了一下MDC的实现原理,大致为MDC持有一个继承的Threadlocal引用,重写了其中的set、get方法,使其中Threadlocal的引用对象为Map,像Map中添加参数,日志格式中使用参数名可以直接输出参数值。所以log4j的MDC方案基本符合我们的日志功能增强的需求。
基本方案确定后,我们又遇到另一个问题,我们何时向MDC中添加自定义的traceId呢?以何种方式将traceId放入MDC中呢?
经过团队的讨论,我们决定采用切面编程的思想将traceId在调用流程线程入口时添加进MDC中;最终,问题进行到这一步,团队决定分工执行,恰巧我分工到了如何实现切面的问题上,因此,我总结了一下现在Java中比较常用的切面实现方案:
二、切面的几种实现方式
1、硬编码
改变原有代码逻辑,在执行方法之前,先校验MDC是否存在traceId,不存在则生成放入。此方法大多数情况下不可取,日志增强功能只是辅助功能,不能改变原有方法的逻辑,希望实现可配置。
2、静态/动态代理
为类添加代理类,创建类工厂,从工厂中取代理类,不直接使用原有类的实例,静态代理的实现方式参照http://www.runoob.com/design-pattern/proxy-pattern.html;动态代理可以考虑使用JDK的动态代理,这两种实现都需要被代理类有接口;还可以考虑cglib,此种实现方式,实现起来原有代码改动过大,不太适合我们的情形。
3、Spring AOP
此方法实现参照https://docs.spring.io/spring/docs/2.5.x/reference/aop.html,此种方式可以直接实现切面,在xml配置所需要拦截的切点就好,但考虑到我们的原有系统,有许多不是spring容器管理的入口,所以此方式对我们来说也不完全适用。
4、修改Java字节码文件
考虑到Java类的加载运行机制,先由Javac将源文件编译成class文件,再由Classloader将calss加载进入内存,Jvm创建对象,分配空间,最后执行。整个过程中,Jvm加载class文件是动态进行加载的,所以我们想到,可以先定义好切面,通过项目初始化启动时,解析自定义的xml切点配置信息,利用字节码修改工具将class文件进行修改,植入切面。最终我决定选用javasisst实现new的对象的切面。主要的实现如下:解析xml测试类
package javasisst;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
/**
* @author bennet
* @create_time 2018-09-08 21:14:11
* @todo 解析xml 测试修改字节码结果
* @class javasisst.Test
*/
public class Test {
private final static Logger logger = LogManager.getLogger(Test.class);
/**
* 切面map
*/
private static Map<String, AspectBean> aspectMap = new HashMap<String, AspectBean>();
/**
* 切点map
*/
private static List<AspectBean> aspectPoints = new ArrayList<AspectBean>();
public static void main(String[] args) throws Exception {
aspectXmlParser();
ClassPool classPool = ClassPool.getDefault();
// Map<String, CtClass> ctMethods = new HashMap<String, CtClass>();
// for (Map.Entry<String, AspectBean> mEntry : aspectMap.entrySet()) {
// CtClass ctClass = classPool.get(mEntry.getValue().getClassName());
// CtMethod declaredMethod = ctClass.getDeclaredMethod(mEntry.getValue().getMethodName());
// ctMethods.put(mEntry.getKey(), declaredMethod);
// }
for (AspectBean aspectBean : aspectPoints) {
CtClass ctClass = classPool.get(aspectBean.getClassName());
CtMethod declaredMethod = ctClass.getDeclaredMethod(aspectBean.getMethodName());
// 切面bean
AspectBean aspectBean2 = aspectMap.get(aspectBean.getAspectId());
// 切面类
CtClass ctClass2 = classPool.get(aspectBean2.getClassName());
//非静态方法
// String fieldSrc="public "+ctClass2.getName()+" "+ctClass2.getSimpleName()+";";
// logger.info("添加成员源码为:"+fieldSrc);
// 添加成員
// ctClass.addField(new CtField(ctClass2, ctClass2.getSimpleName(), ctClass));
// 方法前加入源碼
// String src = "{if("+ctClass2.getSimpleName()+"==null){"+ctClass2.getSimpleName()+"=new "+ctClass2.getName()+"();"+"}"+ ctClass2.getSimpleName() + "." + aspectBean2.getMethodName() + "();}";
//静态方法直接调用
String src="{"+ctClass2.getName()+"."+aspectBean2.getMethodName()+"();}";
logger.info("源码为:" + src);
declaredMethod.insertBefore(src);
// 系统默认生成class的路径 重写class
ctClass.writeFile(ClassLoader.getSystemResource("").getPath());
}
AspectPointTest test = new AspectPointTest();
test.helloWorld();
}
/**
* @author bennet-xiao
* @throws ParserConfigurationException
* @throws IOException
* @throws SAXException
* @create_time 2018-09-08 17:42:21
* @todo 将xml解析为Map
*/
private static void aspectXmlParser() throws Exception {
SAXReader reader = new SAXReader();
File file = new File(ClassLoader.getSystemResource("").getPath() + "/aspect.xml");
Document document = reader.read(file);
// 根节点
Element root = document.getRootElement();
if (root == null) {
throw new NullPointerException("没有找到定义的切面!");
}
//切面
@SuppressWarnings("rawtypes")
Iterator aspects = root.elementIterator("Aspect");
while (aspects.hasNext()) {
Element aspect = (Element) aspects.next();
String aspectId = aspect.element("id").getTextTrim();
String className = aspect.element("class").getTextTrim();
String methodName = aspect.element("method").getTextTrim();
aspectMap.put(aspectId, new AspectBean(className,methodName));
}
//切点
@SuppressWarnings("rawtypes")
Iterator aspectPointIters= root.elementIterator("AspectPoint");
while (aspectPointIters.hasNext()) {
Element aspect = (Element) aspectPointIters.next();
String aspectId = aspect.element("AspectId").getTextTrim();
String className = aspect.element("class").getTextTrim();
String methodName = aspect.element("method").getTextTrim();
aspectPoints.add(new AspectBean(className,methodName,aspectId));
}
}
@org.junit.Test
public void name() {
AspectPointTest test = new AspectPointTest();
test.helloWorld();
}
}
xml结构:
<Aspects>
<!-- 定义切面配置 -->
<Aspect>
<!-- 切面id -->
<id>testAddTraceIdInMDC</id>
<!--切面类 -->
<class>javasisst.AspcetTest</class>
<!-- 切面方法 -->
<method>addTraceIdInMDC</method>
</Aspect>
<!-- 定义切点 -->
<AspectPoint>
<!-- 切面id -->
<AspectId>testAddTraceIdInMDC</AspectId>
<!-- 切入类型 1、前切 2、后切 3、round-->
<Type>1</Type>
<!--切点类 -->
<class>javasisst.AspectPointTest</class>
<!-- 切点方法 -->
<method>helloWorld</method>
</AspectPoint>
</Aspects>
log4j配置:
status = error
dest = err
name = PropertiesConfig
filter.threshold.type = ThresholdFilter
filter.threshold.level = debug
appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%X{traceId}][%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = debug
rootLogger.level = debug
rootLogger.appenderRef.stdout.ref = STDOUT
切面方法:
package javasisst;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
/**
* @author bennet
* @create_time 2018-09-08 16:42:57
* @todo 切面测试类
* @class javasisst.AspcetTest
*/
public class AspcetTest {
private final static Logger logger = LogManager.getLogger(AspcetTest.class);
/**
* the key for log
*/
public final static String LOGKEY = "traceId";
/**
* @author bennet-xiao
* @create_time 2018-09-08 16:43:48
* @todo 在MDC中添加traceId
*/
public static void addTraceIdInMDC() {
logger.info("进入切面方法");
// log4j 2.x中不存在MDC类 详见
// https://logging.apache.org/log4j/2.x/manual/thread-context.html
String value = ThreadContext.get(LOGKEY);
// 不存在放入traceId
if (StringUtils.isEmpty(value)) {
String traceId = uuidGenrator();
logger.info("生成traceId【{}】", traceId);
ThreadContext.put(LOGKEY,traceId);
}
}
/**
* @author bennet-xiao
* @create_time 2018-09-08 16:56:10
* @todo 简单实现生成traceId方法
* @return
*/
private static String uuidGenrator() {
return UUID.randomUUID().toString();
}
}
切点方法:
package javasisst;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
/**
* @author bennet
* @create_time 2018-09-08 18:38:31
* @todo 切點測試
* @class javasisst.AspectPointTest
*/
public class AspectPointTest {
private final static Logger logger=LogManager.getLogger(AspectPointTest.class);
/**
* @author bennet-xiao
* @create_time 2018-09-08 17:38:57
* @todo 简单的方法
*/
public void helloWorld() {
logger.info("hello world!"+ThreadContext.get(AspcetTest.LOGKEY));
}
}
最终效果:
三、结论
通过以上步骤,使问题得到解决,最终代码详见github,没有最好的实现方式,每个问题都得具体分析,最终得到解决方案,以上就是我解决问题的流程。