内容

1
2
if (method.getName().equals("doAction")) {
}

在软件开发过程中,当我们需要对多个方法进行统一的增强处理,比如添加监控日志等,直接指定每个方法名的做法变得不切实际。为了解决这个问题,我们可以采用**切点(Pointcut)**的概念,通过配置灵活的方法名匹配规则来代理一组相关的方法。

目标

  • 实现基于方法名通配符的切点定义。

  • 允许开发者在XML配置文件中自定义切点。

切点设计

切点是面向切面编程(AOP)中的一个核心概念,它描述了“何处”应用通知(Advice)。我们的目标是让切点支持模式匹配,这样就可以根据某种模式选择性地拦截一系列方法。

配置示例

假设我们想要拦截所有以service开头且至少有一个参数的方法,可以在XML配置文件中设置如下:

1
2
3
4
5
<aop:config>
  <aop:pointcut id="myServiceMethods"
                expression="execution(* com.example.myapp.service.*.*(..))" />
  <aop:advisor advice-ref="loggingAdvice" pointcut-ref="myServiceMethods"/>
</aop:config>

这里的expression属性使用了AspectJ表达式语言来定义切点。

改造后的配置文件概览

以下展示了配置文件经过改造后,如何利用切点来实现更灵活的方法代理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
  <!-
- 定义切点 -->
  <aop:config>
    <aop:pointcut id="serviceLayerMethods"
                 expression="execution(* com.yourcompany..*.service.*(..))" />
    <aop:advisor advice-ref="monitoringAdvice" pointcut-ref="serviceLayerMethods"/>
  </aop:config>
  <!-
- 监控逻辑实现 -->
  <bean id="monitoringAdvice" class="com.yourcompany.aop.MonitoringAdvice" />
  <!-
- 其他bean定义... -->
</beans>

此配置允许对com.yourcompany包下的任何service层方法应用MonitoringAdvice,而无需逐一列举方法名。这样的方式极大地提高了灵活性和可维护性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans>
   <bean id="realaction" class="com.test.service.Action1" />
   <bena id="beforeAdvice" class="com.test.service.MyBeforeAdvice" />
   <bean id="advisor" class="com.minis.aop.NameMatchMethodPointcutAdvisor">
      <property type="com.minis.aop.Advice" name="advice" ref="beforeAdvice"/>
      <property type="String" name="mappedName" value="do*"/>
   </bean>
   <bean id="action" class="com.minis.aop.ProxyFactoryBean">
      <property type="String" name="interceptorName" value="advisor" />
      <property type="java.lang.Object" name="target" ref="realaction"/>
   </bean>
</beans>

通过上述改动,我们新定义了一个NameMatchMethodPointcutAdvisor类作为Advisor。其中property属性中的value值为do*,这就是我们说的方法规则,也就是匹配所有以do开头的方法名称。这里你也可以根据实际的业务情况按照一定的规则配置自定义的代理方法,而不仅仅局限于简单的方法名精确相等匹配。有了这个Pointcut,我们就能用一条规则来支持多个代理方法了,这非常有用。如果能实现这个配置,就达到了我们想要的效果。为了实现这个目标,最后构建出一个合适的NameMatchMethodPointcutAdvisor,我们定义了MethodMatcher、Pointcut与PointcutAdvisor三个接口。MethodMatcher这个接口代表的是方法的匹配算法,内部的实现就是看某个名是不是符不符合某个模式。

1
2
3
4
package com.minis.aop;
public interface MethodMatcher {
    boolean matches(Method method, Class<?> targetCLass);
}

Pointcut接口定义了切点,也就是返回一条匹配规则。

1
2
3
4
package com.minis.aop;
public interface Pointcut {
    MethodMatcher getMethodMatcher();
}

PointcutAdvisor接口扩展了Advisor,内部可以返回Pointcut,也就是说这个Advisor有一个特性:能支持切点Pointcut了。这也是一个常规的Advisor,所以可以放到我们现有的AOP框架中,让它负责来增强。

1
2
3
4
package com.minis.aop;
public interface PointcutAdvisor extends Advisor{
    Pointcut getPointcut();
}

接口实现与方法匹配

在定义了接口之后,我们需要实现这些接口对应的功能。虽然理论上可以实现多种规则,但目前我们只能通过名称进行简单的模式匹配来理解其原理。

核心问题:如何匹配方法?

我们默认的实现方式是使用 NameMatchMethodPointcutNameMatchMethodPointcutAdvisor

1. NameMatchMethodPointcut

  • 这是一种基于方法名称的匹配方式。

  • 它允许我们通过指定的方法名称来定位和匹配方法。

2. NameMatchMethodPointcutAdvisor

  • 这是另一种基于方法名称的匹配方式,通常与 NameMatchMethodPointcut 结合使用。

  • 它提供了额外的灵活性,允许我们在匹配方法时考虑更多的上下文信息。 通过这两种实现,我们可以在不深入了解复杂规则的情况下,通过方法名称来识别和匹配目标方法。这种方式虽然简单,但足以帮助我们理解方法匹配的基本原理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.minis.aop;
public class NameMatchMethodPointcut implements MethodMatcher, Pointcut{
    private String mappedName = "";
    public void setMappedName(String mappedName) {
        this.mappedName = mappedName;
    }
    @Override
    public boolean matches(Method method, Class<?> targetCLass) {
        if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) {
            return true;
        }
        return false;
    }
    //核心方法,判断方法名是否匹配给定的模式
    protected boolean isMatch(String methodName, String mappedName) {
        return PatternMatchUtils.simpleMatch(mappedName, methodName);
    }
    @Override
    public MethodMatcher getMethodMatcher() {
        return null;
    }
}

我们看到了,这个类的核心方法就是 isMatch(),它用到了一个工具类叫 PatterMatchUtils。我们看一下这个工具类是怎么进行字符串匹配的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * 用给定的模式匹配字符串。
 * 模式格式: "xxx*", "*xxx", "*xxx*" 以及 "xxx*yyy",*代表若干个字符。
 */
public static boolean simpleMatch( String pattern,  String str) {
    //先判断串或者模式是否为空
	if (pattern == null || str == null) {
		return false;
	}
    //再判断模式中是否包含*
	int firstIndex = pattern.indexOf('*');
	if (firstIndex == -1) {
		return pattern.equals(str);
	}
    //是否首字符就是*,意味着这个是*XXX格式
    if (firstIndex == 0) {
		if (pattern.length() == 1) {  //模式就是*,通配全部串
			return true;
		}
		//尝试查找下一个*
        int nextIndex = pattern.indexOf('*', 1);
		if (nextIndex == -1) { //没有下一个*,说明后续不需要再模式匹配了,直接endsWith判断
			return str.endsWith(pattern.substring(1));
		}
        //截取两个*之间的部分
		String part = pattern.substring(1, nextIndex);
		if (part.isEmpty()) { //这部分为空,形如**,则移到后面的模式进行匹配
			return simpleMatch(pattern.substring(nextIndex), str);
		}
        //两个*之间的部分不为空,则在串中查找这部分子串
		int partIndex = str.indexOf(part);
		while (partIndex != -1) {
            //模式串移位到第二个*之后,目标字符串移位到字串之后,递归再进行匹配
			if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) {
				return true;
			}
			partIndex = str.indexOf(part, partIndex + 1);
		}
		return false;
	}

    //对不是*开头的模式,前面部分要精确匹配,然后后面的子串重新递归匹配
	return (str.length() >= firstIndex &&
		pattern.substring(0, firstIndex).equals(str.substring(0, firstIndex)) &&
		simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex)));
}

在进行字符串模式匹配时,我们采用了一种基于扫描的算法。这种算法支持使用通配符*来进行部分或全部字符串的匹配。这里的模式可以是多种多样的形式,比如"xxx*", "*xxx", "*xxx*" 或者 "xxx*yyy" 等等。

匹配过程概述

  • 扫描算法:该算法从字符串的第一个字符开始向后扫描。
  • 分段匹配:每当遇到通配符*时,将字符串分为几段,并对每一段单独处理。
  • 递归机制:由于*代表任意数量(包括零)的任何字符,因此对于含有*的部分,我们需要采用递归来尝试所有可能的匹配情况。
  • 灵活性:此方法能够灵活地应对不同长度和结构的模式字符串。

使用PatternMatchUtils工具类

现在我们有了上述实现的基础之后,就可以利用PatternMatchUtils这个工具类来执行实际的字符串匹配任务了。这个工具类封装了复杂的匹配逻辑,使得用户能够更加便捷地进行模式匹配操作。

NameMatchMethodPointcutAdvisor简介

此外,还介绍了NameMatchMethodPointcutAdvisor的一个简单实现。此类内部维护了一个NameMatchMethodPointcut属性以及一个MappedName属性。通过这两个属性,它能够提供一种基于名字匹配的方法切面功能,从而在AOP(面向切面编程)场景中发挥重要作用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.minis.aop;
public class NameMatchMethodPointcutAdvisor implements PointcutAdvisor{
	private Advice advice = null;
	private MethodInterceptor methodInterceptor;
	private String mappedName;
	private final NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
	public NameMatchMethodPointcutAdvisor() {
	}
	public NameMatchMethodPointcutAdvisor(Advice advice) {
		this.advice = advice;
	}
	public void setMethodInterceptor(MethodInterceptor methodInterceptor) {
		this.methodInterceptor = methodInterceptor;
	}
	public MethodInterceptor getMethodInterceptor() {
		return this.methodInterceptor;
	}
	public void setAdvice(Advice advice) {
		this.advice = advice;
		MethodInterceptor mi = null;
		if (advice instanceof BeforeAdvice) {
			mi = new MethodBeforeAdviceInterceptor((MethodBeforeAdvice)advice);
		}
		else if (advice instanceof AfterAdvice){
			mi = new AfterReturningAdviceInterceptor((AfterReturningAdvice)advice);
		}
		else if (advice instanceof MethodInterceptor) {
			mi = (MethodInterceptor)advice;
		}
		setMethodInterceptor(mi);
	}
	@Override
	public Advice getAdvice() {
		return this.advice;
	}
	@Override
	public Pointcut getPointcut() {
		return pointcut;
	}
	public void setMappedName(String mappedName) {
		this.mappedName = mappedName;
		this.pointcut.setMappedName(this.mappedName);
	}
}

上述实现代码对新增的Pointcut和MappedName属性进行了处理,这正好与我们定义的XML配置文件保持一致。而匹配的工作,则交给NameMatchMethodPointcut中的matches方法完成。如配置文件中的mappedName设置成了 “do*",意味着所有do开头的方法都会匹配到。

1
2
3
4
<bean id="advisor" class="com.minis.aop.NameMatchMethodPointcutAdvisor">
    <property type="com.minis.aop.Advice" name="advice" ref="beforeAdvice"/>
    <property type="String" name="mappedName" value="do*"/>
</bean>

另外,我们还要注意setAdvice()这个方法,它现在通过advice来设置相应的Intereceptor,这一段逻辑以前是放在ProxyFactoryBean的initializeAdvisor()方法中的,现在移到了这里。现在这个新的Advisor就可以支持按照规则匹配方法来进行逻辑增强了。在上述工作完成后,相关的一些类也需要改造。JdkDynamicAopProxy类中的实现,现在我们不再需要将方法名写死了。你可以看一下改造之后的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.minis.aop;
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
    Object target;
    PointcutAdvisor advisor;
    public JdkDynamicAopProxy(Object target, PointcutAdvisor advisor) {
        this.target = target;
        this.advisor = advisor;
    }
    @Override
    public Object getProxy() {
        Object obj = Proxy.newProxyInstance(JdkDynamicAopProxy.class.getClassLoader(), target.getClass().getInterfaces(), this);
        return obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Class<?> targetClass = (target != null ? target.getClass() : null);
        if (this.advisor.getPointcut().getMethodMatcher().matches(method, targetClass)) {
            MethodInterceptor interceptor = this.advisor.getMethodInterceptor();
            MethodInvocation invocation =
                    new ReflectiveMethodInvocation(proxy, target, method, args, targetClass);
            return interceptor.invoke(invocation);
        }
        return null;
    }
}

核心方法 invoke() 的变化

在AOP(面向切面编程)框架中,核心方法 invoke() 用于执行目标方法之前或之后的增强逻辑。以下是 invoke() 方法的变化概述:

旧方法:方法名匹配

旧代码中,invoke() 方法通过 method.getName().equals("doAction") 来判断是否执行增强逻辑,这种方式要求方法名必须完全等于 “doAction”。

新方法:使用Pointcut匹配

新代码中,判断条件变得更加灵活和可扩展,通过 this.advisor.getPointcut().getMethodMatcher().matches(method, targetClass) 进行匹配校验。这种方式允许使用Pointcut的matcher进行更复杂的匹配逻辑。

Advisor到PointcutAdvisor的转变

随着 invoke() 方法的变化,原有的 Advisor 类也被更细粒度的 PointcutAdvisor 类所替代。这种变化意味着:

  • PointcutAdvisor 提供了更具体的切点(Pointcut)和增强(Advice)之间的关联。

  • 相关的引用类也需要相应地进行修改,以适应新的 PointcutAdvisor 类型。

DefaultAopProxyFactory的调整

DefaultAopProxyFactory 类的 createAopProxy() 方法中,现在可以接受 PointcutAdvisor 类型的参数,从而支持新的切面匹配和增强逻辑。

总结

通过这些变化,AOP框架的灵活性和扩展性得到了增强,使得开发者可以更精确地控制哪些方法需要被增强,以及如何增强。

1
2
3
4
5
6
7
package com.minis.aop;
public class DefaultAopProxyFactory implements AopProxyFactory{
    @Override
    public AopProxy createAopProxy(Object target, PointcutAdvisor advisor) {
        return new JdkDynamicAopProxy(target, advisor);
    }
}

而ProxyFactoryBean可以简化一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.minis.aop;
public class ProxyFactoryBean implements FactoryBean<Object>, BeanFactoryAware {
    private BeanFactory beanFactory;
    private AopProxyFactory aopProxyFactory;
    private String interceptorName;
    private String targetName;
    private Object target;
    private ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader();
    private Object singletonInstance;
    private PointcutAdvisor advisor;
    public ProxyFactoryBean() {
        this.aopProxyFactory = new DefaultAopProxyFactory();
    }

    //省略一些getter/setter

    protected AopProxy createAopProxy() {
        return getAopProxyFactory().createAopProxy(target, this.advisor);
    }
    @Override
    public Object getObject() throws Exception {
        initializeAdvisor();
        return getSingletonInstance();
    }
    private synchronized void initializeAdvisor() {
        Object advice = null;
        MethodInterceptor mi = null;
        try {
            advice = this.beanFactory.getBean(this.interceptorName);
        } catch (BeansException e) {
            e.printStackTrace();
        }
        this.advisor = (PointcutAdvisor) advice;
    }
    private synchronized Object getSingletonInstance() {
        if (this.singletonInstance == null) {
            this.singletonInstance = getProxy(createAopProxy());
        }
        return this.singletonInstance;
    }
}

ProxyFactoryBean中的initializeAdvisor方法改进

在ProxyFactoryBean的initializeAdvisor方法中,我们可以看到,不再需要对不同的Interceptor类型进行判断。相关的实现已经被抽取到了NameMatchMethodPointcutAdvisor类中。

测试

使用HelloWorldBean进行测试

我们继续使用之前的HelloWorldBean作为测试案例。现在,我们可以按照以下方式编写测试程序。

  1. 初始化Bean:首先,我们需要初始化HelloWorldBean。

  2. 配置ProxyFactoryBean:接着,配置ProxyFactoryBean,利用新的initializeAdvisor方法。

  3. 测试拦截器:最后,测试不同的拦截器是否能够正确工作。 通过这种方式,我们可以确保新的initializeAdvisor方法能够正确地处理拦截器,并且不需要对每种类型的Interceptor进行单独的判断。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	@Autowired
	IAction action;

	@RequestMapping("/testaop")
	public void doTestAop(HttpServletRequest request, HttpServletResponse response) {
		action.doAction();
	}
	@RequestMapping("/testaop2")
	public void doTestAop2(HttpServletRequest request, HttpServletResponse response) {
		action.doSomething();
	}

配置文件就是我们最早希望达成的样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<bean id="realaction" class="com.test.service.Action1" />
<bena id="beforeAdvice" class="com.test.service.MyBeforeAdvice" />
<bean id="advisor" class="com.minis.aop.NameMatchMethodPointcutAdvisor">
    <property type="com.minis.aop.Advice" name="advice" ref="beforeAdvice"/>
    <property type="String" name="mappedName" value="do*"/>
</bean>
<bean id="action" class="com.minis.aop.ProxyFactoryBean">
    <property type="String" name="interceptorName" value="advisor" />
    <property type="java.lang.Object" name="target" ref="realaction"/>
</bean>

使用了新的Advisor, 匹配规则是 "do*",真正执行的类是Action1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.test.service;
public class Action1 implements IAction {
	@Override
	public void doAction() {
		System.out.println("really do action1");
	}
	@Override
	public void doSomething() {
		System.out.println("really do something");
	}
}

在这节课中,我们探讨了如何通过模式匹配来扩展查找方法名的能力,以实现对符合特定规则的方法进行统一处理。这种做法在AOP(面向切面编程)中被称为Pointcut(切点)。

关键概念回顾

  • Join Point:连接点是指程序执行过程中的某个点,比如方法调用时,在这些点上我们可以插入额外的行为。

  • Advice:通知定义了“什么”以及“何时”应用通知逻辑,即在指定的连接点上执行的动作。

  • Advisor:通知者负责将Advice与具体的Pointcut关联起来,告诉系统何时何地应用Advice。

  • Interceptor:拦截器用于拦截对象的调用,并允许在调用前后添加额外行为。

  • Pointcut:切点指定了Advice应该被应用到哪些连接点上,可以类比为SQL查询语句中的WHERE子句。

实现思路

为了支持基于方法名模式的匹配,我们创建了一个特殊的Advisor。该Advisor接受一个模式字符串作为配置参数,并提供isMatch()方法来判断给定的方法名是否与模式匹配。模式匹配是通过简单的从前向后的字符串扫描来完成的,它检查字符串的部分段落是否符合预设模式。

案例分析

考虑Action1类内包含两个方法 doActiondoSomething,它们的名字都以do开头。根据上述配置规则,每当业务代码调用这两个方法之一时,就会动态地插入由MyBeforeAdvice所定义的前置逻辑。

扩展性思考

如果希望支持更多样化的匹配规则而不仅仅局限于当前的通配符模式,我们需要对现有的框架进行如下改进:

  • 引入正则表达式 支持更复杂的匹配需求。

  • 增强isMatch()方法 使其能够解析并使用正则表达式或其他高级匹配算法。

  • 灵活配置机制 允许用户通过配置文件或API接口自定义匹配逻辑。 这样的改进能够让我们的AOP框架更加灵活和强大,适应不同场景下的具体需求。 欢迎你提出自己的见解并在留言区分享你的想法。同时,也鼓励你将这节课的内容分享给身边的朋友。让我们期待下一次课程!