这个举例事基于XML的,之前我们都是这么来用的。那么现在用Java代码的方式也实现一遍(不需要Spring容器):
public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new Person()); //声明一个aspectj切点,一张切面 JdkRegexpMethodPointcut cut = new JdkRegexpMethodPointcut(); //cut.setPattern("com.fsx.maintest.Person.run"); //它会拦截Person类下所有run的方法(无法精确到方法签名) //cut.setPattern(".*run.*");//.号匹配除"\r\n"之外的任何单个字符。*号代表零次或多次匹配前面的字符或子表达式 所以它拦截任意包下任意类的run方法 cut.setPatterns(new String[]{".*run.*", ".*say.*"}); //可以配置多个正则表达 式... sayHi方法也会被拦截 // 声明一个通知(此处使用环绕通知 MethodInterceptor ) Advice advice = (MethodInterceptor) invocation -> { System.out.println("============>放行前拦截..."); Object obj = invocation.proceed(); System.out.println("============>放行后拦截..."); return obj; }; //切面=切点+通知 // 它还有个构造函数:DefaultPointcutAdvisor(Advice advice); 用的切面就是Pointcut.TRUE,所以如果你要指定切面,请使用自己指定的构造函数 // Pointcut.TRUE:表示啥都返回true,也就是说这个切面作用于所有的方法上/所有的方法 // addAdvice();方法最终内部都是被包装成一个 `DefaultPointcutAdvisor`,且使用的是Pointcut.TRUE切面,因此需要注意这些区别 相当于new DefaultPointcutAdvisor(Pointcut.TRUE,advice); Advisor advisor = new DefaultPointcutAdvisor(cut, advice); factory.addAdvisor(advisor); Person p = (Person) factory.getProxy(); // 执行方法 p.run(); p.run(10); p.say(); p.sayHi("Jack"); p.say("Tom", 666); } } class Person { public int run() { System.out.println("我在run..."); return 0; } public void run(int i) { System.out.println("我在run...<" + i + ">"); } public void say() { System.out.println("我在say..."); } public void sayHi(String name) { System.out.println("Hi," + name + ",你好"); } public int say(String name, int i) { System.out.println(name + "----" + i); return 0; } } 输出: ============>放行前拦截... 我在run... ============>放行后拦截... ============>放行前拦截... 我在run...<10> ============>放行后拦截... ============>放行前拦截... 我在say... ============>放行后拦截... ============>放行前拦截... Hi,Jack,你好 ============>放行后拦截... ============>放行前拦截... Tom----666 ============>放行后拦截...
最后需要注意的是:RegexpMethodPointcutAdvisor没有提供不匹配的正则表达式注入方法,即没有excludedPatterns注入,如果需要该功能请还是使用JdkRegexpMethodPointcut。
AspectJExpressionPointcut详细使用和分析
AspectJExpressionPointcut如字面,它和Expression有关,而这个切点表达式,最终还是依赖于AspectJ的jar包去解析的~~~~ Spring在使用@Aspect注解时,会大量的用到它
用AspectJExpressionPointcut实现的切点比JdkRegexpMethodPointcut实现切点的好处就是,在设置切点的时候可以用切点语言来更加精确的表示拦截哪个方法。(可以精确到返回参数,参数类型,方法名,当然,也可以模糊匹配)
这里面就必须要先了解一下Pointcut的切点表达式的写法,以及Spring支持哪些呢?推荐博文:
【小家Spring】Spring AOP中@Pointcut切入点表达式最全面使用介绍
纯Java方式Demo:
下面我先用一个纯Java的方式的例子,先体验一把:
public static void main(String[] args) { //String pointcutExpression = "execution( int com.fsx.maintest.Person.run() )"; // 会拦截Person.run()方法 //String pointcutExpression = "args()"; // 所有没有入参的方法会被拦截。 比如:run()会拦截,但是run(int i)不会被拦截 // ... AspectJExpressionPointcut支持的表达式 一共有11种(也就是Spring全部支持的切点表达式类型) String pointcutExpression = "@annotation(org.springframework.test.context.transaction.AfterTransaction)"; // 拦截上方法标有@AfterTransaction此注解的任意方法们 // ============================================================= ProxyFactory factory = new ProxyFactory(new Person()); //声明一个aspectj切点,一张切面 AspectJExpressionPointcut cut = new AspectJExpressionPointcut(); cut.setExpression(pointcutExpression); // 设置切点表达式 // 声明一个通知(此处使用环绕通知 MethodInterceptor ) Advice advice = (MethodInterceptor) invocation -> { System.out.println("============>放行前拦截..."); Object obj = invocation.proceed(); System.out.println("============>放行后拦截..."); return obj; }; //切面=切点+通知 // 它还有个构造函数:DefaultPointcutAdvisor(Advice advice); 用的切面就是Pointcut.TRUE,所以如果你要指定切面,请使用自己指定的构造函数 // Pointcut.TRUE:表示啥都返回true,也就是说这个切面作用于所有的方法上/所有的方法 // addAdvice();方法最终内部都是被包装成一个 `DefaultPointcutAdvisor`,且使用的是Pointcut.TRUE切面,因此需要注意这些区别 相当于new DefaultPointcutAdvisor(Pointcut.TRUE,advice); Advisor advisor = new DefaultPointcutAdvisor(cut, advice); factory.addAdvisor(advisor); Person p = (Person) factory.getProxy(); // 执行方法 p.run(); p.run(10); p.say(); p.sayHi("Jack"); p.say("Tom", 666); } } class Person { @AfterTransaction public int run() { System.out.println("我在run..."); return 0; } public void run(int i) { System.out.println("我在run...<" + i + ">"); } public void say() { System.out.println("我在say..."); } public void sayHi(String name) { System.out.println("Hi," + name + ",你好"); } public int say(String name, int i) { System.out.println(name + "----" + i); return 0; } }
如上面的图,其实Spring也我们提供了AspectJExpressionPointcutAdvisor
来专门处理基于AspectJ的通知+切点的
XML方式
public class Person { @AfterTransaction public int run() { System.out.println("我在run..."); return 0; } public void run(int i) { System.out.println("我在run...<" + i + ">"); } public void say() { System.out.println("我在say..."); } public void sayHi(String name) { System.out.println("Hi," + name + ",你好"); } public int say(String name, int i) { System.out.println(name + "----" + i); return 0; } } // 相当于准备了一个Advice public class MyMethodInteceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("============>放行前拦截..."); Object obj = invocation.proceed(); System.out.println("============>放行后拦截..."); return obj; } }
书写Spring的xml配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 作为示例,这是需要被切入的目标类 --> <bean class="com.fsx.bean.Person"/> <!-- 切面=切点+通知 (采用面向切点语言进行配置切面) 此处为了便捷 直接使用 AspectJExpressionPointcutAdvisor --> <bean id="advisor" class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor"> <property name="expression" value="@annotation(org.springframework.test.context.transaction.AfterTransaction)"></property> <!-- 一个Advisor里面对应一个advice~~~ --> <property name="advice"> <bean class="com.fsx.aop.MyMethodInteceptor"/> </property> </bean> </beans>
把该xml配置文件导入Config配置类,让它生效
... @Configuration @ImportResource(locations = "classpath:spring.xml") public class RootConfig { ... }
启动Spring容器(采用JUnit测试):
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private Person p; @Test public void test1() { System.out.println(p.getClass()); //class com.fsx.bean.Person$$EnhancerBySpringCGLIB$$cba1d735 p.run(); p.run(10); p.say(); p.sayHi("Jack"); p.say("Tom", 666); } } 输出: class com.fsx.bean.Person$$EnhancerBySpringCGLIB$$cba1d735 // 说明它是CGLIB的代理 ============>放行前拦截... 我在run... ============>放行后拦截... 我在run...<10> 我在say... Hi,Jack,你好 Tom----666
备注:此xml的配置方式其实是最原始的Spring AOP的使用。并没有使用到
<aop:config>、<aop:pointcut>
这种标签注解,关于使用它们的xml配置方式,此处不做过多介绍了~~~因为也比较简单~
AspectJExpressionPointcut
源码分析
// 很容易发现,自己即是ClassFilter,也是MethodMatcher // 它是子接口:ExpressionPointcut的实现类 public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware { ... private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>(); // 从此处可以看出,Spring支持的AspectJ的切点语言表达式一共有10中(加上后面的自己的Bean方式一共11种) // AspectJ框架本省支持的非常非常多,详解枚举类:org.aspectj.weaver.tools.PointcutPrimitive static { SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ANNOTATION); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS); SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET); } ... // 它持有BeanFactory 的引用,但是是可以为null的,也就是说它脱离容器也能够正常work @Nullable private BeanFactory beanFactory; ... // PointcutExpression是org.aspectj.weaver.tools.PointcutExpression是AspectJ的类 // 它最终通过一系列操作,由org.aspectj.weaver.tools.PointcutParser#parsePointcutExpression从字符串表达式解析出来 @Nullable private transient PointcutExpression pointcutExpression; ... // 由此可见,我们不仅仅可议写&& || !这种。也支持 and or not这种哦~~~ private String replaceBooleanOperators(String pcExpr) { String result = StringUtils.replace(pcExpr, " and ", " && "); result = StringUtils.replace(result, " or ", " || "); result = StringUtils.replace(result, " not ", " ! "); return result; } ... // 这是ClassFilter 匹配类。借助的PointcutExpression#couldMatchJoinPointsInType 去匹配 public boolean matches(Class<?> targetClass) { ... } // MethodMatcher 匹配方法,借助的PointcutExpression和ShadowMatch去匹配的 public boolean matches(Method method, @Nullable Class<?> targetClass, boolean hasIntroductions) { ... } @Override public boolean isRuntime() { //mayNeedDynamicTest 相当于由AspectJ框架去判断的(是否有动态内容) return obtainPointcutExpression().mayNeedDynamicTest(); } ... // 初始化一个Pointcut的解析器。我们发现最后一行,新注册了一个BeanPointcutDesignatorHandler 它是准们处理Spring自己支持的bean() 的切点表达式的 private PointcutParser initializePointcutParser(@Nullable ClassLoader classLoader) { PointcutParser parser = PointcutParser .getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution( SUPPORTED_PRIMITIVES, classLoader); parser.registerPointcutDesignatorHandler(new BeanPointcutDesignatorHandler()); return parser; } // 真正的解析,依赖于Spring自己实现的这个内部类(主要是ContextBasedMatcher 这个类,就会使用到BeanFactory了) private class BeanPointcutDesignatorHandler implements PointcutDesignatorHandler { private static final String BEAN_DESIGNATOR_NAME = "bean"; @Override public String getDesignatorName() { return BEAN_DESIGNATOR_NAME; } // ContextBasedMatcher由Spring自己实现,对容器内Bean的匹配 @Override public ContextBasedMatcher parse(String expression) { return new BeanContextMatcher(expression); } } }
NameMatchMethodPointcut
如果创建切入点时候,我们往往只需要方法名字匹配,无需理会方法的签名和返回类型,这种情况下,我们可以使用 NameMatchMethodPointCut
方法名字匹配切入点。(这种功能最弱,但显然效率是最高的)
public class Main { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new Person()); // 声明一个通知(此处使用环绕通知 MethodInterceptor ) Advice advice = (MethodInterceptor) invocation -> { System.out.println("============>放行前拦截..."); Object obj = invocation.proceed(); System.out.println("============>放行后拦截..."); return obj; }; 声明一个aspectj切点,一张切面 //NameMatchMethodPointcut cut = new NameMatchMethodPointcut(); //cut.setMappedName("run"); //会匹配所有的方法名为run的方法 切点+通知 //Advisor advisor = new DefaultPointcutAdvisor(cut, advice); NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(); advisor.setMappedName("run"); advisor.setAdvice(advice); factory.addAdvisor(advisor); Person p = (Person) factory.getProxy(); // 执行方法 p.run(); p.run(10); p.say(); p.sayHi("Jack"); p.say("Tom", 666); } } 输出: ============>放行前拦截... 我在run... ============>放行后拦截... ============>放行前拦截... 我在run...<10> ============>放行后拦截... 我在say... Hi,Jack,你好 Tom----666
其它Pointcut
上面已经介绍了Spring中使用得比较多的Pointcut,接下来简单的讲述一下稍微偏门些的Pointcut。
从顶部的pointcut的继承图中可以看出,有很多实现类。
ControlFlowPointCut:流程切入点
如果有这样的特殊需求:我们对一个方法进行切入通知,但只有这个方法在一个特定方法中被调用的时候执行通知(即存在流程上行的依赖关系),我们可以使用ControlFlowPointCut流程切入点
public class Main { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new Person()); // 声明一个通知(此处使用环绕通知 MethodInterceptor ) Advice advice = (MethodInterceptor) invocation -> { System.out.println("============>放行前拦截..."); Object obj = invocation.proceed(); System.out.println("============>放行后拦截..."); return obj; }; 声明一个aspectj切点,一张切面 // 含义:Main类里面,方法名为funabc执行时,内部调用的任何代理的方法都会被拦截~~~ 它控制的是整个流程 ControlFlowPointcut cut = new ControlFlowPointcut(Main.class, "funabc"); // 切点+通知 Advisor advisor = new DefaultPointcutAdvisor(cut, advice); factory.addAdvisor(advisor); Person p = (Person) factory.getProxy(); // 执行方法 p.run(); p.run(10); p.say(); p.sayHi("Jack"); p.say("Tom", 666); // 此处调用Main类,方法名为funabc的方法。内部代理对象的方法就都会被拦截上了 funabc(p); } private static void funabc(Person person) { person.run(); person.say(); } } 输出: 我在run... 我在run...<10> 我在say... Hi,Jack,你好 Tom----666 ============>放行前拦截... 我在run... ============>放行后拦截... ============>放行前拦截... 我在say... ============>放行后拦截...
使用流程切入点有时候可以解决不少问题,但值得注意的是:
使用流程切入点在jdk1.4中比其他切入点要慢5倍,在1.3上则要慢10倍,追求高性能的要慎重使用