在 Java 的编程世界中离不开 spring 家族,因此spring是必须要深入了解的;而spring的核心之一就是 AOP 切面编程,这一块内容的原理是必须要了解的;
如果直接读源码,可能会让你很痛苦。spring中各种错综复杂的调用关系可能会看的你头晕脑胀的。结果事倍功半,直接把你劝退。
单独来看spring的AOP部分,其实就2点:
- 1.要筛选出目标代理对象;
- 2.动态代理这些目标类;
这2部分都不难实现,因此最好的办法是自己实现spring的这部分功能。实现了之后再对比去看spring的源码,你会很快抓住重点。并且这个时候去看源码,你会不自觉的对比自己的实现有哪些不足又或者哪些是你比源码实现的更好。正是有了这种对比,你会对源码有更深刻的理解。
总之不管是想深入理解AOP,还是想事半功倍的学习AOP都应该自己去实现AOP的功能。这样才能又快又牢靠的掌握这部分知识。
本文就是讲要如何实现spring对目标对象的筛选。从以下三个方面去实现目标类和目标方法的过滤:
- 首先要判断检查写的pointcut表达式是否符合规则,即对pointcut表达式的约束;
- 其次解析pointcut表达式;
- 最后筛选匹配目标类,目标方法;
1.Pointcut表达式各部分的约束规则
在spring中配置切面或者数据库的事务会要求:对具体方法或者是一类特征相同的方法添加日志,事务,或者其他对原方法的增强。这时候就会用到pointcut表达式对方法进行过滤,筛选出符合要求的方法;
既然会涉及到筛选具体的方法,那pointcut一定要匹配出完整的方法路径: 全限定类名+方法名 ;在同一个类中,方法可能被重写而区分重写的方法就是: 参数列表 ;因此pointcut表达式中必须包含这3部分: 全限定类名+方法名+参数列表 ;在spring中还有:访问修饰符,返回值类型;这2个不是必须的;pointcut整体结构:
有了pointcut的整体结构之后就可以根据自己的规则,分别写这几部分的 正则表达式 了;
- execution()这部分是固定写法,它包含了完整的表达式;
- 访问修饰符,返回值, classpath ,methodName之间用空格分开;
- methodName,paramList用()分开;methodName(paramList);
1.1 ACCESS_MOD if IER 访问修饰符
访问修饰符有四种取值:public, Private ,protected,defalut; ” * ” ,对访问修饰符不加限制;
1.2 RETURN _TYPE 返回值类型
返回值类型可以是任意类型:8个基础类型 + 对象 + 数组 + void ;
- 基础类型的匹配 BASIC _TYPE: ( byte |char|boolean| short |int|long|double|float)
- 对象匹配: [A-Z]\\w *
- 返回类型可以是以上2种类型的数组(可以是2维,3维,4维。。);PARAM_TYPE = (BASIC_TYPE|[A-Z]\\w )(\\[\\]) *
最后结合返回值:void, * (表示任意类型);返回值的所有可能取值的正则表达式: (void|PARAM_TYPE|\\*)
1.3 CLASS_PATH 全限定类名
有一个特点:( package Name + “.”) * + className;包名,类名都可以用同一个正则表达式;
CLASS_PATH = "((\\*?\\w+\\*?|\\*)\\.)*(\\*?\\w+\\*?|\\*)";
复制代码
1.3 EXECUTION 表达式
METHOD_NAME,PARAM_LIST这2个的匹配分别参照:全限定类名,返回值类型的正则表达式;将这些部分分别写完之后,再按顺序组合一下就可以得到完整的表达式了;再考虑到,写表达式的时候,会习惯性的敲空格,因此可以在合适的地方允许空格;
public class PointcutUtils {
private static final String ACCESS_MODIFIER = "(public|private|protected|default|\\*)";
private static final String RETURN_TYPE;
private static final String CLASS_PATH = "((\\*?\\w+\\*?|\\*)\\.)*(\\*?\\w+\\*?|\\*)";
private static final String METHOD_NAME = "(\\*?\\w+\\*?|\\*)";
private static final String PARAM_LIST ;
private static final String EXECUTION ;
//基础类型
private static final String BASIC_TYPE="(byte|char|boolean|short|int|long|double|float)";
//参数类型
private static final String PARAM_TYPE;
static{
//参数类型:基础类型 + Object + 数组类型
PARAM_TYPE = "("+BASIC_TYPE+"|[A-Z]\\w*)(\\[\\])*";
//返回值类型:void + 参数类型
RETURN_TYPE="(void|"+PARAM_TYPE+"|\\*)";
//参数列表
PARAM_LIST = "(\\.\\.|"+PARAM_TYPE+"(\\s*,\\s*"+PARAM_TYPE+")*" +"|)";
//execution表达式
EXECUTION = "\\s*execution\\s*\\(\\s*"+ACCESS_MODIFIER+"\\s+"+RETURN_TYPE+"\\s+"+CLASS_PATH+"\\s+"+METHOD_NAME+"\\(\\s*"+PARAM_LIST+"\\s*\\)\\s*"+"\\s*\\)\\s*";
}
//检测pointcut是否是正确的
static boolean checkPointcut(String pointcutReg){
return pointcutReg == null ? false : pointcutReg.matches(EXECUTION);
}
}
复制代码
得到的EXECUTION表达式可以用来检测pointcut表达式是否写正确;
String pointcut = "execution ( default int[][] * rr( .. ) ) ";
//default访问修饰符,int[][] 二维数组 ;
// * 不限定类名; rr 方法名; .. 任意类型的参数列表;符合定义的规则,预期结果为 true
boolean correct = PointcutUtils.checkPointcut(pointcut );
System.out.println(correct);//结果:true
复制代码
2.拆分pointcut表达式
拆分流程:
代码:
//拆分pointcut
public static Pointcut parsePointcut(String pointcut){
if(!checkPointcut(pointcut))throw new IllegalArgument Exception ("execution grammar format error.");
String exeReg = getBracketStr(pointcut);//获取execution();中括号包裹的部分;
String paramList = getBracketStr(exeReg).replaceAll(" ","");
int start = exeReg.indexOf("(");
exeReg = exeReg.substring(0,start);
String[] regs = exeReg.split("\\s+");
String accessModifier = regs[0];
String returnType = regs[1];
String classPath = regs[2];
String methodName = regs[3];
return new Pointcut(accessModifier,returnType,classPath,methodName,paramList);
}
static String getBracketStr(String str){
int start = str.indexOf("(");
int end = str.lastIndexOf(")");
return str.substring(start+1,end).trim();
}
复制代码
3.过滤匹配目标对象
分别匹配class和method;在拿到pointcut的时候,如何匹配class和method呢?这个时候要对类路径的正则表达式做一下处理;比如:pointcut的classpath部分:
"*weqq*.dgdfgfg.df*"
*weqq*:我们希望能匹配到包名含有 weqq的包;直接使用这个作为正则表达式去匹配类路径肯定是不行的;
* ====》 重复匹配0次或多次前一个字符或者表达式;
如何能达到要求呢?只需要做一下简单处理就好了:*weqq* ====》 \\w*weqq\\w*;将 * 替换成 \\w* 就可以匹配字符了
还有需要注意的是 ".",在正则表达式中表示匹配任意字符;
而我们希望它只是包的分割符,它只表示" . ",而不需要有任何其他的含义,因此需要将 "."转换成普通字符 : . ===> \\.
复制代码
- 匹配class的类名
public static boolean matchClass(Pointcut pointcut,Class cla){
if(pointcut.getClassPath().equals("*"))return true;
return cla.get TypeName ().matches(pointcut.getClassPath().replaceAll("\\*","\\\\w*").replaceAll("\\.","\\\\."));
}
复制代码
- 匹配方法:访问修饰符,返回值,方法名,参数列表
public static boolean matchMethod(Pointcut pointcut, Method method){
return matchModifier(pointcut.getAccessModifier(),method) &&
matchReturnType(pointcut.getReturnType(),method) &&
matchMethodName(pointcut.getMethodName(),method) &&
matchParamList(pointcut.getParamList(),method);
}
static boolean matchModifier(String modifier,Method method){
int modifiers = method.getModifiers();
switch (modifier){
case "default" :return method.isDefault();
case "public" :return Modifier.isPublic(modifiers);
case "private" :return Modifier.isPrivate(modifiers);
case "protected":return Modifier.isProtected(modifiers);
case "*" :return true;
default:
return false;
}
}
static boolean matchReturnType(String returnType,Method method){
if(returnType.equals("*"))return true;
return returnType.equals(method.getReturnType().getSimpleName());
}
static boolean matchMethodName(String name,Method method){
if(name.equals("*"))return true;
return method.getName().matches(name.replaceAll("\\*","\\\\w*"));
}
static boolean matchParamList(String paramList,Method method){
if(paramList.equals(".."))return true;
Class<?>[] parameterTypes = method.getParameterTypes();
if((paramList.equals("")) ){
if( parameterTypes.length == 0 )return true;
else return false;
}
StringBuilder methodParamList = new StringBuilder();
for (Class<?> parameterType : parameterTypes) {
methodParamList.append(","+parameterType.getSimpleName());
}
String methodParam = methodParamList.toString().substring(1);
return methodParam.equals(paramList);
}
复制代码
同时匹配上类和方法,就可以对方法增强了;其实可以看到在实现pointcut表达式只用到了少量的正则表达式的知识;execution整体拼凑起来有点多,但是分开来看每部分还是简单的;只需要了解简单的正则和反射,就可以自定义一个pointcut过滤器了。有了这个之后,就可以自己定义实现选择性AOP了;