Spring AOP无法拦截内部方法

Spring AOP无法拦截内部方法调用


spring aop之对象内部方法间的嵌套失效

来看一个内部调用失败的经典例子:

//如果存在事务,则抛出异常。
public class TxDemo{
   @Transactional(propagation = Propagation.NEVER)
    public void serviceH() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1000);
        infoVo.setUserName("H");
        userInfoDao.save2(infoVo);
    }

    public void serviceI() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1000);
        infoVo.setUserName("I");
        userInfoDao.save2(infoVo);
    }


    @Transactional
    public void serviceHI() {
        serviceI();
        serviceH(); 
    }
}


上面的例子中serviceH头上的注解事务说的是,如果当前存在事务则抛出异常。而我这个测试例子中,serviceHI存在了事务,再次调用serviceH,正常情况下应该会抛出异常的,但是结果却是没有异常,顺利执行了。为什么呢?

我们从实战中来看AOP的底层原理。 
Spring AOP的底层无非就是动态代理,而事务也是基于动态代理的,那我们为什么不手写动态代理呢?

代理模拟


一、基于jdk代理的分析


代码:测试Dome

第一步,先定义一个接口,我们来做一个基于JDK接口的动态代理模型。

public interface ReadableInterface {

    void read();

    void write();

}


第二步:实现接口类

public class StudentImpl implements ReadableInterface {

    @Override
    public void read() {
        System.out.println("read........");
        write();
    }

    @Override
    public void write() {
        System.out.println("write ..........");
    }

}


创建代理:

第三步:创建jdk动态代理

public class ProxyObjectImpl implements InvocationHandler {

    private Object proxied = null;

    public ProxyObjectImpl() {
    }

    public ProxyObjectImpl(Object proxied) {
        this.proxied = proxied;
    }


    /**
     * // 该方法负责集中处理动态代理类上的所有方法调用
     //
     * @param proxy:第一个参数既是代理类实例
     * @param method:第二个参数是被调用的方法对象
     * @param args:第三个方法是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行
     * 注意:千万别把第一个参数放入method.invoke里面哦,不然就会死循环了,代理类执行代理类,变成自己代理自己了
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理做事前。。。。。。。");

        Object o=method.invoke(proxied, args);
        System.out.println("代理做事后。。。。。。");
        return o;
    }
}


第四步:模拟客户端调用

public class DynamicProxyDemo {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        proxy1(); 
    }

    private static void proxy2() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        StudentImpl student = new StudentImpl();
        //获取委托方
        ReadableInterface readableInterface = (ReadableInterface) Proxy.newProxyInstance(StudentImpl.class.getClassLoader(),
                StudentImpl.class.getInterfaces(),
                new ProxyObjectImpl(student));
        readableInterface.read(); 
    }

}



输出结果为:

代理做事前。。。。。。。
read.........
write ..........
代理做事后。。。。。。


客户端调用read的时候走的是代理,所以打印出了“代理前,代理后”字样;但是write则是直接输出的,因为write()是在read()内部调用的。 
整个过程相当于

//客户端如下:
main(){
      proxy.read();
}

//被代理的接口类最终的样子如下:proxy.class

proxy.read(){
      print("调用前...");
      read();
      print("调用后...");
}

proxy.write(){
      print("调用前...");
      write();
      print("调用后...");
}

read(){
   this.write();
}

write(){
}


看到了吗?Clint里面调用的是proxy.read();是走了代理的,然后read()里面则是直接调用了write,而不是proxy.write(); 
这个不是Spring AOP的问题,而是我们使用代理他本身存在的一种问题。

以上分析也只是jdk基于接口代理所引发的问题而已。

二、基于CGlib的代理
CGLib是基于类的代理,而不是接口代理。

第一步:基础类

public class Person {

    public void play(){
        System.out.println("person is play ....");
    }

    public void getMoney(){
        play();
        System.out.println("give me 100$");
    }
}
1
2
3
4
5
6
7
8
9
10
11
第二步:编写代理


public class CglibProxyHandler implements MethodInterceptor {


    public CglibProxyHandler() {
    }

    /**
     * 1、代理对象;2、委托类方法;3、方法参数;4、代理方法的MethodProxy对象。
     * @param o
     * @param method
     * @param objects
     * @param methodProxy
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("proxy before......");
        Object o1 = methodProxy.invokeSuper(o, objects);
        System.out.println("proxy after .......");
        return o1;
    }
}

第三步:客户端编写

public class CglibProxyDome {

    public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "target/cglib");
        CglibProxyHandler cglibProxy = new CglibProxyHandler();

        //jdk需要提供接口,cglib需要是非私有类,且不能处理final关键字修饰的方法
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(Person.class);
        //设置回调对象
        enhancer.setCallback(cglibProxy); 

        Person person = (Person) enhancer.create(); 
        person.getMoney();
    }
}

打印结果:

proxy before......
proxy before......
person is play ....
proxy after .......
give me 100$
proxy after .......


你会发现内部方法调用也走了代理哦! 
为什呢?

基于类的代理模式为生成一个子类,来继承父类的方式

由CGLib生成的子类最终形态如下:

public class Person$$EnhasncerBy....... extends Person{

    public final void play(){
       print("代理前");
        super.play();
        print("代理后");
    }

    public final void getMoney(){
       print("代理前");
        super.getMoney();
        print("代理后");
    }

}

public class Demo(){
        Person p=new Person$$Enhasncer.....()
        p.getMoney();
}


来看一个测试更简单的例子

第一步:定义一个父类

public class Animal {

    public void play() {
        System.out.println("animal play .....");
    }

    public void money() {
        play();
        System.out.println("animal money......");
    }
}


第二步:模拟CGLib动态代理

public class Animal#dog extends Animal {

    public final void play() {
        System.out.println("dog play start22222.....");
        super.play();
        System.out.println("dog play end......");
    }

    public final void money() {
        System.out.println("dog money start.......");
        super.money();
        System.out.println("dog money end......");
    }

}



第三步: 模拟客户端

public class MainDemo {

    public static void main(String[] args) {
        Animal animal = new Animal#dog();
        animal.money();
    }
}


测试结果:

dog money start.......
dog play start22222.....
animal play .....
dog play end......
animal money......
dog money end......


由于new出来的是子类的对象,而不是父类的对象,所以在调用了父类的money之后,父类的money中调用了this.play(); 
但是这个this.play()不是父类中的play,而是子类Animal#dog中的play(); 因为new的对象内部存储的是父类的,而不是子类中的对象。

也就是说如果Spring Aop使用CGLib 来做动态代理,则可以避免上面说的那种,内部调用实效的问题。

Spring AOP使用CGLib解决内部调用失效的问题
第一步:

内部调用不走代理解决方法:<aop:aspectj-autoproxy expose-proxy="true" />
1
第二步: 
使用AopContent直接操作((PerformanceService) AopContext.currentProxy()).music();

@Service
public class PerformanceServiceImpl implements PerformanceService {
    public void play() {

        System.out.println("start performance......"+this.getClass()); 
       ((PerformanceService) AopContext.currentProxy()).music();
        //music();
    }

    public void music() {

        System.out.println("play music........."+this.getClass());
    }
}


基于AspectJ编译期织入
AspectJ是在java文件编译期间,织入字节码,改变原有的类。

第一步:

public class MusicDome {

    public void play() {
        System.out.println("play music...." + this.getClass());
        music();
    }

    public static void main(String[] args) {
        MusicDome dome = new MusicDome();
        dome.play();
        System.out.println("main====" + dome.getClass());
    }

    public void music() {
        System.out.println("musice......" + this.getClass());
    }
}


第二步:编写编译期间织入

public aspect AspectJDome {

    //pointcut callPointCut():call(void com.test.proxy.sound.MusicDome.play());

    //测试方法内部调用是否走了这里
    /**
     * 研究发现,静态编译织入和动态代理还是很不一样,动态代理是生成一个代理对象,然后代理你去做事情;
     * 但是静态编译织入,虽然也是代替你执行一些事情,但是他不是生成一个动态代理类,而是在你原有的类中在编译期间去改变你原有的类,在原有的
     * 类中加入一些事情。
     *
     */
    pointcut callPointCut():call(void com.test.proxy.sound.MusicDome.*());

    before():callPointCut(){
        System.out.println();
        System.out.println("-------AspectJ log--------");
        System.out.println("signature:"+thisJoinPoint.getStaticPart().getSignature());
        System.out.println("------------end---------");

    }

}



打印结果:

-------AspectJ log--------
signature:void com.test.proxy.sound.MusicDome.play()
------------end---------
play music....class com.test.proxy.sound.MusicDome

-------AspectJ log--------
signature:void com.test.proxy.sound.MusicDome.music()
------------end---------
musice......class com.test.proxy.sound.MusicDome
main====class com.test.proxy.sound.MusicDome


发现编译期织入也不会有内部调用失败的问题。

我们都知道Demo.java 文件是不能直接允许的,而是要通过编译器编译成Demo.class class字节码,这样jvm虚拟机才能识别。 
而AspectJ就是通过编译器在编译Demo.java->加入一些增强->Demo.class. 
我们来看下编译期间织入的代码样子

public class MusicDome {

    public void play() {
       System.out.println("-------AspectJ log--------");
        System.out.println("play music...." + this.getClass());
        music();
        System.out.println("-------end--------");
    }

    public static void main(String[] args) {
        MusicDome dome = new MusicDome();
        dome.play();
        System.out.println("main====" + dome.getClass());
    }

    public void music() {
        System.out.println("-------AspectJ log--------");
        System.out.println("musice......" + this.getClass());
        System.out.println("-------end--------");
    }
}


AspectJ是在编译期间就已经把增强的代码给织入到原有的类里面了,所以不管你怎么调用,都不会有影响。

代理比较一下
JDK动态代理和CGLIB字节码生成的区别? 
- JDK动态代理只能对实现了接口的类生成代理,而不能针对类 
- CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final,static,private会导致代理失效。

参考
http://blog.csdn.net/doctor_who2004/article/details/51814476 
http://www.jianshu.com/p/6534945eb3b5
--------------------- 
作者:piaoslowly 
来源:CSDN 
原文:https://blog.csdn.net/piaoslowly/article/details/81743692 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值