点击上方"欧学长的架构成长之路" 关注我

前言

首先说一下什么是AOP?

AOP就是面向切面编程,它是一个思想,通过切面,我们可以将那些反复出现的代码抽取出来,放在一个地方统一处理,提高代码的复用性。AOP的一大好处就是解耦。以下几种方式实现AOP:

1自定义注解+@Aspect

2拦截器

3过滤器

4.JDK动态代理和CGlib

5.设计模型--静态代理

*.基于非侵入式运行时AOP方案(篇幅问题,不细说,感兴趣的朋友可以自行百度阿里开源的jvm-Sandbox)

自定义注解+@Aspect 实现日志记录

1.首先你需要先引入pom依赖。(springboot2.x默认使用的代理是cglib代理)

<dependency>    <groupId>org.springframework.bootgroupId>    <artifactId>spring-boot-starter-aopartifactId>dependency><dependency>    <groupId>com.google.code.gsongroupId>    <artifactId>gsonartifactId>    <version>2.8.5version>dependency>

注意:

  • 在application.properties中也不需要添加spring.aop.auto=true,这个默认就是true,值为true就是启用@EnableAspectJAutoProxy注解了。

  • 你不需要手工添加在启动类上添加 @EnableAspectJAutoProxy 注解。

  • 当你需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true,这个默认值是false,不然默认使用的是标准Java的实现(JDK动态代理基于接口代理)。

2.自定义日志注解(使用Java元注解,Java5.0定义了4个标准的meta-annotation类型)

@Retention(RetentionPolicy.RUNTIME) //定义为运行时使用注解@Target({ElementType.METHOD})//在方法上使用注解@Documented//注解将包含javaDoc中public @interface WebLog {    /**     * 日志描述信息     * @return     */     //定义一个属性,默认作为空字符串    String description() default "";}

3.配置AOP切面类

@Aspect@Component //将这个类交给Spring管理public class WebLogAspect {    private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);    /** 换行符 */    private static final String LINE_SEPARATOR = System.lineSeparator();    /** 以自定义 @WebLog 注解为切点 */    @Pointcut("@annotation(site.exception.aspect.WebLog)") //<=全路径    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();        // 获取 @WebLog 注解的描述信息        String methodDescription = getAspectLogDescription(joinPoint);        // 打印请求相关参数        logger.info("========================================== Start ==========================================");        // 打印请求 url        logger.info("URL: {}", request.getRequestURL().toString());        // 打印描述信息        logger.info("Description : {}", methodDescription);        // 打印 Http method        logger.info("HTTP Method : {}", request.getMethod());        // 打印调用 controller 的全路径以及执行方法        logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());        // 打印请求的 IP        logger.info("IP : {}", request.getRemoteAddr());        // 打印请求入参        logger.info("Request Args : {}", new Gson().toJson(joinPoint.getArgs()));    }    /**     * 在切点之后织入     * @throws Throwable     */    @After("webLog()")    public void doAfter() throws Throwable {        // 接口结束后换行,方便分割查看        logger.info("=========================================== End ===========================================" + LINE_SEPARATOR);    }    /**     * 环绕     * @param proceedingJoinPoint     * @return     * @throws Throwable     */    @Around("webLog()")    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {        long startTime = System.currentTimeMillis();       //执行切点后,会去依次调用 @Before -> 接口逻辑代码(之后,执行完doAround方法) -> @After -> @AfterReturning;        Object result = proceedingJoinPoint.proceed();        // 打印出参        logger.info("Response Args  : {}", new Gson().toJson(result));        // 执行耗时        logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);        return result;    }    /**     * 获取切面注解的描述     *     * @param joinPoint 切点     * @return 描述信息     * @throws Exception     */    public String getAspectLogDescription(JoinPoint joinPoint)            throws Exception {        String targetName = joinPoint.getTarget().getClass().getName();        String methodName = joinPoint.getSignature().getName();        Object[] arguments = joinPoint.getArgs();        Class targetClass = Class.forName(targetName);        Method[] methods = targetClass.getMethods();        StringBuilder description = new StringBuilder("");        for (Method method : methods) {            if (method.getName().equals(methodName)) {                Class[] clazzs = method.getParameterTypes();                if (clazzs.length == arguments.length) {                    description.append(method.getAnnotation(WebLog.class).description());                    break;                }            }        }        return description.toString();    }}

4.使用注解

@PostMapping("/user")@WebLog(description="用户请求接口")public User userLogin(@RequestBody User user){  logger.info("user login ...");  return user; }

特别说明

多切面如何指定优先级?

假设说我们的服务中不止定义了一个切面,比如说我们针对 Web 层的接口,不止要打印日志,还要校验 token 等。要如何指定切面的优先级呢?也就是如何指定切面的执行顺序?

我们可以通过 @Order(i)注解来指定优先级,注意:i 值越小,优先级则越高。

假设说我们定义上面这个日志切面的优先级为 @Order(10), 然后我们还有个校验 token 的切面 CheckTokenAspect.java,我们定义为了 @Order(11), 那么它们之间的执行顺序如下

spring借鉴了AspectJ的切面,以提供注解驱动的AOP,本质上它依然是Spring基于代理的AOP,只是编程模型与AspectJ完全一致,这种风格的好处就是不需要使用XML进行配置。

通过拦截器实现

拦截器拦截的是URL

拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。Spring中拦截器有三个方法:preHandle,postHandle,afterCompletion。

@Configurationpublic class HomeOpenHandlerConfigration extends WebMvcConfigurerAdapter {    //关键,将拦截器作为bean写入配置中    @Bean    public HomeOpenInterceptor myInterceptor(){        return new HomeOpenInterceptor();    }        @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(myInterceptor()).addPathPatterns("/api/open/portal/**")        .excludePathPatterns("/api/open/footerInfo").excludePathPatterns("/api/open/portal/template/default");        super.addInterceptors(registry);    }}
/** * 首页外放拦截器 * */@Componentpublic class HomeOpenInterceptor extends HandlerInterceptorAdapter {        @Autowired    private PortalCommonService portalCommonService;    @Autowired    private ApplicationProperties applicationProperties;        @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)            throws Exception {        //判断是否需要拦截        Boolean flag = false;        if(flag){            //判断是否允许不登录的情况下 访问主页            //如果不允许匿名访问返回401               throw new UnauthenticatedException();        }        //否则允许直接放过,不进行任何拦截        return true;    }}

过滤器的实现

过滤器拦截的是URL

Spring中自定义过滤器(Filter)一般只有一个方法,返回值是void,当请求到达web容器时,会探测当前请求地址是否配置有过滤器,有则调用该过滤器的方法(可能会有多个过滤器),然后才调用真实的业务逻辑,至此过滤器任务完成。过滤器并没有定义业务逻辑执行前、后等,仅仅是请求到达就执行。

特别注意:过滤器方法的入参有request,response,FilterChain,其中FilterChain是过滤器链,使用比较简单,而request,response则关联到请求流程,因此可以对请求参数做过滤和修改,同时FilterChain过滤链执行完,并且完成业务流程后,会返回到过滤器,此时也可以对请求的返回数据做处理。

@Component@Order(1) //注解,配合 @WebFilter 注解使用,用于多个过滤器时定义执行顺序,值越小越先执行。@WebFilter(urlPatterns = "/*", filterName = "test")public class TestFilter implements Filter {   @Override  public void init(FilterConfig arg0) throws ServletException {    System.out.println("过滤器初始化");  }   @Override  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)      throws IOException, ServletException {    System.out.printf("过滤器实现");    System.out.println(((HttpServletRequest) servletRequest).getRequestURI());    filterChain.doFilter(servletRequest, servletResponse);  }   @Override  public void destroy() {    System.out.println("过滤器销毁了");  } }

3.JDK动态代理

@SuppressWarnings("restriction")public class JavaProxyTest {    public static void main(String[] args) throws Exception {        JavaProxyInterface javaProxyInterface = new ConcreteClass();                JavaProxyInterface newJavaProxyInterface = (JavaProxyInterface) Proxy.newProxyInstance(                JavaProxyTest.class.getClassLoader(), new Class[] { JavaProxyInterface.class },                new MyInvocationHandler(javaProxyInterface));        //这里可以看到这个类以及被代理,在执行方法前会执行aopMethod()。这里需要注意的是oneDay()方法和oneDayFinal()的区别。oneDayFinal的方法aopMethod执行1次,oneDay的aopMethod执行1次        newJavaProxyInterface.gotoSchool();        newJavaProxyInterface.gotoWork();        newJavaProxyInterface.oneDayFinal();        newJavaProxyInterface.oneDay();    }}/*** InvocationHandler 的一个实现,实际上处理代理的逻辑在这里*/class MyInvocationHandler implements InvocationHandler {    JavaProxyInterface javaProxy;    public MyInvocationHandler(JavaProxyInterface javaProxy) {        this.javaProxy = javaProxy;    }    private void aopMethod() {        System.out.println("before method");    }  //继承方法,代理时实际执行的犯法,如果要实现原方法,则需要调用method.invoke(javaProxy, args),这里还调用了一个aopMethod(),可以类比于Spring中的切面before注解。    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        aopMethod();        return method.invoke(javaProxy, args);    }}/*** 需要一个最顶层接口,必须*/interface JavaProxyInterface {    void gotoSchool();    void gotoWork();    void oneDay();    void oneDayFinal();}/*** 需要被代理的类,实现了顶层接口,非必须*/class ConcreteClass implements JavaProxyInterface {    @Override    public void gotoSchool() {        System.out.println("gotoSchool");    }    @Override    public void gotoWork() {        System.out.println("gotoWork");    }    @Override    public void oneDay() {        gotoSchool();        gotoWork();    }    @Override    public final void oneDayFinal() {        gotoSchool();        gotoWork();    }}

底层实现部分代码:

// proxyName 为类名,interfaces为顶层接口Classbyte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); File file = new File("D:/testProxy/Ddd.class");FileOutputStream fileOutputStream = new FileOutputStream(file);fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();

CGlib动态代理

public class CglibProxyTest {    public static void main(String[] args) throws Exception {        CglibTestSon CglibTestSon = new CglibTestSon();        Enhancer enhancer = new Enhancer();        Callback s = new MthdInvoker(CglibTestSon);        enhancer.setSuperclass(CglibTestSon.class);        Callback callbacks[] = new Callback[] { s };        enhancer.setCallbacks(callbacks);        CglibTestSon CglibTestSon2 = (CglibTestSon) enhancer.create();        CglibTestSon2.gotoHome();        CglibTestSon2.gotoSchool();    //这里可以看到这个类以及被代理,在执行方法前会执行aopMethod()。这里需要注意的是oneDay()方法和onedayFinal()的区别。onedayFinal的方法aopMethod执行2次,oneDay的aopMethod执行1次 ,注意这里和jdk的代理的区别        CglibTestSon2.oneday();        CglibTestSon2.onedayFinal();    }}/*** 需要被代理的类,不需要实现顶层接口*/class CglibTestSon {    public CglibTestSon() {    }    public void gotoHome() {        System.out.println("============gotoHome============");    }    public void gotoSchool() {        System.out.println("===========gotoSchool============");    }    public void oneday() {        gotoHome();        gotoSchool();    }    public final void onedayFinal() {        gotoHome();        gotoSchool();    }}/*** 可以类比于jdk动态代理中的InvocationHandler ,实际上被代理后重要的类,实际上后续执行的就是intercept里的方法,如果需要执行原来的方法,则调用 method.invoke(s, args);这里也加了一个aopMethod();*/class MthdInvoker implements MethodInterceptor {    private CglibTestSon s;    public MthdInvoker(CglibTestSon s) {        this.s = s;    }    private void aopMethod() {        System.out.println("i am aopMethod");    }    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {        aopMethod();        Object a = method.invoke(s, args);        return a;    }}

CGlib底层实现部分代码:

byte[] bs = DefaultGeneratorStrategy.INSTANCE.generate(enhancer);FileOutputStream fileOutputStream = new FileOutputStream("D:/testProxy/Cc.class");fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();

简单来看就是先生成新的class文件,然后加载到jvm中,然后使用反射,先用class取得他的构造方法,然后使用构造方法反射得到他的一个实例。

标红的是最复杂的。然后cglib的实现原理基本一致,唯一的区别在于生成新的class文件方式和结果不一样。

4.静态代理模式的实现AOP

Font.java

package com.java.proxy; import lombok.Data; @Datapublic class Font {   private String name;}

FontProvider.java

package com.java.proxy;  public interface FontProvider {    Font getFont(String name);    void printName(String name);}

代理类CachedFontProvider.java

/** * 给FontProvider的getFont添加缓存功能,用静态代理来实现 * */public class CachedFontProvider implements FontProvider {   private FontProvider fontProvider;    private Map cached = new HashMap();      public CachedFontProvider() {    fontProvider = new FontProviderFromDisk();  }     @Override  public Font getFont(String name) {    System.out.println("静态代理getFont()");    Font font = cached.get(name);    if(font == null) {      font = fontProvider.getFont(name);      cached.put(name, font);    }    return font;  }     @Override  public void printName(String name) {    System.out.println("静态代理printName()");    fontProvider.printName(name);;      }   }

工厂类ProviderFactory.java

/** * 每个字体都增加了缓存功能,其实工厂就是用的缓存字体提供器,跟io一样 * 使用代理(静态),已经避免了再去修改每个字体提供器(这违反了开闭原则,而且工作量很大,容易出错;而且如果要增加别的功能 * 比如日志打印,权限检查,异常处理,每个都要去修改,代码重复,而且很麻烦) *  * ② 然而为什么要用动态代理? *考虑以下各种情况,有多个提供类,每个类都有getXxx(String name)方法, *每个类都要加入缓存功能,使用静态代理虽然也能实现,但是也是略显繁琐,需要手动一一创建代理类。 */public class ProviderFactory {   public static FontProvider getFontProvider() {    return new CachedFontProvider();  }  }

测试类;

public class Business {  public static void main(String[] args) {     FontProvider fontProvider = ProviderFactory.getFontProvider();          Font font = fontProvider.getFont("微软雅黑");          System.out.println(font);          fontProvider.printName("代理模式实现AOP");  } }

总结

三者功能类似,但各有优势,从过滤器--》拦截器--》切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。

点个“在看”表示朕

已阅

java 切面_实用|AOP切面编程手段大汇总相关推荐

  1. vue面向切面_感受面向切面编程

    什么是面向切面 初听面向切面编程时, 一头雾水, 什么是面向切面, 只听说过面向对象(OOP), 面向过程(PO), 函数式编程(FP), 面向切面 ? 面向的难道是某一个面? 面向搜索引擎后才了解到 ...

  2. 实用java教程_实用Java教程 目录

    目录 第一部分 面向对象基础 第1章 对象和类 ------------1 1.1   对象和类 -------------1 1.2   创建对象 -------------1 1.3   调用方法 ...

  3. Java并发编程知识大汇总

    线程简介 什么是线程 现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建很多是线程,这些线程都有自己的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量. 之所以我们感觉不到 ...

  4. java 发送邮件_老板要实现SpringBoot发送邮件?大神发了这篇文章后,今晚准点下班...

    正文开始前,分享阿里 P8 资深架构师吐血总结的 <Java 核心知识体系&面试资料.pdf> 阿里 P8 级资深架构师吐血总结的一份 Java 核心知识.pdf, 内容覆盖很广, ...

  5. 记账App Java代码_基于android的记账APP大作业项目

    基于android的记账APP 一 项目介绍 由于日常生活中,有很多月光一族的人们,他们往往不知不觉,觉得钱用的差不多了.而且最后回想起来,还不知道钱用到哪些地方了.所以开发此软件的目的,就是为了解决 ...

  6. pointnet分割自己的点云数据_点云学习历史文章大汇总

    LaserNet:一种高效的自动驾驶概率三维目标探测器 从相机标定到SLAM,极简三维视觉六小时课程视频(附PPT) 计算机图形学遇上深度学习,为3D图像任务打造的深度学习利器TensorFlow G ...

  7. 先收藏!关于Java类、接口、枚举的知识点大汇总

    摘要:Java知识点精选之类.接口.枚举30问,算是比较基础的,希望大家一起学习进步. 整理了一些JAVA语言的在类.接口.枚举等方面的知识点以及大家常遇到的问题.希望能帮助到大家. Q: 各修饰符所 ...

  8. php大作业含代码_目标检测 | 目标检测技巧大汇总(含代码与解读)

    点击上方"AI算法修炼营",选择加星标或"置顶" 标题以下,全是干货 来自 | 知乎作者丨初识CV来源丨https://zhuanlan.zhihu.com/p ...

  9. 一键清除苹果锁屏密码_苹果手机恢复出厂设置问题大汇总

    最近,陆续有不少牛粉问小编关于苹果手机恢复出厂设置的问题.大家的问题都是很细节的问题,但小编问了问周围同事,这些大大小小的问题同事们还真的都遇到过.所以呢,小编今天就把大家关于苹果手机恢复出厂设置的问 ...

最新文章

  1. 轮询 长轮询 websocket
  2. linux sa 命令,Linux 常用命令全拼
  3. Qt paintevent事件
  4. 回调函数自定义传参_10分钟教你手写8个常用的自定义hooks
  5. 使用top命令监控linux系统cpu变化
  6. 树上倍增求LCA及例题
  7. Spring Validation校验
  8. 利用条件运算符的嵌套来完成此题:学习成绩 =90分的同学用A表示,60-89分之间的用B表示,60分以下的用C表示。...
  9. 这两年亚马逊创业都是一个非常火热的话题
  10. 【docker】kubernetes集群一键部署包
  11. phpcms v9 index.php,Phpcms V9后台登录地址修改方法
  12. libmesh 思维导图(类接口设计)
  13. Web API 2 对于 Content-Length 要求严格
  14. 双系统重装Ubuntu经验分享
  15. adb连接手机显示:List of devices attached
  16. c语言实现输入电压检测,STC12C2052AD单片机AD转换C语言程序的实现
  17. 计算机关机后耗电问题,笔记本关机后耗电严重?Win10笔记本关机还耗电解决办法...
  18. 在线客服机器人交互功能开发总结
  19. python课程回顾复习记录简要6
  20. 解决微信小程序银行卡号输入转换格式

热门文章

  1. Datagridview拖放数据到TreeView
  2. 创建索引的方法有两种
  3. 错误:readline/readline.h:没有那个文件或目录解决方法
  4. MongoDB中关于64位整型存储解决方案
  5. solaris下修改 IPMP 配置
  6. (转载)简洁、明晰!数据库设计三大范式应用实例剖析
  7. synchronized 方法 导致插入数据插不进_synchronized 原理知多少
  8. python docker自动化_「docker实战篇」python的docker爬虫技术-移动自动化控制工具appium工具(17)...
  9. html上传文件_.NET基于WebUploader大文件分片上传、断网续传、秒传
  10. python 添加图例_Python | 在图例标签中添加Sigma