关于Spring AOP 你需要知道的几个地方

了解AOP

在了解aop之前,先了解下OOP和POP,OOP即面向对象的程序设计,POP即面向过程程序设计,POP它是以功能为中心来进行思考和组织的一种编程方式,强调的是系统的数据被加工和处理的过程,实际上就是注重功能性的实现,效果达到就好了,而OOP则注重封装,强调整体性的概念,以对象为中心,将对象的内部组织与外部环境区分开来。
为了加深理解,我们把程序设计比喻成一个房子的布置,一间房子的布局中,需要各种功能的家具和洁具(类似方法),如马桶、浴缸、天然气灶,床、桌子等,对于(POP)面向过程的程序设计更注重的是功能的实现(即功能方法的实现),东西可以随意摆放,功能区互相也没有互相隔开,各种功能都已实现,房子也就可以正常居住了。但对于(OOP)面向对象的程序设计则是无法忍受的,这样的设置使房子内的各种家具和洁具间摆放散乱并且相互暴露的机率大大增加,各种气味相互参杂,显然是很不好的,于是为了更优雅地设置房屋的布局,我们加上具体功能区的房间,每个房间都有各自的名称和相应功能(类似class类),把对应功能的家具放到具体功能区的房间里,如卫生间是大小解和洗澡梳妆用的,卧室是休息用的,厨房则是做饭用的,每个房间都各司其职并且无需时刻向外界暴露内部的结构,整个房间结构清晰,外界只需要知道这个房间并使用房间内提供的各项功能即可(方法调用),同时也更有利于后期的拓展了,毕竟哪个房间需要添加那些功能,其范围也有了限制,也就使职责更加明确了(单一责任原则)
但随着软件规模的增大,应用的逐渐升级,慢慢地,OOP也开始暴露出一些问题,核心业务中总掺杂着一些不相关联的特殊业务,如日志记录,权限验证,事务控制,性能检测,错误信息检测等,这些特殊业务可以说和核心业务没有根本上的关联而且核心业务也不关心。
这些特殊业务会带来哪些问题呢?
1.代码混乱,大量的外围操作可能会混乱核心操作的代码,而且当外围模块有重大修改时也会影响到核心模块。
2.代码分散和冗余:同样的功能代码,在其他的模块几乎随处可见,导致代码分散并且冗余度高。
3.代码质量低扩展难:由于不太相关的业务代码混杂在一起,无法专注核心业务代码,当进行类似无关业务扩展时又会直接涉及到核心业务的代码,导致拓展性低。
解决:
假设现在我们把日志、权限、事务、性能监测等外围业务看作单独的关注点(也可以理解为单独的模块),每个关注点都可以在需要它们的时刻及时被运用而且无需提前整合到核心模块中。将每个关注点与核心业务模块分离,作为单独的功能,横切几个核心业务模块。这就叫AOP(面向切面编程)
在这里插入图片描述

1.自定义注解

import java.lang.annotation.*;
//表明该注解标记的元素可以被Javadoc 或类似的工具文档化
@Document
//注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
@Retention(RetentionPolicy.RUNTIME) 
//注解作用的位置,ElementType.METHOD表示该注解仅能作用于方法上
@Target(ElementType.METHOD) 
public @interface SysLog {
    String value() default "";
}

注解作用的位置
在这里插入图片描述
注解的生命周期
在这里插入图片描述

2.切面

port java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
@Aspect
public class SysLogAspect {
    

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    
  /**
   * 其中@Pointcut声明了切点(这里的切点是我们自定义的注解类),
   * @Before声明了通知内容,在具体的通知中,我们通过@annotation(logger)拿到了自定义的注解对象,
   * 所以就能够获取我们在使用注解时赋予的值了。
   */
	
    @Pointcut("@annotation(com.up.springboot.anno.SysLog)")
    private void pointcut() {}
    
    @Before("pointcut() && @annotation(logger)")
    public void around(JoinPoint joinPoint,SysLog logger) {
    	 //类名
    	 String className=joinPoint.getSignature().getDeclaringType().getSimpleName();
         //方法名
         String modName= joinPoint.getSignature().getName();
         //参数
         Object[] args = joinPoint.getArgs();
         StringBuffer result = new StringBuffer();
         result.append("["+className+"]");
         result.append("["+modName+"]");
         Arrays.stream(args).forEach(arg->{
             try {
             	result.append("["+OBJECT_MAPPER.writeValueAsString(arg)+"]");
             } catch (JsonProcessingException e) {
                 
             }
         });
          MethodSignature signature =(MethodSignature) joinPoint.getSignature();
    	Method method = signature.getMethod();
    	//注解上的描述
	    SysLog syslog = method.getAnnotation(SysLog.class)
	    result.append("["+syslog.value+"]");
	    //HttpServletRequest request =HttpContextUtils.getHttpServletRequest();
	    //获取Ip
	    //IPUtils.getIpAddr(request);
         long beginTime =System.currentTimeMillis();
         //执行方法
         Object result = point.proceed();
         //执行时间
         long time =System.currentTimeMillis()- beginTime
          result.append("["+time +"]");
         System.out.println(result.toString());
    }

3.使用自定义注解

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.up.springboot.anno.SysLog;
import com.up.springboot.common.ReturnItemUtils;
import com.up.springboot.common.pojo.ReturnItem;

@RestController
@RequestMapping("/sys/test")
public class TestController {
	 @SysLog()
	 @GetMapping("/test")
	 public ReturnItem select(@RequestParam(name = "token") String token,
	     	 @RequestParam(name = "p1",required = false) String p1,
	     	 @RequestParam(name = "p2",required = false) Integer p2) {
		
		try {
			
			System.out.println("======参数:"+p1);
		 }catch (Exception e) {
			 
		 }finally {
			 return ReturnItemUtils.newSuccessReturnItem();
		}
		
	 }
	
}

4.依赖包

             <dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-aop</artifactId>
		    <version>2.2.1.RELEASE</version>
	     </dependency>
	     <dependency>
	        <groupId>org.aspectj</groupId>
	        <artifactId>aspectjweaver</artifactId>
	     </dependency>

5.Pointcut切入点

任意公共方法的执行:
execution(public * *..))

任何一个名字以 set 开始的方法的执行:
execution(* set*..))

UserService 接口定义的任意方法的执行:
execution(* com.test.service.UserService .*(..))

在 service 包中定义的任意方法的执行:
execution(* com.test.service.*.*(..))

在 service 包或其子包中定义的任意方法的执行:
execution(* com.test.service..*.*(..))

在 service 包中的任意连接点(在 Spring AOP 中只是方法执行):
within(com.test.service.*)

在 service 包或其子包中的任意连接点(在 Spring AOP 中只是方法执行):
within(com.test.service..*)

实现 UserService 接口的代理对象的任意连接点 (在 Spring AOP 中只是方法执行):
this(com.test.service.UserService )

实现 UserService 接口的目标对象的任意连接点 (在 Spring AOP 中只是方法执行):
target(com.test.service.UserService )

任何一个只接受一个参数,并且动态运行时候传入的参数是 Serializable 接口的连接点(在 Spring AOP 中只是方法执行):
args(java.io.Serializable)



目标对象中有一个 @SysLog 自定义注解的任意连接点 (在 Spring AOP 中只是方法执行):
@target(org.springframework.transaction.annotation.SysLog)

任何一个目标对象声明的类型有一个 @SysLog注解的连接点 (在 Spring AOP 中只是方法执行):
@within(org.springframework.transaction.annotation.SysLog)

任何一个执行的方法有一个 @SysLog注解的连接点 (在 Spring AOP 中只是方法执行):
@annotation(org.springframework.transaction.annotation.SysLog)

任何一个只接受一个参数,并且运行时所传入的参数类型具有 @SysLog注解的连接点(在 Spring AOP 中只是方法执行):
@args(com.test.security.SysLog)

任何一个在名为 userService 的 Spring bean 之上的连接点 (在 Spring AOP 中只是方法执行):	
bean(userService)

任何一个在名字匹配通配符表达式*Service的 Spring bean 之上的连接点 (在 Spring AOP 中只是方法执行):
bean(*Service)

6.spring aop5种通知类型

1.spring aop通知(advice)分成五类:
前置通知[Before advice] @Before:在目标方法执行之前执行执行的通知
前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。

2.后置通知[After returning advice] @AfterReturning:在目标方法正常执行完成后执行,如果连接点抛出异常,则不会执行。
在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,还可以通过配置获取返回值,但是这个参数必须处在参数列表的第一个,否则抛异常,

3.异常通知[After throwing advice] @AfterThrowing:在目标方法抛出异常时执行的通知,可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位,否则会报错,另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象。

4.最终通知[After (finally) advice] @After:在目标方法执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。 和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法抛出异常,则后置通知不会执行。
而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。另外,后置通知可以通过配置得到返回值,而最终通知无法得到。最终通知也可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数,否则会报错。

5.环绕通知[Around advice] @Around:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作,在目标方法执行之前和之后都可以执行额外代码的通知。
在环绕通知中必须调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,调用一下 pjp.proceed();否则,Controller 中的接口将没有机会被执行,从而也导致了 @Before这个advice不会被触发,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。
要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。
环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力。

6.执行顺序
正常情况的执行顺序 aop–》@Around–》@Before–》Method–》@Around–》@After–》@AfterReturning
异常情况的执行顺序 aop–》@Around–》@Before–》Method–》@Around–》@After–》@AfterThrowing

7.两个切面的时候,可以为 apsect1 和 aspect2 分别添加 @Order 注解,里面加上序号,来控制切面的执行先后顺序

@Order(7)
@Component
@Aspect
public class Aspect1 {
  // ...
}
 
@Order(8)
@Component
@Aspect
public class Aspect2 {
  // ...
}

Aspect1 里执行到@Before之后,会到Aspect2 @Around里,执行完Aspect2里所有通知后,再回到Aspect1 的 @Around,执行Aspect1 后面的通知

8.常用场景
在这里插入图片描述

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值