AOP的理解

拈花ヽ惹草 提交于 2019-11-26 17:16:10

描述:对于AOP术语一些总结以及相关的案例
标签:Spring 面向切面编程

  • 目录

    {:toc}

几个术语的基本解释

  1. 通知

    直接明白的说就是你想要的功能(为了更清晰的逻辑,让你的业务逻辑去关注本身的业务,而不是去想其他的事情,这些其他的事情包括,比如安全、事务、日志等)

  2. 连接点

    就是允许你使用通知的地方,比如每个方法的前后或者抛出异常时都是连接点。Spring只支持方法连接点

  3. 切入点

    比如说,你的一个类里有15个方法,那不就是几十个连接点了,但是你不可能在所有方法上面都写一个通知吧,你只是想让其中几个方法,就可以使用切点去筛选连接点,选中那几个你想要的方法

  4. 切面

    切面就是通知和切入点的集合,也就是一个类。没连接点什么事情,连接点就是为了让你更好的去理解切点。通知说明了干什么和什么时候干(通过@Before @After等注解就可以明白),而切入点说明了在哪里干(指定到底是哪个方法)

  5. 引入

    允许我们向现有的类(目标类)添加新方法属性。其实说白了就是切面类(这里不就是定义了切入点 通知嘛),就是切面类可以为目标类添加新方法属性。

  6. 目标

    也就是上面提到的目标类,也就是需要被通知的对象。这个目标类的话就是我们的真正业务逻辑,而不是其他事情(其他事情在切面类中去做,上面提到的日志、事务等)

  7. 代理

    怎么实现整套AOP机制的,都是通过代理

  8. 织入

    把切面类上的逻辑应用到目标对象创建新的代理对象的过程。

AOP原理

​Spring使用代理类包裹切面,把代理类织入到Spring管理的Bean中。也就是说代理类会先执行切面,然后转发给真正的bean去执行相应的逻辑。

🐫第一种模式:兄弟模式

1、实现和目标类相同的接口,我也实现和你一样的接口,反正上层都是接口级别的调用,这样我就伪装成了和目标类一样的类(实现了同一接口,咱是兄弟了),也就逃过了类型检查,到java运行期的时候,利用多态的后期绑定(所以spring采用运行时),伪装类(代理类)就变成了接口的真正实现,而他里面包裹了真实的那个目标类,最后实现具体功能的还是目标类,只不过伪装类在之前干了点事情(写日志,安全检查,事物等)。

2、这就好比,一个人让你办件事,每次这个时候,你弟弟就会先出来,当然他分不出来了,以为是你,你这个弟弟虽然办不了这事,但是他知道你能办,所以就答应下来了,并且收了点礼物(写日志),收完礼物了,给把事给人家办了啊,所以你弟弟又找你这个哥哥来了,最后把这是办了的还是你自己。但是你自己并不知道你弟弟已经收礼物了,你只是专心把这件事情做好。

🐫第二种模式:父子模式

1、要是本身这个类就没实现一个接口呢,也就是说兄弟没法做了,我就压根没有机会让你搞出这个双胞胎的弟弟,那么就用其他方式,创建一个目标类的子类,生个儿子,让儿子做代理。生成子类调用,这次用子类来做为代理类,子类重写了目标类的所有方法,当然在这些重写的方法中,不仅实现了目标类的功能,还在这些功能之前,实现了一些其他的(写日志,安全检查,事务等)。

2、 这次的对比就是。儿子先从爸爸那把本事都学会了,所有人都找儿子办事情,但是儿子每次办和爸爸同样的事之前,都要收点小礼物(写日志),然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说,某些本事是爸爸独有的(final的),儿子学不了,学不了就办不了这件事,办不了这个事情,自然就不能收人家礼了。

总结

前一种兄弟模式,spring会使用JDK的java.lang.reflect.Proxy类,它允许Spring动态生成一个新类来实现必要的接口,织入通知,并且把对这些接口的任何调用都转发到目标类。

后一种父子模式,spring使用CGLIB库生成目标类的一个子类,在创建这个子类的时候,spring织入通知,并且把对这个子类的调用委托到目标类。
相比之下,还是兄弟模式好些,他能更好的实现松耦合,尤其在今天都高喊着面向接口编程的情况下,父子模式只是在没有实现接口的时候,也能织入通知,应当做一种例外。

实际案例

原始写法


现在的需求是在一个接口里面方法调用的前后,打印调用钱的毫秒数以及调用后的毫秒数


如下面的代码,这是接口的定义:

package com.hht.api.aopdemo;

/**
 * @author hht
 * @create 2019-08-09 11:16
 */
public interface Dao {
    public void insert();
    public void delete();
}

下面是实现类:

package com.hht.api.aopdemo;

/**
 * @author hht
 * @create 2019-08-09 11:22
 */
public class DaoImpl implements Dao {
    @Override
    public void insert() {
        System.out.println("现在调用的是insert方法");
    }
    @Override
    public void delete() {
        System.out.println("现在调用的是delete方法");
    } 
}

现在需要在调用前后实现打印功能的话 那么最原始的写法是,新建一个类去包装他们,再书写一层方法

package com.hht.api.aopdemo;

/**
 * @author hht
 * @create 2019-08-09 11:28
 */
public class ServiceImpl {

      private Dao dao = new DaoImpl();

      public  void insert(){
          System.out.println("insert执行开始的时间是:"+System.currentTimeMillis());
          dao.insert();
          System.out.println("insert执行结束的时间是:"+System.currentTimeMillis());
      }

    public   void delete(){
        System.out.println("delete执行开始的时间是:"+System.currentTimeMillis());
        dao.delete();
        System.out.println("delete执行结束的时间是:"+System.currentTimeMillis());
    }
    
}

这种原始的写法缺点比较明显:

1、输出调用前后时间的业务逻辑无法复用

2、如果这个Dao还有其他的实现类的,还需要书写一个新的ServiceImpl类进行包装实现业务逻辑

使用装饰者模式

装饰者模式的核心就是实现Dao接口同时持有Dao接口的引用。我们新建类LogDao

package com.hht.api.aopdemo;

/**
 * @author hht
 * @create 2019-08-09 11:46
 */
public class LogDao implements Dao {
    
    private Dao dao;

    public LogDao(Dao dao) {
        this.dao = dao;
    }

    @Override
    public void insert() {
        System.out.println("insert执行开始的时间是:"+System.currentTimeMillis());
        dao.insert();
        System.out.println("insert执行结束的时间是:"+System.currentTimeMillis());
    }

    @Override
    public void delete() {
        System.out.println("delete执行开始的时间是:"+System.currentTimeMillis());
        dao.delete();
        System.out.println("delete执行结束的时间是:"+System.currentTimeMillis());
    }
}

在实际调用的时候 使用 Dao dao = new LogDao(new DaoImpl())的方式调用,里面DaoIml是实际实现接口的类。

可以看出这种方式的优点:

1、透明。对于调用着来说。他只知道他自己实现的dao功能,但是日志功能他不知晓,实现了透明化

2、类不会无限膨胀,如果说Dao的其他实现类需要输出日志,只需要向LogDao的构造函数中传入对应的实现类即可。

缺点:

1、其实还是一样的问题,就是输出日志的逻辑不能实现复用

2、输出日志部分的逻辑代码和实际业务逻辑代码有耦合(其实说的就是第一个缺点)

使用代理模式(Java代理)

使用代理模式的话,那么我们需要定义InvocationHandler(实现该接口)。

下面是代理类的实现代码:

package com.hht.api.aopdemo;

import org.omg.PortableInterceptor.ObjectReferenceFactory;
import sun.reflect.generics.scope.MethodScope;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @author hht
 * @create 2019-08-09 14:28
 */
public class LogInvocationHandler implements InvocationHandler {

    private Object object;

    public LogInvocationHandler(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        String methodName = method.getName();
        if("insert".equals(methodName) || "delete".equals(methodName)){
            System.out.println(methodName +"()方法开始执行的时间是:"+System.currentTimeMillis());
         //实际执行目标的业务逻辑
            Object result = method.invoke(object, args);
            System.out.println(methodName +"()方法结束的时间是:"+System.currentTimeMillis());
            return result;
        }

        return method.invoke(object,args);
    }
}

测试类的书写:(这里直接使用main了)

package com.hht.api.aopdemo;

import java.lang.reflect.Proxy;

/**
 * @author hht
 * @create 2019-08-09 11:34
 */
public class Test {
    public static void main(String[] args) {
//        ServiceImpl service = new ServiceImpl();
//
//        service.insert();
//        service.delete();

        //测试代理模式
        Dao dao = new DaoImpl();
   
        Dao proxyDao = (Dao) Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), new Class<?>[]{Dao.class}, new LogInvocationHandler(dao));
        proxyDao.insert();
        System.out.println("-----------------");
        proxyDao.delete();
        System.out.println("-------------------");
    }
}

显然优点是比较明显的:

1、输出日志的逻辑已经复用起来了,如果需要针对其他接口用上输出日志的逻辑,只要在newProxyInstance的后面的第二个参数增加Class<?>数组里面的内容即可

缺点:

1、 JDK提供的动态代理只能针对接口做代理,不能针对类做代理

2、代码还是不足,如果需要对接口中新的方法,比如说新加了一个update方法,也需要添加日志打印,那么你在带来类中还是需要增加相应的判断

使用代理模式(CGLIB代理)

使用CGLIB的话,需要实现MethodInterceptor

下面是代理类的实现代码:

package com.hht.api.aopdemo;


import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @author hht
 * @create 2019-08-09 14:57
 * CGLIB代理类
 */
public class DaoProxy implements MethodInterceptor {


    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        String methodName = method.getName();
     
        if("insert".equals(methodName) || "delete".equals(methodName)){
            System.out.println(methodName +"()方法开始执行的时间是:"+System.currentTimeMillis());
            //实际执行目标的业务逻辑
            methodProxy.invokeSuper(object,args);
            System.out.println(methodName +"()方法结束的时间是:"+System.currentTimeMillis());
            return object;
        }
        methodProxy.invokeSuper(object,args);
        return object;
    }
}

测试类的书写:

package com.hht.api.aopdemo;

import org.springframework.cglib.proxy.Enhancer;

import java.lang.reflect.Proxy;

/**
 * @author hht
 * @create 2019-08-09 11:34
 */
public class Test {


    public static void main(String[] args) {

//        ServiceImpl service = new ServiceImpl();
//
//        service.insert();
//        service.delete();

        //测试代理模式 Java代理
        Dao dao = new DaoImpl();

        Dao proxyDao = (Dao) Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), new Class<?>[]{Dao.class}, new LogInvocationHandler(dao));

        proxyDao.insert();
        System.out.println("-----------------");
        proxyDao.delete();
        System.out.println("-------------------");

        System.out.println("CGLIB开始");

        //测试代理模式  CGLIB代理
        DaoProxy daoProxy = new DaoProxy();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(DaoImpl.class); //这是与Java代理的不同 可以对类进行代理
        enhancer.setCallback(daoProxy);

        Dao dao1  = (DaoImpl)enhancer.create();
        dao1.insert();
        System.out.println("-----------------");
        dao1.delete();
        System.out.println("--------------");
    }
}

对比的话,其实好像和JDK代理没有什么区别,只是CGLIB解决了JDK代理只能针对接口做代理的不足。

转下区别: https://blog.csdn.net/yhl_jxy/article/details/80635012

1、JDK动态代理

利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,

在调用具体方法前调用InvokeHandler来处理。

2、CGLIB动态代理

利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

3、何时使用JDK还是CGLIB?

1)如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。

2)如果目标对象实现了接口,可以强制使用CGLIB实现AOP。

3)如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。

4、如何强制使用CGLIB实现AOP?

1)添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar)

2)在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class=“true”/>

5、JDK动态代理和CGLIB字节码生成的区别?

1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类。

2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,

 并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,

 对于final类或方法,是无法继承的。
6、CGlib比JDK快?

1)使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,

在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理,

因为CGLib原理是动态生成被代理类的子类。

2)在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,

只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,

总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐。

7、Spring如何选择用JDK还是CGLIB?

1)当Bean实现接口时,Spring就会用JDK的动态代理。

2)当Bean没有实现接口时,Spring使用CGlib是实现。

3)可以强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class=“true”/>)。


千呼万唤始出来的AOP


使用AOP

Aop的概念:

1、AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用

2、使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

比如说现在的需求是在访问每个Controller前 记录下请求方法 参数邓 访问后返回结果

首先需要定义一个切面(切入点和通知的集合)代码如下:

package com.hht.api.aopdemo;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
 *
 *切面 需要进行增强的业务实现 在这里进行编码书写
 */
@Aspect
@Component
public class WebLogAcpect {
    private Logger logger = LoggerFactory.getLogger(WebLogAcpect.class);
    /**
     * 定义切入点,切入点为com.example.aop下的所有函数
     */
    @Pointcut("execution(public * com.hht.api.controller.*.*(..))")
    public void webLog(){}

    /**
     * 前置通知:在连接点之前执行的通知
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录下请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
    }

    @AfterReturning(returning = "ret",pointcut = "webLog()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        logger.info("RESPONSE : " + ret);
    }
}

Controller层代码:

@RequestMapping(value = "/sayHello",method = RequestMethod.GET)
    public String sayHello(String name){
        return "hello " + name;
    }

观看控制台信息:
在这里插入图片描述
这里我们可以看出:

1、切面上写的代码是可以复用的

2、避免代理类中去关注目标类的方法,我们只需要关注切面内容本身

3、代码和代码之间没有耦合

用一张图来展示AOP的作用:
在这里插入图片描述

AOP总结

我们传统的编程方式是垂直化的编程,即A–>B–>C–>D这么下去,一个逻辑完毕之后执行另外一段逻辑。但是AOP提供了另外一种思路,它的作用是在业务逻辑不知情(即业务逻辑不需要做任何的改动)的情况下对业务代码的功能进行增强,这种编程思想的使用场景有很多,例如事务提交、方法执行之前的权限检测、日志打印、方法调用事件等等。

AOP使用场景举例

​ 1、MyBatis的事务默认是不会自动提交的,因此在编程的时候我们必须在增删改完毕之后调用SqlSession的commit()方法进行事务提交,这非常麻烦,下面利用AOP简单写一段代码帮助我们自动提交事务。

代码:

 public void commit(JoinPoint jp) {
        Object obj = jp.getTarget();
        if (obj instanceof MailDao) {
            Signature signature = jp.getSignature();
            if (signature instanceof MethodSignature) {
                SqlSession sqlSession = SqlSessionThrealLocalUtil.getSqlSession();       
                MethodSignature methodSignature = (MethodSignature)signature;
                Method method = methodSignature.getMethod();        
                String methodName = method.getName();
                if (methodName.startsWith("insert") || methodName.startsWith("update") || methodName.startsWith("delete")) {
                    sqlSession.commit();
                }    
                sqlSession.close();
            }
        }
    }

2、权限控制的例子,不管是从安全角度考虑还是从业务角度考虑,我们在开发一个Web系统的时候不可能所有请求都对所有用户开放,因此这里就需要做一层权限控制了,大家看AOP作用的时候想必也肯定会看到AOP可以做权限控制,这里我就演示一下如何使用AOP做权限控制。我们知道原生的Spring MVC,Java类是实现Controller接口的,基于此,利用AOP做权限控制的大致代码如下

 public void hasPermission(JoinPoint jp) throws Exception {
        Object obj = jp.getTarget();
        
        if (obj instanceof Controller) {
            Signature signature = jp.getSignature();
            MethodSignature methodSignature = (MethodSignature)signature;
            // 获取方法签名
            Method method = methodSignature.getMethod();
            // 获取方法参数
            Object[] args = jp.getArgs();  
            // Controller中唯一一个方法的方法签名ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
            // 这里对这个方法做一层判断
            if ("handleRequest".equals(method.getName()) && args.length == 2) {
                Object firstArg = args[0];
                if (obj instanceof HttpServletRequest) {
                    HttpServletRequest request = (HttpServletRequest)firstArg;
                    // 获取用户id
                    long userId = Long.parseLong(request.getParameter("userId"));
                    // 获取当前请求路径
                    String requestUri = request.getRequestURI();     
                    if(!PermissionUtil.hasPermission(userId, requestUri)) {
                        throw new Exception("没有权限");
                    }
                }
            }
        }
        
    }

= args[0];
if (obj instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest)firstArg;
// 获取用户id
long userId = Long.parseLong(request.getParameter(“userId”));
// 获取当前请求路径
String requestUri = request.getRequestURI();
if(!PermissionUtil.hasPermission(userId, requestUri)) {
throw new Exception(“没有权限”);
}
}
}
}

}



易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!