菜鸟的AOP理解贴

SpringBoot的AOP相关使用

AOP相关概念

首先声明本人也只是个菜鸟,此贴既是为了整理一下本人的思路,也希望能够帮助到其它人。
AOP的全称为Aspect Oriented Programming,翻译过来就是:面向切面编程。作为Java开发人员,AOP是一个十分重要的概念,它能够通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。我的理解为:可以将许多适普的功能与操作使用AOP与项目代码解耦,应用场景包括Log打印,权限校验,登录验证等。下面将在学习完AOP的相关知识后实现AOP的日志管理。

一.Aspect注解

Aspect注解表明它是一个切面类,在 Aspect 中会往往会包含着一些 Pointcut 以及相应的 Advice。
那么Aspect(切面)到底是个什么东西呢,我们先看下图?

Aspect

如上图所见,当我们有多个请求到Web服务器,我们对于用户注册、用户登录、以及其他的所有操作,有可能涉及到日志输出,请求合法性校验,缓存等需求,这些需求实际上与我们的业务功能无关,但又是我们出于维护、监控等原因必须要有的东西,那我们改如何去实现它?
1.我们可以创建一个类,将需要扩展的操作或者功能在该类中实现。由我们的业务类去继承该类,也可以达到这样的效果,下面使用伪代码表示:
基础类:

@Controller
public Class BaseCotroller(){
	//这里写我们`在这里插入代码片需要扩展的业务,登录验证,请求合法性验证等。
}

我们实际使用的业务接口

@Controller
public class UserController extends BaseController(){
	UserController(){
		super.init();//执行默认的权限校验代码
	}
}

2.定义一个切面,可以很好的解决这个问题。我们下面会讲到,代码先不贴。

对比以上两种方式,我们会发现当我们的实际业务需要集成其他类时,会产生问题,因为Java只能单继承。当然,这种方式也意味着所有的controller都将在构造函数中调用父类的扩展操作。
那么我们使用Aspect切面将会如何实现呢?下面还是以权限为例:
1.创建一个Annonation类。
创建一个切面
2.代码如下:

package com.pay.unionpay.anno;
import java.lang.annotation.*;
//该注解被使用在方法上
@Target(value = {ElementType.METHOD})
//将该类编译到JVM中,可以在程序运行中获取。
@Retention(value = RetentionPolicy.RUNTIME)
//api
@Documented
public @interface MyTestAnno  {
    public String value() default "/index";
}

下面我们定义一个切面,实现该需要的操作。
1.在pom.xml文件中添加aop依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.创建MyAspect类
切面类
3.加入一下代码:

//加入Component和Aspect注解表示这是一个切面
@Component
@Aspect
public class MyAnnAspect {
//定义一个切入点,路径为我们定义的注解的路径
   @Pointcut("@annotation(com.test.A.anno.MyTestAnno)")
    public void  myTestAnno(){}
	//环绕通知
    @Around("myTestAnno()")
    public void sayHe(ProceedingJoinPoint proceedingJoinPoint){
   	//扩展代码
   	......
    
    }
}

为了简化代码,这里只定义一个around,其他后续会加入。
4.在需要加入接口中添加注释,如下:

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    @ResponseBody
    @MyTestAnno(value = "test")
    public String sayHell(){
        return "Hello USER";
    }

这样我们就完成了一个简单的Aspect切面。这样的方式显然更加简单而优雅。

在小试牛刀之后,我们接下来详细了解一下Aspect

AOP详解

aspect:表示切面,上面已经提到过
JointPoint:连接点,表示在程序中明确定义的点,程序中的所有对类的访问,方法的调用,异常处理的代码块,都是连接点。同时JointPoint还可以嵌套其它的连接点。
PointCut:切入点,表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方
Advice(增强):Advice 定义了在 Pointcut 里面定义的具体要做的事,它包括 before、after 和 around ,以区别是在每个 joint point 之前、之后还是代替执行的代码。

是不是觉得以上概念特别迷糊?AOP概念实在太多,有迷糊的点也正常,以下我将根据我的理解,尽可能地将这个概念描述得好理解一些。
还记得下面这张图吗?
AOP面向切面编程
关于切面已经解释过了,此处不再赘述,下面就梳理一下JointPoint、Pointcut、Advice的概念。

JointPoint:连接点,概念上说,所有的对方法的调用,类成员的调用,以及异常处理的“代码块”都是连接点。那所谓的连接点怎么解释呢?由上图我们都知道,用户注册和用户登录,都需要一个切面去做某些与业务无关的事。那么用户注册和用户登陆这两个接口和切面连接的地方,我们叫它连接点。那么我们切面需要做的事,比如日志管理,权限验证,都需要在调用接口前处理好,那么想简单一些,这个接口,就是我们的连接点。

Pointcut:切入点
切入点就是一组连接点,我们的接口中会有各种逻辑判断,处理代码,参数等等,那么我们需要从中匹配出需要进行Advice的内容。

Advice:
增强,就是对这些匹配出来的切入点,进行处理操作,包括在Joint Point前还是在JointPoint前。

举个例子:
假设你开了一家超市,每天都会有很多人来购物,那么,你和买家的连接点就是“购物”这一个行为,所有的买家进进入超市,目的就是购物。(严肃,不要杠,假装所有人去超市都是为了购物)。连接点有了,那么是不是所有的买家进入超市都“会”买呢?(可能某个富婆觉得你卖得太便宜,也可能你开的黑心超市质量不过关,总之他不想买了),那么“付钱”这一操作就不会在这些商户身上发生。那个这个切入点就是,购物车。 只有购物车里面有商品,才需要结账,才需要付钱,没有商品就不需要结账了。我们继续,购物车已经有商品了,要付钱了,这个行为就被称作“Advice(增强)”,最终的目的就是为了付钱。付钱前要将商品价格计算出来,付钱后帮你把商品放到购物袋中。到此,就完成了我们的购物体验。

当然,以上例子可能不是特别恰当,重点是需要理解这些概念。

上面我们了解了Aspect相关的一些概念,下面将集中说一下Advice。
这里主要说明Advice的种类。
around :在Joint Point执行前或执行后都执行的advice。我们也叫它环绕通知
before: 在 join point 前被执行的 advice,但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
after return:当正常返回以后的操作
after throwing:当出现异常以后执行的内容
after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice

在了解了相关概念以后,我们来实现一个ASPECT实现日志管理的小例子。

实现访问日志统一管理

1.创建一个注解类,命名为:LogAnno
2.代码如下:

package com.pay.unionpay.anno;

import java.lang.annotation.*;

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnno {
    String value() default "Log";
}

3.创建一个Asspect,命名为:LogAspect.java

package com.pay.unionpay.listener;

import com.alibaba.fastjson.JSON;
import com.pay.unionpay.anno.LogAnno;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;

@Aspect
@Component
public class LogAspcet {
    private final static Logger logger = LoggerFactory.getLogger(LogAspcet.class);
    //定义切入点为扫描到LogAnno注解为连接点
    @Pointcut("@annotation(com.pay.unionpay.anno.LogAnno)")
    public void pointcut(){}
    //定义切入点
    @Around("pointcut()")
    public Object printLog(ProceedingJoinPoint point) throws Throwable {		
    	//统一日志管理完毕以后继续请求,不影响用户请求
        Object obj  =  point.proceed();
        logger.info("=================统一日志管理记录=======================");
        //获取请求对象,可以拿到请求信息
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //IP地址
        String ip = getRemoteHost(request);
        //端口
        int port = request.getRemotePort();
        //请求Url
        String url = request.getRequestURI();
        //请求参数
        String reqParam = Arrays.toString(point.getArgs());
        //请求类
        String className = point.getTarget().getClass().getName();
        //请求类路径
        String strPackage = point.getTarget().getClass().getPackage().toString();
        logger.info("#请求源IP:{},端口:{}",ip,port);
        logger.info("#请求路径:{}",url);
        logger.info("#执行包名:{},请求类{}",strPackage,className);
        logger.info("#请求参数:{}",reqParam);
        logger.info("=======================================================");

        return obj;
    }

    private String getRemoteHost(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}

4.在需要使用的Controller上添加注解:

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    @ResponseBody
    @LogAnno
    public String sayHell(){
        return "Hello USER";
    }

5.访问:http://localhost:8090/user/hello
6.访问效果
访问效果
7.查看控制台效果:
控制台效果

以上案例就完成啦!总结到此结束。这也只是我的个人总结,以及目前的理解,如有错误,还请提出来。第一次发文章,不喜勿喷。蟹蟹

本文章概念相关主要参考地址,这里有更加详细的描述:https://blog.csdn.net/q982151756/article/details/80513340

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值