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 aspectMap = new HashMap();

	/**
	 * 切点map
	 */
	private static List aspectPoints = new ArrayList();

	public static void main(String[] args) throws Exception {		
		aspectXmlParser();
		ClassPool classPool = ClassPool.getDefault();
//		Map ctMethods = new HashMap();
//		for (Map.Entry 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结构:


	
	
	    
		testAddTraceIdInMDC
		
		javasisst.AspcetTest
		
		addTraceIdInMDC
	
	
	
	    
		testAddTraceIdInMDC
		
		1
		
		javasisst.AspectPointTest
		
		helloWorld
	

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,没有最好的实现方式,每个问题都得具体分析,最终得到解决方案,以上就是我解决问题的流程。

你可能感兴趣的:(Java中几种代理实现的方式)