基于javassist实现aop

背景:
系统集成测试,需要写mock(单元测试有spock等mock工具)。
于是基于spring的aop实现了一个mock框架,可以用于集成测试。
但是只能对spring管理的bean进行mock。
对于有些工具类,是没办法mock的,比如一些加解密,签名验签,都是用静态方法实现的。
于是寻找一些解决方案,希望尽可能的减少侵入性,实现对静态方法、非spring对象的mock。
思路是在类加载之前,修改其字节码,实现aop。
于是选择了基于javassist的实现。
核心代码如下
MyMethodProxy.java

package org.jsirenia.javassist;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import org.jsirenia.reflect.ClassUtil;
import org.jsirenia.string.JRender;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
/**
 * 结合PackageUtil,可以设置指定包下的指定类的指定方法的 方法拦截。支持环绕通知。
 */
public class MyMethodProxy {
	private static final Map<String,MethodInvoker> contextMap = new ConcurrentHashMap<>();
	
	public static Object invoke(String uid,String className, String methodName,String parameterTypeNames,Object self,Object[] args) throws Throwable{
		Class<?> selfClass = Class.forName(className);
		String[] parameterTypeArray = parameterTypeNames.split(",");
		Class<?>[] parameterTypes = new Class[parameterTypeArray.length];
		for(int i=0;i<parameterTypeArray.length;i++){
			parameterTypes[i] = ClassUtil.forName(parameterTypeArray[i]);
		}
		Method thisMethod = selfClass.getMethod(methodName,parameterTypes);
		Method proceed = selfClass.getMethod(methodName+"$proxy",parameterTypes);
		MethodInvoker invoker = contextMap.get(uid);
		return invoker.invoke(self, thisMethod, proceed, args);
	}
	public static void proxy(String className, MyMethodFilter[] filters, MethodInvoker invoker) throws NotFoundException, CannotCompileException, ClassNotFoundException, IOException {
		ClassPool pool = ClassPool.getDefault();
		ClassLoader classLoader = MyMethodProxy.class.getClassLoader();
		pool.appendClassPath(new LoaderClassPath(classLoader));
		CtClass ct = pool.getCtClass(className);
		// 解冻
		ct.defrost();
		String uid = UUID.randomUUID().toString();
		CtMethod[] methods = ct.getDeclaredMethods();//ct.getMethods();
		for (int i = 0; i < methods.length; i++) {
			CtMethod method = methods[i];
			boolean accept = true;
			if(filters!=null){
				for(MyMethodFilter filter : filters){
					if(!filter.equals(method)){
						accept = false;
						break;
					}
				}
			}
			if(!accept){
				continue;
			}
			String methodName = method.getName();
			int modifiers = method.getModifiers();
			boolean isStatic = Modifier.isStatic(modifiers);
			CtMethod copyMethod = CtNewMethod.copy(method, method.getName() + "$proxy", ct, null);
			ct.addMethod(copyMethod);
			CtClass[] paramCtClasses = method.getParameterTypes();
			String[] paramClassNames = new String[paramCtClasses.length];
			for (int j = 0; j < paramCtClasses.length; j++) {
				String name = paramCtClasses[j].getName();
				paramClassNames[j] = name;
			}
			String parameterTypes = String.join(",", paramClassNames).toString();
			String methodProxyName = MyMethodProxy.class.getName();
			List<String> body = new ArrayList<>();
			body.add("{");
			if (isStatic) {
				body.add("return ($r)${methodProxyName}.invoke(${uid},${className},${methodName},${parameterTypes},null,$args);");
			} else {
				body.add("return ($r)${methodProxyName}.invoke(${uid},${className},${methodName},${parameterTypes},$0,$args);");
			}
			body.add("}");
			String bodyString = String.join("\n", body);
			Map<String, Object> variables = new HashMap<>();
			variables.put("methodProxyName", methodProxyName);
			variables.put("uid", wrapQuota(uid));
			variables.put("className", wrapQuota(className));
			variables.put("methodName", wrapQuota(methodName));
			variables.put("parameterTypes", wrapQuota(parameterTypes));
			bodyString = new JRender("${","}").withVariables(variables ).render(bodyString);
			method.setBody(bodyString);
		}
		ct.writeFile();
		ct.toClass();
		contextMap.put(uid, invoker);
	};
	private static String wrapQuota(String text){
		return "\""+text+"\"";
	}
}

MyMethodFilter.java

package org.jsirenia.javassist;

import javassist.CtMethod;

public interface MyMethodFilter {
	boolean filter(CtMethod method);
}

MethodInvoker.java

package org.jsirenia.javassist;

import java.lang.reflect.Method;

public interface MethodInvoker {
	Object invoke(Object self, Method thisMethod,Method proceed, Object[] args) throws Throwable;
}

如果是web项目,在ContextLoadListener中,对需要修改的类,调用MyMethodProxy的proxy方法。
效果:
类的字节码被修改,它的方法(包括静态方法)在执行时,会执行MethodInvoker的invoke方法。
可以在invoke方法中进行想要的操作,可以调用原来的逻辑、可以替换掉原来执行逻辑,反正就是
一个环绕通知的效果。
好处是可以对静态方法、私有方法进行拦截,也可以修改MyMethodProxy的实现,对继承的方法也进行
拦截。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值