欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java中几种代理实现的方式

程序员文章站 2022-03-14 23:08:51
...

一、问题来源

      最近在做项目的过程中,遇到一个问题,随着项目的日益庞大,组件间关系依赖复杂,项目的运行日志在多线程中杂乱无章,问题的定位与排查越来越困难;因此,团队讨论后决定使用日志聚合工具,对同一业务的单个流程的日志进行聚合,为了方便日志聚合,团队决定对项目日志的输出进行增强,对每个运行流程添加相同的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));
	}
}

最终效果:

Java中几种代理实现的方式

三、结论

    通过以上步骤,使问题得到解决,最终代码详见github,没有最好的实现方式,每个问题都得具体分析,最终得到解决方案,以上就是我解决问题的流程。