【JAVA】浅谈AOP

AOP (面向切面编程)Aspect Oriented Programming的缩写

优点:解耦

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。
主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

AOP相关概念

1.方面(Aspect)
2.连接点(Joinpoint)
3.通知(Advice): 各种类型的通知包括“around”、“before”和“throws”通知。
许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。
Spring中定义了四个advice:
BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
4.切入点(Pointcut)
5.引入(Introduction): 添加方法或字段到被通知的类。
6.目标对象(Target Object): 被通知或被代理对象。
7.AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。
8.织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

Spring AOP代理对象的生成

Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。

  • 静态代理
  • JDK动态代理
  • CGlib动态代理

静态代理

静态代理很简单,咱们自己在写代码的时候都会写到这种类似静态代理的代码。简单来说,就是把被代理类作为参数传给代理类的构造方法,让代理类替被代理类实现更强大的功能。

public class StaticProxyTest { 
      public static void main(String[] args) {
            UserService userService = new UserService();
            LogProxy logProxy = new LogProxy(userService);
            logProxy.addUser();
            logProxy.deleteUser();
      }
}

interface IUserService{
      void addUser();
      void deleteUser();
}

class UserService implements IUserService{
      @Override
      public void addUser() {
            System.out.println("添加用户");
        }
        @Override    
        public void deleteUser() {
            System.out.println("删除用户");
        }
}

//日志代理
class LogProxy implements IUserService{    
    //目标类    
    private UserService target;

    public LogProxy(UserService target){
          this.target = target;
      }
  
      @Override    
      public void addUser() {
          System.out.println("记录日志开始");
          target.addUser();
          System.out.println("记录日志结束");
      }
  
      @Override
      public void deleteUser() {
          System.out.println("记录日志开始");
          target.deleteUser();
          System.out.println("记录日志结束");
      }
}

动态代理在Spring源码中,用到的动态代理主要有两种,JDK动态代理以及CGLib动态代理。

两者主要区别是:
JDK动态代理一般针对实现了接口的类生成代理。
目标对象没有实现接口,则默认会采用CGLIB代理。如果目标对象实现了接口,可以强制使用CGLIB实现代理。

相同点:两种动态代理本质上都是字节码组装


如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标对象实现的接口都将被代理。 若该目标对象没有实现任何接口,则创建一个CGLIB代理。
CGLIB代理模式下每一个目标对象创建一个子类。每一个代理实例会生成两个对象:实际代理对象和它的一个实现了通知的子类实例。
且CGLib的效率没有使用JDK代理机制高,速度平均要慢8倍左右。


JDK动态代理的代理类一般需要实现接口

public class JdkProxyTest {
    public static void main(String[] args) {
        IPersonService personService = JdkDynamicProxy.getProxy();
        personService.addPerson();
        personService.deletePerson();
    }
}

interface IPersonService{
    void addPerson();
    void deletePerson();
}

class PersonService implements IPersonService{
    @Override   
    public void addPerson() {
        System.out.println("添加人物");
    }
    @Override
    public void deletePerson() {
        System.out.println("删除人物");
    }
}

/**
 * newProxyInstance方法参数说明:
 * ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的
 * Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
*  InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法
 */
class JdkDynamicProxy{
    public static IPersonService getProxy(){

        IPersonService personService = new PersonService();

        IPersonService proxy = (IPersonService) Proxy.newProxyInstance(IPersonService.class.getClassLoader(), new Class<?>[]{IPersonService.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
               System.out.println("记录日志开始");
               Object obj = method.invoke(personService, args);
               System.out.println("记录日志结束");
              return obj;
          }
      });
       return proxy;
    }
}

CGLib动态代理

public class CglibProxyTest {
    public static void main(String[] args) {
        CglibProxy proxy = new CglibProxy();
        Train t = (Train)proxy.getProxy(Train.class);
        t.move();
    }
}

class Train {
    public void move(){
        System.out.println("火车行驶中...");
    }
}

class CglibProxy implements MethodInterceptor {

    private Enhancer enhancer = new Enhancer();

    public Object getProxy(Class clazz){
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    /**
     * 拦截所有目标类方法的调用
     * obj  目标类的实例
     * m   目标方法的反射对象
     * args  方法的参数
     * proxy代理类的实例
     */
    @Override
    public Object intercept(Object obj, Method m, Object[] args,  MethodProxy proxy) throws Throwable {
        System.out.println("日志开始...");
        //代理类调用父类的方法
        proxy.invokeSuper(obj, args);
        System.out.println("日志结束...");
        return null;
    }
}

原理分析

下面我们来研究一下Spring如何使用JDK来生成代理对象,具体的生成代码放在JdkDynamicAopProxy这个类中,假设我们要对下面这个用户管理进行代理:

public interface UserMgr {  
    void addUser();  
    void delUser();  
}  

public class UserMgrImpl implements UserMgr {  
    @Override  
    public void addUser() {  
        System.out.println("添加用户.....");  
    }  

    @Override  
    public void delUser() {  
        System.out.println("删除用户.....");  
    }  
}  

按照代理模式的实现方式,肯定是用一个代理类,让它也实现UserMgr接口,然后在其内部声明一个UserMgrImpl,然后分别调用addUser和delUser方法,并在调用前后加上我们需要的其他操作。但是这样很显然都是写死的,我们怎么做到动态呢?别急,接着看。 我们知道,要实现代理,那么我们的代理类跟被代理类都要实现同一接口,但是动态代理的话我们根本不知道我们将要代理谁,也就不知道我们要实现哪个接口,那么要怎么办呢?我们只有知道要代理谁以后,才能给出相应的代理类,那么我们何不等知道要代理谁以后再去生成一个代理类呢?想到这里,我们好像找到了解决的办法,就是动态生成代理类!

这时候我们亲爱的反射又有了用武之地,我们可以写一个方法来接收被代理类,这样我们就可以通过反射知道它的一切信息——包括它的类型、它的方法等等。

JDK动态代理的两个核心分别是InvocationHandler和Proxy,下面我们就用简单的代码来模拟一下它们是怎么实现的:
InvocationHandler接口:

package com.tgb.proxy;  

import java.lang.reflect.Method;  

public interface InvocationHandler {  
    public void invoke(Object o, Method m);  
} 

实现动态代理的关键部分,通过Proxy动态生成我们具体的代理类:
动态生成Java源文件,写入本地,然后将Java文件编译成class文件,加载到内存,并实例化,删除本地源文件。

package com.tgb.proxy;  

public class Proxy {  
    /** 
     *  
     * @param infce 被代理类的接口 
     * @param h 代理类 
     * @return 
     * @throws Exception 
     */  
    public static Object newProxyInstance(Class infce, InvocationHandler h) throws Exception {   
        String methodStr = "";  
        String rt = "\r\n";  

        //利用反射得到infce的所有方法,并重新组装  
        Method[] methods = infce.getMethods();    
        for(Method m : methods) {  
            methodStr += "    @Override" + rt +   
                         "    public  "+m.getReturnType()+" " + m.getName() + "() {" + rt +  
                         "        try {" + rt +  
                         "        Method md = " + infce.getName() + ".class.getMethod(\"" + m.getName() + "\");" + rt +  
                         "        h.invoke(this, md);" + rt +  
                         "        }catch(Exception e) {e.printStackTrace();}" + rt +                          
                         "    }" + rt ;  
        }  

        //生成Java源文件  
        String srcCode =   
            "package com.tgb.proxy;" +  rt +  
            "import java.lang.reflect.Method;" + rt +  
            "public class $Proxy1 implements " + infce.getName() + "{" + rt +  
            "    public $Proxy1(InvocationHandler h) {" + rt +  
            "        this.h = h;" + rt +  
            "    }" + rt +            
            "    com.tgb.proxy.InvocationHandler h;" + rt +                           
                methodStr + rt +  
            "}";  

        String fileName =  "d:/src/com/tgb/proxy/$Proxy1.java";  
        File f = new File(fileName);  
        FileWriter fw = new FileWriter(f);  
        fw.write(srcCode);  
        fw.flush();  
        fw.close();  

        //将Java文件编译成class文件  
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();  
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);  
        Iterable units = fileMgr.getJavaFileObjects(fileName);  
        CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);  
        t.call();  
        fileMgr.close();  

        //加载到内存,并实例化  
        URL[] urls = new URL[] {new URL("file:/" + "d:/src/")};  
        URLClassLoader ul = new URLClassLoader(urls);  
        Class c = ul.loadClass("com.tgb.proxy.$Proxy1");  

        Constructor ctr = c.getConstructor(InvocationHandler.class);  
        Object m = ctr.newInstance(h);  

        return m;  
    }  
} 

这个类的主要功能就是,根据被代理对象的信息,动态组装一个代理类,生成Proxy1.java文件,然后将其编译成Proxy1.class。这样我们就可以在运行的时候,根据我们具体的被代理对象生成我们想要的代理类了。这样一来,我们就不需要提前知道我们要代理谁。也就是说,你想代理谁,想要什么样的代理,我们就给你生成一个什么样的代理类。

然后,在客户端我们就可以随意的进行代理了。

package com.tgb.proxy;  

public class Client {  
    public static void main(String[] args) throws Exception {  
        UserMgr mgr = new UserMgrImpl();  

        //为用户管理添加事务处理  
        InvocationHandler h = new TransactionHandler(mgr);  
        UserMgr u = (UserMgr)Proxy.newProxyInstance(UserMgr.class,h);  

        //为用户管理添加显示方法执行时间的功能  
        TimeHandler h2 = new TimeHandler(u);  
        u = (UserMgr)Proxy.newProxyInstance(UserMgr.class,h2);  

        u.addUser();  
        System.out.println("\r\n==========华丽的分割线==========\r\n");  
        u.delUser();  
    }  
}  

事务处理:

package com.tgb.proxy;  

import java.lang.reflect.Method;  

public class TransactionHandler implements InvocationHandler {  

    private Object target;  

    public TransactionHandler(Object target) {  
        super();  
        this.target = target;  
    }  

    @Override  
    public void invoke(Object o, Method m) {  
        System.out.println("开启事务.....");  
        try {  
            m.invoke(target);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        System.out.println("提交事务.....");  
    }  
}  

运行结果:

开始时间:2014年-07月-15日 15时:48分:54秒  
开启事务.....  
添加用户.....  
提交事务.....  
结束时间:2014年-07月-15日 15时:48分:57秒  
耗时:3秒  

==========华丽的分割线==========  

开始时间:2014年-07月-15日 15时:48分:57秒  
开启事务.....  
删除用户.....  
提交事务.....  
结束时间:2014年-07月-15日 15时:49分:00秒  
耗时:3秒  

项目中使用AOP遇到的坑

1.在Spring配置文件中配置了事务管理器,如下:

<!--事务管理器配置,单数据源事务-->
<tx:annotation-driven transaction-manager="transactionManager" />  
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource" ref="dataSource" />
</bean>

2.配置了事务管理器后,加入了@Transactional注解

@Service
@Transactional
public class AccountService{
    //to do something
}

上面配置默认启用JDK动态代理,JDK只能代理接口不能代理类。而我的项目中用的是这个配置,却因为没有定义Service接口导致项目启动报错。

如果需要使用CGLIB动态代理:配置如下:

<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/> 

关于spring的aop拦截的问题 protected方法代理问题
jdk是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截到;
cglib是子类,private的方法照样不会出现在子类里,也不能被拦截。


Example

public class ServiceA {  
  
    private ServiceA  self;  
  
    public void setSelf(ServiceA self) {  
        this.self = self;  
    }  
  
    public String methodA(String str) {  
        System.out.println("methodA: args=" + str);  
        self.methodB("b");  
        return "12345" + str;  
    }  
  
    private String methodB(String str) {  
        System.out.println("methodB: args=" + str);  
        self.methodC("c");  
        return "12345" + str;  
    }  
  
    public String methodC(String str) {  
        System.out.println("methodC: args=" + str);  
        return "12345" + str;  
    }  
}  

execution(* test.aop.ServiceA.*(..))
如果外部调用methodA,那么methodA和methodC会被拦截到,methodB不行,但是protected方法能被拦截到

execution(public * test.aop.ServiceA.*(..))
还有个奇怪的现象,execution里如果不写权限,那么public protected package的方法都能被拦截到
如果写了public,那就只拦截public方法这个没问题,
如果写了protected,他就什么事情都不做,连protected的方法也不拦截。

分析
在Spring使用纯Spring AOP(只能拦截public/protected/包),private方法都是无法被拦截的,因为子类无法覆盖;包级别能被拦截的原因是,如果子类和父类在同一个包中是能覆盖的。

在cglib代理情况下, execution(* *(..)) 可以拦截 public/protected/包级别方法(即这些方法都是能代理的)。

private static boolean isOverridable(Method method, Class targetClass) {  
        if (Modifier.isPrivate(method.getModifiers())) {  
            return false;  
        }  
        if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) {  
            return true;  
        }  
        return getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass));  
    }  

如果想要实现拦截private方法的 可以使用 原生 AspectJ 编译期/运行期织入。

场景1:execution(* *(..))

public class Impl2  {  
      
    protected/public String testAop2() {  
        System.out.println("234");  
        return "1233";  
    }  
}  

因为切入点没有访问修饰符,即可以是任意,因此canApply方法能拿到这种protected/public方法,即可以实施代理。


场景2:execution(public * *(..))

public class Impl2  {  
      
    public String testAop2() {  
        System.out.println("234");  
        return "1233";  
    }  
}  

因为拦截public的,因此canApply方法能拿到public方法(拿不到protected),即可以实施代理。


场景3:execution(protected * *(..))

public class Impl2  {  
      
    protected String testAop2() {  
        System.out.println("234");  
        return "1233";  
    }  
}  

还记得之前说过,在canApply方法中 的 Method[] methods = clazz.getMethods();只能拿到public方法的,因此跟protected访问修饰符是无法匹配的,所以如果“execution(protected * *(..))” 是 无法代理的。


在使用代理的时候,@Transactional 注解应该只被应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,系统也不会报错, 但是这个被注解的方法将不会执行已配置的事务设置。如果你非要注解非公共方法的话,请参考使用AspectJ

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容