对方法进行性能监控,在方法调用时统计出方法执行时间。

原始做法:在内个方法的开头获取系统时间,然后在方法的结尾获取时间,最后把前后台两次分别获取的系统时间做一个减法,即可获取方法执行所消耗的总时间。

项目中大量的方法,如果对每个方法开头结尾都加上这些代码,工作量会很大。现在不用修改现有代码,在另一个地方做性能监控,AOP(Aspect Oriented Programming,面向方面编程)就是我们寻找的解决方案。

在AOP中,我们需要定义一个Aspect(切面)类来编写需要横切业务逻辑的代码,也就是性能监控代码。此外,我们需要通过一个条件来匹配想要拦截的类,这个条件在AOP中称为Pointcut(切点)。

案例思路,统计出执行每个Controller类的各个方法所消耗的时间。每个Controller类都有Controller注解,也就是说,我们只需要拦截所有带有Controller注解的类就行了,切点很容易就能确定下来,剩下的就是做一个切面了。

代理技术

代理,或称为Proxy,意思就是你不用去做,别人替你去处理。比如说:赚钱方面,我就是我老婆的Proxy;带小孩方面,我老婆就是我的Proxy;家务事方面,没有Proxy。

它在程序中开发起到了非常重要的作用,比如AOP,就是针对代理的一种应用。此外,在设计模式中,还有一个“代理模式”,在公司要上网,要在浏览器中设置一个Http代理。

Hello World例子

//接口
public interface Hello{
void say(String name);
} //实现类
public class HelloImpl implements Hello{
@Override
public void say(String name){
System.out.println("Hello!"+name);
}
}

如果要在println方法前面和后面分别需要处理一些逻辑,怎么做呢?把这些逻辑写死在say方法里面吗?这么做肯定不够优雅,“菜鸟”一般这样干,作为一名资深的程序员,我们坚决不能这么做!

我们要用代理模式,写一个HelloProxy类,让它去调用HelloImpl的say方法,在调用的前后分别进行逻辑处理。

public class HelloProxy implements Hello{
private Hello hello; private HelloProxy(){
hello=new HelloImpl();
} private void say(String name){
before();
hello.say(name);
after();
} private void before(){
System.out.println("Before");
} private void after(){
System.out.println("After");
}
}

用HelloProxy类实现了Hello接口(和HelloImpl实现相同的接口),并且在构造方法中new出一个HelloImpl类的实例。这样一来,我们就可以在HelloProxy的say方法里面去调用HelloImpl的say方法了。更重要的是,我们还可以在调用的前后分别加上before和after两个方法,在这两个方法里去实现那些前后逻辑。

main方法测试

public static void main(String[] args){
Hello helloProxy = new HelloProxy();
helloProxy.say("Jack");
} //打印结果
Before
Hello! Jack
After

JDK动态代理

于是疯狂使用代理模式,项目中到处都是XXXProxy的身影,直到有一天,架构师看到了我的代码,他惊呆了,对我说“你怎么这么喜欢静态代理呢?你就不会用动态代理吗?全部重构!”

研究了一下,原来一直用的是静态代理(上面的例子),到处都是XXXProxy类。一定要将这些垃圾Proxy都重构为“动态代理”。

/**
* 动态代理
*/
public class DynamicProxy implements InvocationHandler {
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
/*
* 用时代理
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
before();
Object result method.invoke(target, args);
after();
return result;
    }

}

    //运行
public static void main(String[] args) {
Hello hello = new HelloImpl();//用时代理
DynamicProxy dynamicProxy = new DynamicProxy(hello);
Hello helloProxy = (Hello)Proxy.newProxyInstance(
      hello.getClass().getClassLoader(),
      hello.getClass().getInterfaces(),
      dynamicProxy
     );
//运行run方法
helloProxy.say("Jack");
}

在这个例子中,DynamicProxy定义了一个Object类型的Object变量,它就是被代理的目标对象,通过构造函数来初始化(“注入”,构造方法初始化叫“正着射”,所以反射初始化叫“反着射”,简称“反射”)。

通过DynamicProxy类去包装Car实例,然后再调用JDK给我们的提供的Proxy类的工厂方法newProxyInstance去动态的创建一个Hello接口的代理类,最后调用这个代理类的run方法。

Proxy.newProxyInstance方法的参数

参数1:ClassLoader

参数2:该实现类的所有接口

参数3:动态代理对象

调用完了用强制类型转换下

这一块想办法封装一下,避免再次出现到处都是Proxy.newProxyInstance方法的情况。于是将这个DynamicProxy重构一下:

public class DynamicProxy implements InvocationHandler{    
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}

   @SupressWarnings("unchecked")
public <T> T getProxyInstance() {
return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
} @Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
} //请求前的操作
public void before(){
//预处理
System.out.println(target.getClass()+"被动态代理了,在它执行之前要执行动态代理加入的预处理方法");
}
//请求后的操作
public void after(){
//善后处理
System.out.println(target.getClass()+"被动态代理了,在它执行之后要执行动态代理加入的善后方法");
}
} public class DynamicProxyDemo {
public static void main(String[] args) {
    DynamicProxy dynamicProxy = new DynamicProxy(new HelloImpl());
    Hello helloProxy = dynamicProxy.getProxy();
    helloProxy.say("Jack");
}
}

在DynamicProxy里添加了一个getProxy方法,无需传入任何参数,将刚才所说的那块代码放在这个方法中,并且该方法返回一个泛型类型,就不会强制转换类型了。方法头上@SupressWarnings(“unchecked”)注解表示忽略编译时的警告(因为Proxy.newProxyInstance方法返回的是一个Object,这里强制转换为T了,这是向下转型,IDE中就会有警告,编译时也会出现提示)。

调用时就简单了,用2行代理去掉了前面的7行代码(省了5行)。

CGlib动态代理

用了DynamicProxy以后,好处是接口变了,这个动态代理类不用动。而静态代理就不一样了,接口变了,实现类还要动,代理类也要动。但是动态代理并不是万能的,它也有搞不定的时候,比如要代理一个没有任何接口的类,它就没有用武之地了。

CGlib是一个能代理没有接口的类,虽然看起来不起眼,但Spring、Hibernate这样高端的开源框架都用到了它,它是一个在运行期间动态生成字节码的工具,也就是动态生成代理类了。

public class CGLibProxy implements MethodInterceptor{

    public <T> T getProxy(Class<T> cls){
return (T) Enhancer.create(cls,this);
} public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable{
before();
Object result = proxy.invokeSuper(obj,args);
after();
return result;
} ......
}

需要实现CGLib给我们提供的MethodInterceptor实现类,并填充intercept方法。方法中最后一个MethodProxy类型的参数proxy值得注意。CGLib给我们提供的是方法级别的代理,也可以理解为堆方法拦截(也就是“方法拦截器”)。这个功能对于我们程序员来说,如同雪中送炭。我们直接调用proxy的invokeSuper方法,将被代理的对象obj以及方法参数args传入其中即可。

与DynamicProxy类似,在CGLibProxy中也添加了一个泛型的getProxy方法,便于我们可以快速地获取自动生成的代理对象。

public static void main(){
CGLibProxy cgLibProxy = new CGLibProxy();
Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class);
helloProxy.say(Jack);
}

仍然通过2行代码就可以返回代理对象,与JDK动态代理不同的是,这里不需要任何的接口信息,对谁都可以生成动态代理对象

用2行代码返回代理对象还是有些多余的,不想总是去new这个CGLibProxy对象,最好new一次,以后随时拿随时用,于是想到了“单例模式”:

public class CGLibProxy implements MethodInterceptor{
private static CGLibProxy instance = new CGLibProxy(); private CGLibProxy(){ }
private static CGLibProxy getInstance(){
return instance;
}
...
getProxy...
intercept...
}

加上以上几行代码问题就解决了。需要说明的是,这里有一个private的构造方法,就是为了限制外界不能再去new它了,换句话说,这个类被阉割了。

public static void main(String[] args){
Hello helloProxy = CGLibProxy.getInstance().getProxy(HelloImpl.class);
helloProxy.say("Jack");
}

这里只需要一行代码就可以获取代理对象了.

AOP技术

什么是AOP

AOP(Aspect-Oriented Programming),名字与OOP仅仅差一个字母,其实它是对OOP编程方式的一种补充,并非是取而代之。翻译过来就是“面向切面编程”或“面向方面编程”。最重要的工作就是写这个“切面”,那么什么事“切面”呢?

切面是AOP中的一个术语,表示从业务逻辑中分离出来的横切逻辑,比如性能监控、日志记录、权限控制等,这些功能都可以从业务逻辑代码中抽离出去。也就是说,通过AOP可以解决代码耦合问题,让职责更加单一。

需要澄清的是,其实很早以前就出现了AOP这个概念。最知名强大的Java开源项目就是AspectJ了。它的前身是AspectWerkz(AOP真正的的老祖宗)。Rod Johnson写了一个Spring框架,称为Spring之父。他在Spring的IOC框架基础上又实现了一套AOP框架,后来掉进了深渊,在无法自拔的时候被迫使用了AspectJ。所以我们现在用的最多的就是Spring+AspectJ这种AOP框架了。

写死代码

public interface Greeting {
void sayHello(String name);
} public class GreetingImpl implements Greeting {
@Override
public void sayHello(String name) {
before();
System.out.println("Hello! "+name);
after();
} private void before(){
System.out.println("Before");
} private void after(){
System.out.println("After");
}
}

before与after方法写死在sayHello方法体中了,这样的代码非常不好。如果我们要统计每一个方法的执行时间,以对性能进行评估,那是不是每个方法的一头一尾都做点手脚呢?

再比如我们要写一个JDBC程序,那是不是也要在方法的开头去连接数据库,方法的末尾去关闭数据库连接呢?

这样写代码只会把程序员累死,把架构师气死!

一定要想办法对上面的代码进行重构,首先给出三个解决方案:

静态代理

JDK动态代理

CGLib动态代理

静态代理

最简单的解决方案就是使用静态代理模式了,我们单独为GreetingImpl这个类写一个代理类:

public class GreetingProxy implements Greeting {
private GreetingImpl greetingImpl; public GreetingProxy(GreetingImpl greetingImpl) {
this.greetingImpl = greetingImpl;
} @Override
public void sayHello(String name) {
before();
System.out.println("Hello! "+name);
after();
} private void before(){
System.out.println("Before");
}
private void after(){
System.out.println("After");
}
}

就用这个GreetingProxy去代理GreetingImpl,看看客户端如何来调用:

public class Client {
public static void main(String[] args) {
Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
greetingProxy.sayHello("Jack");
}
}

这个写的没错,但是有个问题。XxxProxy这样的类会越来越多(这里构造函数参数为GreetingImpl,所以换一个子类就要再次写一个代理类,如果构造函数参数改为接口Greet,这样再使用Greet的子类时可以使用这个类,当换一个接口时又要去写一个代理类),如何才能将这些代理类尽可能减少呢?最好只有一个代理类。

这时我们需要使用JDK的动态代理了。

JDK动态代理

public class JDKDynamicProxy implements InvocationHandler {
private Object target; public JDKDynamicProxy(Object target) {
this.target = target;
} @SuppressWarnings("unchecked")
public <T> T getProxy(){
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target,args);
after();
return result;
} private void before(){
System.out.println("Before");
} private void after(){
System.out.println("After");
}
}

这样所有的代理类都合并到动态代理类中了,但这样做仍然存在一个问题:JDK给我们提供的动态代理只能代理接口,而不能代理没有接口的类

public class Client {
public static void main(String[] args) {
//静态代理
/*Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
greetingProxy.sayHello("Jack");*/
//JDK动态代理
Greeting greeting = new JDKDynamicProxy(new GreetingImpl())
.getProxy();
greeting.sayHello("Jack");
}
}

CGLib动态代理

我们使用开源的CGLib类库可以代理没有接口的类,这样就弥补了JDK的不足。CGLib动态代理类是这样的:

public class CGLibDynamicProxy implements MethodInterceptor{
//单例模式
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
//私有化构造函数,防止new
private CGLibDynamicProxy(){}
//提供给外界获取单一实例的方法
public static CGLibDynamicProxy getInstance(){
return instance;
} @SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls){
return (T) Enhancer.create(cls,this);
} @Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(target,args);
after();
return result;
} private void before(){
System.out.println("Before");
}
private void after(){
System.out.println("After");
}
}

注意这里的坐标

        <!--不能超过3.0版本,这里用2.2-->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
<!--CGLib依赖此包-->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.2</version>
</dependency>

到此为止,能做的都做了,问题似乎全部都解决了。但事情总不会那么完美,而我们一定要追求完美。

Spring AOP

Rod Johnson搞出了一个AOP框架,Spring AOP:前置增强、后置增强、环绕增强(编程式)

上面例子中提到的before方法,在Spring AOP里就叫Before Advice(前置增强)。有些人将Advice直译为“通知”,这里是不太合适的,因为它没有“通知”的含义,而是对原有代码功能的一种“增强”。再者,CGLib中也有一个Enhancer类,它就是一个增强类。

此外,像after这样的方法就叫After Advice(后置增强),因为它放在后面来增强代码的功能。

如果能把before与after结合在一起,那就叫Around Advice(环绕增强),就像汉堡一样。

前置增强类代码(这个类实现了org.spring.framework.aop.MethodBeforeAdvice):

import org.springframework.aop.MethodBeforeAdvice;

public class GreetingBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("Before");
}
}

后置增强类:

import org.springframework.aop.AfterReturningAdvice;

public class GreetingAfterAdvice implements AfterReturningAdvice {

    @Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("After");
}
}

类似的这里实现了org.springframework.aop.afterReturningAdvice接口。

调用

public class Client {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(); //创建代理工厂
proxyFactory.setTarget(new GreetingImpl()); //摄入目标类对象
proxyFactory.addAdvice(new GreetingBeforeAdvice()); //添加前置增强
proxyFactory.addAdvice(new GreetingAfterAdvice()); //添加后置增强
Greeting greeting = (Greeting) proxyFactory.getProxy();
greeting.sayHello("Jack"); }
}

当然我们完全可以用一个增强类,让它同时实现MethodBeforeAdvice和AfterReturningAdvice这两个接口,代码:

public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice,AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("before");
} @Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("after");
}
}

这样我们只需要使用一行代码,就可以同时添加前置与后置增强

proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());

刚才有提到过“环绕增强”,其实它可以把“前置增强”与“后置增强”的功能合并起来,无须让我们同时实现两个接口。

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method; public class GreetingAroundAdvice implements MethodInterceptor { @Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
before();
Object result = methodInvocation.proceed();
after();
return result;
} private void before(){
System.out.println("Before");
} private void after(){
System.out.println("After");
}
}

环绕增强需要实现org.aopalliance.intercept.MethodInterceptor接口。注意,这个接口不是Spring提供的,它是AOP联盟(一个很高大上的技术联盟)写的,Spring只是借用了它,在客户端汇总同样也需要将该增强类的对象添加到代理工厂中。

public class Client {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(); //创建代理工厂
proxyFactory.setTarget(new GreetingImpl()); //摄入目标类对象
//proxyFactory.addAdvice(new GreetingBeforeAdvice()); //添加前置增强
//proxyFactory.addAdvice(new GreetingAfterAdvice()); //添加后置增强
//proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice()); //实现两个接口
proxyFactory.addAdvice(new GreetingAroundAdvice()); //实现一个Around环绕式接口
Greeting greeting = (Greeting) proxyFactory.getProxy();
greeting.sayHello("Jack");
}
}

以上就是SpringAOP的基本用法,单这只是“编程式”而已。Spring AOP如果只是这样,那就太弱了,它曾经也一度宣传用Spring配置文件的方式来定义Bean对象,把代码中的new操作全部解脱出来。

SpringAOP:前置增强、后置增强、环绕增强(声明式)

Spring配置文件篇日志

    <!--扫描指定包(将带有Component注解的类自动定义为SpringBean)-->
<context:component-scan base-package="com.smart4j.framework"/> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的接口-->
<property name="target" ref="greetingImpl"/> <!--实现接口类-->
<property name="interceptorNames"> <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
<list>
<value>greetingAroundAdvice</value>
</list>
</property>
</bean>

使用ProxyFactoryFactoryBean就可以取代前面的ProxyFactory,其实他们是一回事。interceptorNames改名为adviceNames或许更容易让人理解。就是网这个属性里添加增强类。

此外,如果只有一个增强类,可以使用下面这个方法来简化

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的接口 name也可以为proxyInterfaces-->
<property name="target" ref="greetingImpl"/> <!--实现接口类 name也可以为targetName-->
<property name="interceptorNames" value="greetingAroundAdvice"> <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
</property>
</bean>

需要注意的是,这里使用了Spring2.5+的"Bean扫描"特性,这样我们就无需再Spring配置问加你了不断的定义<bean id="xxx" class="xxx"/>了,从而解脱了我们的双手。

去掉原本的

    <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"></bean>

    <bean id="greetingAroundAdvice" class="com.smart4j.framework.aop.GreetingAroundAdvice"></bean>

改为使用@Compoent

@Component
public class GreetingImpl implements Greeting{
。。。
} @Component
public class GreetingAroundAdvice implements MethodInterceptor {
。。。
}

代码量确实少了,我们将配置性的代码放入配置文件,这样也有助于后期维护。更重要的是,代码值关注于业务逻辑,而将配置放入文件中,这是一条最佳实践!

除了上面提到的那三个增强意外,其实还有两个增强也需要了解一下,关键的时候要能想到它们才行。

Spring AOP:抛出增强

程序报错,抛出异常了,一般的做法是打印控制台到日志文件中,这样很多地方都得去处理,有没有一个一劳永逸的方法呢?那就是Throws Advice(抛出增强)

@Component
public class GreetingThrowAdvice implements ThrowsAdvice { public void afterThrowing(Method method, Object[] args, Object target,Exception e){
System.out.println("-------------Throw Exception-----------------");
System.out.println("Target class: "+target.getClass().getName());
System.out.println("Method Name: "+method.getName());
System.out.println("Exception Message: "+e.getMessage());
System.out.println("---------------------------------------------");
}
}

配置spring.xml

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的接口-->
<property name="targetName" value="greetingImpl"/> <!--实现接口类-->
<property name="interceptorNames"> <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
<list>
<!-- <value>greetingAroundAdvice</value>-->
<value>greetingThrowAdvice</value>
</list>
</property>
</bean>

结果

抛出增强需要实现org.springframework.aop.ThrowsAdvice接口,在接口方法中可获取方法、参数、目标对象、异常对象等信息。我们可以把这些信息统一写入到日志中,当然也可以持久化到数据库中。

Spring AOP:引入增强

以上提到的都是对方法的增强,那能否对类进行增强呢?用AOP的行话来讲,对方法的增强叫Weaving(织入),而对类的增强叫Introduction(引入),Introduction Advice(引入增强)就是对类的功能增强,它也是Spring AOP提供的最后一种增强。

定义接口

public interface Apology {
void saySorry(String name);
}

但是我们不想在代码中让GreetingImpl直接去实现这个接口,而想在程序运行的时候动态地实现它。因为加入实现了这个接口,那么久一定要改写GreetingImpl这个类,关键是我们不想改它,或许在真实场景中,这个类有一万行代码。于是,我们需要借助Spring的引入增强。

@Component
public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology{ @Override
public Object invoke(MethodInvocation mi) throws Throwable {
return super.invoke(mi);
} @Override
public void saySorry(String name) {
System.out.println("Sorry "+name);
}
}

以上一个引入增强类,扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor类,同时也实现了新定义的Apology接口。在类中首先覆盖了父类的invoke()方法,然后实现了Apology接口的方法。我们相拥这个增强类去丰富GreetingImpl类的功能,那么这个GreetingImpl类无须直接实现Apology接口,就可以直接在程序运行的时候调用Apology接口的方法了。

配置Spring.xml

    <!--引入增强-->
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.smart4j.framework.aop.Apology"/> <!--需要动态实现的接口-->
<property name="targetName" value="greetingImpl"/> <!--目标类-->
<property name="interceptorNames" value="greetingIntroAdvice"/> <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
<property name="proxyTargetClass" value="true"/> <!--代理目标类,(默认为false,代理接口)-->
</bean>

需要注意proxyTargetClass属性,它表明是否代理目标类,默认为false,也就是代理接口,此时Spring就用JDK动态代理;如果为TRUE,那么Spring就用CGLib动态代理。

调用

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
Greeting greeting = (Greeting) context.getBean("greetingProxy"); //从Context中根据id获取Bean对象(其实也就是一个代理)
greeting.sayHello("jack"); //调用代理方法 Apology apology = (Apology) greeting; //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能)
apology.saySorry("jack");

sarySorry方法原来是可以被greetingImpl对象来直接调用的,只需将其强制转换为该接口即可。

SpringAOP:切面

之前谈到的AOP框架其实可以将它理解为一个拦截器框架,但这个拦截器似乎非常武断。比如说,如果它拦截了一个类,那么它就拦截这个类中所有的方法。类似的,当我们在使用动态代理的时候,其实也遇到了这个问题。需要在代码中对所拦截的方法名加以判断,才能过滤出我们需要拦截的方法,这种做法确实不太优雅。在大量的真实项目中,似乎我们只需要拦截特定的方法就行了,没必要拦截所有的方法。于是,Spring借助很重要的工具---Advisor(切面),来解决这个问题。它也是AOP中的核心,是我们关注的重点。

也就是说,我们可以通过切面,将增强类与拦截匹配条件组合在一起,然后将这个切面配置到ProxyFactory中,从而生成代理。

这里提到这个“拦截匹配条件”在AOP中就叫作Pointcut(切点),其实说白了就是一个基于表达式的拦截条件。Advisor(切面)封装了Advice(增强)与Pointcut(切点)。

@Component
public class GreetingImpl implements Greeting {
@Override
public void sayHello(String name) {
System.out.println("Hello! "+name);
} /*切面新增方法*/
public void goodMorning(String name){
System.out.println("Good Morning!"+name);
} public void goodNight(String name){
System.out.println("Good Night!"+name);
}
}

在Spring AOP中,最好用的是基于正则表达式的切面类。

配置

    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="greetingAroundAdvice"/> <!--增强-->
<property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/> <!--切点(正则表达式)-->
</bean>
<!--配置一个代理类-->
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetName" value="greetingImpl"/> <!--目标类-->
<property name="interceptorNames" value="greetingAdvisor"/> <!--切面(替换之前的拦截器名称)-->
<property name="proxyTargetClass" value="true"/> <!--代理目标类,(默认为false,代理接口)-->
</bean>

调用

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
GreetingImpl greeting = (GreetingImpl) context.getBean("greetingProxy"); //从Context中根据id获取Bean对象(其实也就是一个代理)
greeting.sayHello("jack"); //调用未被代理方法
greeting.goodMorning("Jhon");
greeting.goodNight("Sawer");

结果

注意以上代理对象中的配置的interceptorNames,它不再是一个增强,而是一个切面,因为已经将增强封装到该切面中了。此外,切面还定义了一个切点(正则表达式),其目的是为了只对满足切点匹配条件的方法进行拦截。

这里的切点表达式是基于正则表达式的。其中.*代表匹配所有字符,翻译过来就是匹配GreetingImpl类中以good开头的方法。

除了RegexpMethodPointcutAdvisor以外,在Spring AOP中还提供了几个切面类,比如:

  • DefaultPointcutAdvisor - 默认切面(可扩展它来自定义切面)
  • NameMatchMethodPointcutAdvisor - 根据方法名称进行匹配的切面
  • StaticMethodMatcherPointcutAdvisor - 用于匹配静态方法的切面

总的来说,让用户去配置一个或少数几个代理,似乎还可以接受,但随着项目的扩大,代理配置就会越来越多,配置的重复劳动就多了,麻烦不说,还很容易出错。。能否让Spring框架为我们自动生成代理呢?

Spring AOP:自动代理(扫描Bean名称)

Spring AOP提供了一个可以根据Bean名称来自动生成代理的工具,它就是BeanNameAutoProxyCreator。配置如下:

    <!--自动代理-->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="*Impl"/> <!--为后缀是Impl的Bean生成代理-->
<property name="interceptorNames" value="greetingAroundAdvice"/> <!--增强(这里没用切面)-->
<property name="optimize" value="true"/> <!--是否对代理生成策略进行优化-->
</bean>

调用

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl"); //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名)
greeting.sayHello("jack");

以上使用BeanNameAutoProxyCreator只为后缀为"Impl"的Bean生成代理。需要注意的是,这个地方我们不能定义代理接口,也及时interfaces属性,因为我们根本就不知道这些Bean到底实现了多少接口。此时不能代理接口,而只能代理类。所以这里提供了一个新的配置项,它就是optimize。若为true时,则可对代理生成策略进行优化(默认是false)。也就是说,如果该类有接口,就代理接口(JDK动态代理);如果没有接口,就代理类(使用CGLib动态代理)。并非像之前使用的proxyTargetClass属性那样,强制代理类,而不考虑代理接口的方式。

既然CGLib可以代理任何类,那为什么还要用JDK的动态代理呢?

根据实际项目经验得知,CGLib创建代理的速度比较慢,但创建代理后运行的速度却非常快,而JDK动态代理正好相反。如果在运行的时候不断地用CGLib去创建代理,系统的性能会大打折扣,所以建议一般在系统初始化的时候用CGLib去创建代理,并放入Spring的ApplicationContext中以备后用。

这个例子只能匹配目标类,而不能进一步匹配其中指定的方法,要匹配方法,就要考虑使用切面与切点了。Spring AOP基于切面也提供了一个自动代理生成器:DefaultAdvisorAutoProxyCreator。

Spring AOP:自动代理(扫描切面配置)

为了匹配目标类中的指定方法,我们让然需要在Spring中配置切面与切点:

    <!--自动代理 - 扫描切面配置-->
<bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/>
<property name="advice" ref="greetingAroundAdvice"/>
</bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
<property name="optimize" value="true" />
</bean>

这里无须再配置代理,因为代理将由DefaultAdvisorAutoProxyCreator自动生成。也就是说,这个类可以扫描所有的切面类,并为其自动生成代理。

看来不论怎么简化,Rod始终解决不了切面的配置这件繁重的手工劳动。在Spring配置文件中,仍然会存在大量的切面配置。然而在很多情况下,Spring AOP所提供的切面类真的不太够用了,比如像拦截指定注解的方法,我们就必须扩展DefaultPointcutAdvisor类,自定义一个切面类,然后在Spring配置文件中进行切面配置。Rod的解决方案似乎已经掉进了切面类的深渊,最重要的是切面,最麻烦的也是切面。所以要把切面配置给简化掉。

Spring+AspectJ

神一样的rod总算认识到了这一点,接受了网友们的建议,集成了AspectJ,同时也保留了以上提到的切面与代理配置方式(为了兼容老项目,更为了维护自己的面子)。将Spring与AspectJ集成与直接使用AspectJ是不同的,我们不需要定义AspectJ类(它扩展了Java语法的一种新的语言,还需要特定的编译器),只需要使用AspectJ切点表达式即可(它是比正则表达式更加友好的表现形式)。

1.Spring+AspectJ(基于注解:通过AspectJ execution表达式拦截方法)

定义一个Aspect切面类

@Aspect    /*切面*/
@Component
public class GreetingAspect { @Around("execution(* com.smart4j.framework.GreetingImpl.*(..))") /*切点*/
public Object around(ProceedingJoinPoint pjp) throws Throwable { /*增强*/
before();
Object result = pjp.proceed();
after();
return result;
} private void before(){
System.out.println("Before");
}
private void after(){
System.out.println("After");
}
}

注意:类上面标注的Aspect注解表明该类是一个Aspect(其实就是Advisor)。该类无须实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),在方法上标注Around注解,在注解中使用AspectJ切点表达式。方法的参数中包括一个ProceedingJoinPoint对象,它在AOP中称为Joinpoint(连接点),可以通过该对象获取方法的任何信息,例如,方法名、参数等。

解析下切点表达式execution(* com.smart4j.framework.GreetingImpl.*(..))

  • execution表示拦截方法,括号中可定义需要匹配的规则。
  • 第一个"*"表示方法的返回值是任意的;
  • 第二个"*"表示匹配该类中的所有方法;
  • (..)表示方法的参数是任意的。

是不是比正则表达式可读性更强呢?如果想匹配指定的方法,只需将第二个“*”改为指定的方法名即可。

配置如下

    <!--扫描指定包(将带有Component注解的类自动定义为SpringBean)-->
<context:component-scan base-package="com.smart4j.framework"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>

两行配置就行了,不需要配置大量的代理,更不需要配置大量的切面!proxy-target-class属性,它的值默认是false,默认只能代理接口(使用JDK动态代理),当为true时,才能代理目标类(使用CGLib动态代理)。

Spring与AspectJ结合功能远远不止这些,我们还可以拦截指定注解的方法。

2.Spring+AspectJ(基于注解:通过AspectJ @annotation表达式拦截方法)

注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tag {
}

以上定义一个Tag注解,此注解可标注在方法上,在运行时生效。

@Aspect    /*切面*/
@Component
public class GreetingAspect { @Around("@annotation(com.smart4j.framework.aspectj.Tag)") /*切点 - 有Tag标记的Method*/
public Object around(ProceedingJoinPoint pjp) throws Throwable { /*增强*/
before();
Object result = pjp.proceed();
after();
return result;
} private void before(){
System.out.println("Before");
}
private void after(){
System.out.println("After");
}
}

直接将Tag注解定义在想要拦截的方法上

@Component
public class GreetingImpl implements Greeting {
@Tag /*AspectJ 注解*/
@Override
public void sayHello(String name) {
System.out.println("Hello! "+name);
}
}

在以上实例中只有一个方法,如果有多个方法,我们只想拦截其中的某一些时,这种解决方案会更加有价值

除了Around注解外,其实还有几个相关的注解,稍微归纳一下:

  • Before - 前置增强
  • After - 后置增强
  • Around - 环绕增强
  • AfterThrowing - 抛出增强
  • DeclareParents - 引入增强

此外还有一个AfterReturning(返回后增强),也可理解为Finally增强,相当于finally语句,它是在方法结束后执行的,也就是说,它比After晚一些。

3.Spring+AspectJ(引入增强)

为了实现基于AspectJ的引入增强,我们同样需要定义一个Aspect类:

@Aspect    /*切面*/
@Component
public class GreetingAspect {
/*引入增强*/
@DeclareParents(value = "com.smart4j.framework.GreetingImpl",defaultImpl = ApologyImpl.class)
private Apology apology;
}

在Aspect类中定义一个需要引入增强的接口,它也就是运行时需要动态实现的接口。在这个接口上标注了DeclareParents注解,该注解有两个属性:

  • Value - 目标类;
  • defaultImpl - 引入接口的默认实现类。

我们只需要对引入的接口提供一个默认实现类即可完成增强:

public class ApologyImpl implements Apology {
@Override
public void saySorry(String name) {
System.out.println("Sorry! " + name);
}
}

运行

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl"); //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名)
greeting.sayHello("jack"); Apology apology = (Apology) greeting; //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能)
apology.saySorry("jack");

从SpringApplicationContext中获取greetingImpl对象(其实是个代理对象),可转型为自己静态实现的接口Greeting,也可转型为自己动态实现的接口Apology,切换起来非常方便。

使用AspectJ的引入增强比原来的SpringAOP的引入增强更加方便了,而且还可面向接口编程(以前只能面向实现类)。

这一切已经非常强大并且非常灵活了,但仍然还是由用户不能尝试这些特性,因为他们还在使用JDK1.4(根本就没有注解这个东西),怎么办呢?SpringAOP为那些遗留系统也考虑到了。

3.Spring+AspectJ(基于配置)

除了使用Aspect注解来定义切面之外,SpringAOP也提供了基于配置的方式来定义切面类:

    <!--AspectJ - 基于配置-->
<bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"/>
<bean id="greetingAspect" class="com.smart4j.framework.aspectj.GreetingAspect"/> <aop:config>
<aop:aspect ref="greetingAspect">
<aop:around method="around" pointcut="execution(* com.smart4j.framework.GreetingImpl.*(..))"/>
</aop:aspect>
</aop:config>

使用<aop:config>元素来进行AOP配置,在其子元素中配置切面,包括增强类型、目标方法、切点等信息。

无论用户是不能使用注解,还是不愿意使用注解,SpringAOP都能提供全方位的服务。

AOP思维导图

各类增强类型所对应的解决方案

SpringAOP整体架构UML类图

源码

架构探险笔记4-使框架具备AOP特性(上)的更多相关文章

  1. 架构探险笔记5-使框架具备AOP特性(下)

    开发AOP框架 借鉴SpringAOP的风格,写一个基于切面注解的AOP框架.在进行下面的步骤之前,确保已经掌了动态代理技术. 定义切面注解 /** * 切面注解 */ @Target(Element ...

  2. 架构探险笔记3-搭建轻量级Java web框架

    MVC(Model-View-Controller,模型-视图-控制器)是一种常见的设计模式,可以使用这个模式将应用程序进行解耦. 上一章我们使用Servlet来充当MVC模式中的Controller ...

  3. 架构探险笔记12-安全控制框架Shiro

    什么是Shiro Shiro是Apache组织下的一款轻量级Java安全框架.Spring Security相对来说比较臃肿. 官网 Shiro提供的服务 1.Authentication(认证) 2 ...

  4. 架构探险笔记11-与Servlet API解耦

    Servlet API解耦 为什么需要与Servlet API解耦 目前在Controller中是无法调用Servlet API的,因为无法获取Request与Response这类对象,我们必须在Di ...

  5. 架构探险笔记6-ThreadLocal简介

    什么是ThreadLocal? ThreadLocal直译为“线程本地”或“本地线程”,如果真的这么认为,那就错了!其实它就是一个容器,用于存放线程的局部变量,应该叫ThreadLocalVariab ...

  6. Spring框架中AOP特性

    1.AOP介绍 即:面向切面编程,在不改变原有方法的定义与使用.也不改变原程序流程的情况下,可以改变原有方法的功能{增加一些附加的功能,在指定的地方添加其他函数方法:} 2.其他的方法:[需要的四个接 ...

  7. 读《架构探险——从零开始写Java Web框架》

    内容提要 <架构探险--从零开始写Java Web框架>首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA.Maven.Git 等开发工具搭建 Java Web 应用:接着通 ...

  8. 【EatBook】-NO.3.EatBook.3.JavaArchitecture.2.001-《架构探险:从零开始写Java Web框架》-

    1.0.0 Summary Tittle:[EatBook]-NO.3.EatBook.3.JavaArchitecture.2.001-<架构探险:从零开始写Java Web框架>- S ...

  9. 使用 Shiro,从架构谈起,到框架集成!

    使用 Shiro,从架构谈起,到框架集成! 一.架构 1.使用用户的登录信息创建令牌 2.执行登陆动作 3.判断用户 4.两条重要的英文 二.实现Realm 1.缓存机制 2.散列算法与加密算法 3. ...

随机推荐

  1. Python3基础 e记法示例

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...

  2. ng-model绑定的是ng-option中的什么?

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script sr ...

  3. POJ 1679 The Unique MST 【判断最小生成树是否唯一】

    Description Given a connected undirected graph, tell if its minimum spanning tree is unique.  Defini ...

  4. P2042 [NOI2005]维护数列

    思路 超级恶心的pushdown 昏天黑地的调 让我想起了我那前几个月的线段树2 错误 这恶心的一道题终于过了 太多错误,简直说不过来 pushup pushdown 主要就是这俩不太清晰,乱push ...

  5. POJ3580 SuperMemo

    Your friend, Jackson is invited to a TV show called SuperMemo in which the participant is told to pl ...

  6. ISE14.7兼容性问题集锦https://www.cnblogs.com/ninghechuan/p/7241371.html

    ISE14.7兼容性问题集锦 对于电子工程师来说,很多电路设计仿真软件都是特别大的,安装下来一般都是上G,甚至几十G,而且win7的兼容性也是最好的,不愿意升级win10是因为麻烦,而且没有必要,对于 ...

  7. [0413] FFTSHIFT的四种写法

    FFTSHIFT的四种写法 前言 matlab说,"你读过书,--我便考你一考.fftshift的函数,怎样写的?"我想,讨饭一样的人,也配考我么?便回过脸去,不再理会.matla ...

  8. HDU 5727 Necklace(全排列+二分图匹配)

    http://acm.split.hdu.edu.cn/showproblem.php?pid=5727 题意:现在有n个阳珠子和n个阴珠子,现在要把它们串成项链,要求是阴阳珠子间隔串,但是有些阴阳珠 ...

  9. 2019年前端面试题 | CSS篇 (更新于4月15日)

    虽说刷面试题有走捷径之嫌,但我发现,对于我这样没有工作经历的人来说,其实是拓展自己实战技能和加深知识理解的一个好机会. 分享出来,也希望大家不要背完了事,正经的去细细琢磨各种原由. 本篇是一个题目合集 ...

  10. webpack插件配置(一) webpack-dev-server 路径配置

    本文的路径配置主要涉及到webpack.config.js文件中devServer与output两个选项的配置 webpack-dev-server定义 webpack-dev-server主要是启动 ...