springboot中spring aop面向切面编程

1.aop全称Aspect Oriented Programming 面向切面编程

2.aop应用场景

场景一: 记录日志
场景二: 监控方法运行时间 (监控性能)
场景三: 权限控制
场景四: 缓存优化 (第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )
场景五: 事务管理 (调用方法前开启事务, 调用方法后提交关闭事务 )
3.aop一些名词

  • 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。

  • 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。

  • 通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。

  • 切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

  • 引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。

  • 目标对象(Target Object):被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。

  • AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

       其中重要的名词有:切面,切入点

4.示例代码(只做参考)

监控日志 和 监控方法运行时间 (监控性能) 权限控制 

/**
 * 只用作数据权限标记
 * @Author mufeng
 * @Date 2020-3-4 16:47
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataAuthority {
}

  

/**
* @Author mufeng
* @Date 2019-8-23 22:11
*/
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
private static final ThreadLocal<Long> timeLocal=new ThreadLocal<>();
public LogAspect() {
}

@Pointcut("@annotation(io.swagger.annotations.ApiOperation)")
public void log() {
}
@Before("log()")
public void before(JoinPoint joinPoint) throws Exception {
logger.info("aop的before()方法");
timeLocal.set(System.currentTimeMillis());
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String bizSeqNo = "112";
String traceId = bizSeqNo;
String newSysSeqNo = String.valueOf(12);
String deviceFingerPrint = "";
String unionId = "";
String openId = "";
Map<String, String> map = new HashMap();
map.put("SERVICEID", request.getRequestURI());
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
String methodName = method.getName();
JSONObject retJson = new JSONObject();
for(int i=0;i<joinPoint.getArgs().length;i++){
Object obj = joinPoint.getArgs()[i];
String s = methodSignature.getParameterNames()[i];
logger.info("s{}:{}",i,s);
retJson.put(s,obj);
}
logger.info("所有的请求参数 bean={}", retJson.toJSONString());

map.put("BIZSEQNO", bizSeqNo);
map.put("SYSSEQNO", newSysSeqNo);
map.put("DEVICEFINGERPRINT", deviceFingerPrint);
map.put("UNIONID", unionId);
map.put("OPENID", openId);
logger.info("AOP begin ,请求开始方法 :{}", method.getDeclaringClass() + "." + methodName + "()");
logger.info("请求url = {}", request.getRequestURL().toString());
logger.info("请求方法requestMethod = {}", request.getMethod());
logger.info("请求资源uri = {}", request.getRequestURI());
logger.info("流水号对应关系:traceId=" + traceId + "," + "spanId=" + newSysSeqNo + "," + "bizSeqNo=" + bizSeqNo + "," + "sysSeqNo=" + newSysSeqNo);
}
@After("log()")
public void after() {
logger.info("aop的after()方法");
}
@AfterReturning(
returning = "result",
pointcut = "log()"
)
@Around("log()")
private Object handle(ProceedingJoinPoint joinPoint) throws Exception{
/* 获取传参信息 */
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();

/* 获取注解信息 */
DataAuthority dataAuthority = method.getAnnotation(DataAuthority.class);
if(null != dataAuthority){
boolean flag=false;
for(int i=0;i<joinPoint.getArgs().length;i++){
Object obj = joinPoint.getArgs()[i];
String s = signature.getParameterNames()[i];
logger.info("s{}:{}",i,s);
if("userNo".equals(s)&&(int)obj<100){
flag=true;
break;
}
}
if(!flag){
throw new Exception("你没有权限!");
}
}

Object object;
try {
object=joinPoint.proceed();
} catch (Throwable throwable) {
logger.error(throwable.getMessage(),throwable);
throw new Exception(throwable.getMessage());
}
return object;
}
}

5.使用的注解:

   @Aspect:描述一个切面类,定义切面类的时候需要打上这个注解

   @Configuration:spring-boot配置类

   1. @Pointcut:声明一个切入点,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。Spring AOP只支持Spring bean的方法执行连接点。所以你可以把切入点看做是Spring bean上方法执行的匹配。一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。

  注:作为切入点签名的方法必须返回void 类型

  Spring AOP支持在切入点表达式中使用如下的切入点指示符:    

    • execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。

    • within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。

    • this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。

    • target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。

    • args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。

    • @target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。

    • @args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。

    • @within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。

    • @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。

         其中execution使用最频繁,即某方法执行时进行切入。定义切入点中有一个重要的知识,即切入点表达式,我们一会在解释怎么写切入点表达式。

   切入点意思就是在什么时候切入什么方法,定义一个切入点就相当于定义了一个“变量”,具体什么时间使用这个变量就需要一个通知。

   即将切面与目标对象连接起来。

  如例子中所示,通知均可以通过注解进行定义,注解中的参数为切入点。

  spring aop支持的通知:

  @Before:前置通知:在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。

  @AfterReturning :后置通知:在某连接点正常完成后执行的通知,通常在一个匹配的方法返回的时候执行。

      可以在后置通知中绑定返回值,如:        

复制代码
@AfterReturning(
    pointcut=com.test.service.CacheDemoService.findById(..))",
    returning="retVal")
  public void doFindByIdCheck(Object retVal) {
    // ...
  }
复制代码

   @AfterThrowing:异常通知:在方法抛出异常退出时执行的通知。       

   @After 最终通知。当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

   @Around:环绕通知:包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

      环绕通知最麻烦,也最强大,其是一个对方法的环绕,具体方法会通过代理传递到切面中去,切面中可选择执行方法与否,执行方法几次等。

      环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型,在通知体内,调用ProceedingJoinPointproceed()方法会导致后台的连接点方法执行。proceed 方法也可能会被调用并且传入一个Object[]对象-该数组中的值将被作为方法执行时的参数。

6.通知参数

  任何通知方法可以将第一个参数定义为org.aspectj.lang.JoinPoint类型(环绕通知需要定义第一个参数为ProceedingJoinPoint类型,它是 JoinPoint 的一个子类)。JoinPoint接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)

  有时候我们定义切面的时候,切面中需要使用到目标对象的某个参数,如何使切面能得到目标对象的参数。

  从例子中可以看出一个方法:

  使用args来绑定。如果在一个args表达式中应该使用类型名字的地方 使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。

  

 @Before("execution(* findById*(..)) &&" + "args(id,..)")
    public void twiceAsOld1(Long id){
        System.err.println ("切面before执行了。。。。id==" + id);

    }

    

复制代码
@Around("execution(List<Account> find*(..)) &&" +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")        
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern)
throws Throwable {
  String newPattern = preProcess(accountHolderNamePattern);
  return pjp.proceed(new Object[] {newPattern});
}        
复制代码

9.切入点表达式

  现在我们介绍一下最重要的切入点表达式:

  如上文所说,定义切入点时需要一个包含名字和任意参数的签名,还有一个切入点表达式,就是* findById*(..)这一部分。

  切入点表达式的格式:execution([可见性] 返回类型 [声明类型].方法名(参数) [异常])

  其中【】中的为可选,其他的还支持通配符的使用:

    *:匹配所有字符
      ..:一般用于匹配多个包,多个参数
      +:表示类及其子类

  运算符有:&&、||、!

  

切入点表达式关键词:   

    1)execution:用于匹配子表达式。

            //匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
            @Pointcut("execution(* com.cjm.model..*.*(..))")
            public void before(){}

      2)within:用于匹配连接点所在的Java类或者包。

            //匹配Person类中的所有方法
            @Pointcut("within(com.cjm.model.Person)")
            public void before(){}

            //匹配com.cjm包及其子包中所有类中的所有方法

            @Pointcut("within(com.cjm..*)")
            public void before(){}

     3) this:用于向通知方法中传入代理对象的引用。
            @Before("before() && this(proxy)")
            public void beforeAdvide(JoinPoint point, Object proxy){
                  //处理逻辑
            }

      4)target:用于向通知方法中传入目标对象的引用。
            @Before("before() && target(target)
            public void beforeAdvide(JoinPoint point, Object proxy){
                  //处理逻辑
            }

      5)args:用于将参数传入到通知方法中。
            @Before("before() && args(age,username)")
            public void beforeAdvide(JoinPoint point, int age, String username){
                  //处理逻辑
            }
 
      6)@within :用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配。 

            @Pointcut("@within(com.cjm.annotation.AdviceAnnotation)") - 所有被@AdviceAnnotation标注的类都将匹配
            public void before(){}

  

      7)@target :和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。
            @Pointcut("@target(com.cjm.annotation.AdviceAnnotation)")
            public void before(){}

      8)@args :传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。
            @Before("@args(com.cjm.annotation.AdviceAnnotation)")
            public void beforeAdvide(JoinPoint point){
                  //处理逻辑
            }

  

      9)@annotation :匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。
            @Pointcut("@annotation(com.cjm.annotation.AdviceAnnotation)")
            public void before(){}

      10)bean:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
            @Pointcut("bean(person)")
            public void before(){}

参考资料:https://www.cnblogs.com/lic309/p/4079194.html

原文地址:https://www.cnblogs.com/mufeng07/p/12400633.html