aop详解与实战

1. 什么是AOP

aop:面向切面编程。采用横向机制。
oop:面向对象编程。采用纵向机制。

AOP,面向切面编程。就是通过某个切入点(比如方法开始、结束)向某个切面(被切的对象)切入环绕通知(需要切入的逻辑代码)。
比如一个类中的所有方法执行前都需要打印日志,那么可以通过AOP的方式来统一实现,而不需要在每个方法中都加入打印日志的代码逻辑。

2. AOP的常用使用场景

  • 日志记录
  • 权限控制
  • 事物管理
  • 缓存处理
    ...

3. AOP的实现方式

  • Spring AOP
        a) JDK 动态代理
        b) Cglib 动态代理
  • AspectJ

4. AspectJ是什么

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
Spring2.0以后新增了对AspectJ的全面支持。
常用注解:

@Before             前置通知                                          使用时需要指定一个value属性值,用于指定一个切入点表达式。【可以是现写的表达式,也阔以是已有的】。
@AfterReturning     后置通知                                          使用时需要指定pointcut/value属性值,用于指定切入点表达式。还能使用returning属性指定一个形参,该形参可用于访问目标方法的返回值。
@Around             环绕通知                                          使用时需要指定一个value属性值,用于指定一个切入点表达式。
@AfterThrowing      异常通知                                          使用时需要指定pointcut/value属性值,用于指定切入点表达式。还能使用throwing属性指定一个形参,该形参可用于访问目标方法抛出的异常。
@After              最终final通知,不管是否异常,该通知都会执行            使用时需要指定一个value属性值,用于指定一个切入点表达式。
@DeclareParents     引介通知
@Aspect             用在类上,表示当前类为一个切面类

在通知中可以使用了 JoinPoint 接口及其子接口 ProceedingJoinPoint 作为参数来获得目标对象的————类名,方法名,方法参数等。
★环绕通知必须接受一个类型为 ProceedingJoinPoint 的参数, 返回值也必须是 Object 类型,且需要抛出异常。返回值即为目标方法的返回值★
★异常通知可以传入Throwable类型的参数 来接收异常信息★

切入点表达式:
格式:execution( * com.example.aop.*.*(..))
    execution:就是表达式的主体。
    第一个*:返回类型,可以用代表所有类型
    com.example.aop:表示需要拦截的路径包名
    第二个*:类名,可以用
代表所有类
    第三个*:方法名,可以用*表示所有方法
    (..):..表示任意参数

还支持通配符的使用:

1) *:匹配所有字符
2) ..:一般用于匹配多个包,多个参数
3) +:表示类及其子类
4)运算符有:&&,||,!  AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式。

除了上述表达式写法,aop还支持如下的几种写法:
参考
@Around("within(类型表达式)")

within(cn.javass..*)                        cn.javass包及子包下的任何方法执行

within(cn.javass..IPointcutService+)        cn.javass包或所有子包下IPointcutService类型及子类型的任何方法

within(@cn.javass..Secure *)                持有cn.javass..Secure注解的任何类型的任何方法,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用

@Around("@within(注解类型))

@within cn.javass.spring.chapter6.Secure)   任何目标对象对应的类型持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用

@Around("target(类型全限定名)")

target(cn.javass.spring.chapter6.service.IPointcutService)      当前目标对象(非AOP对象)实现了 IPointcutService接口的任何方法
target(cn.javass.spring.chapter6.service.IIntroductionService)  当前目标对象(非AOP对象) 实现了IIntroductionService 接口的任何方法不可能是引入接口

@Around("@target(注解类型)")

@target (cn.javass.spring.chapter6.Secure)    任何目标对象持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用

@Around("args(参数类型列表)")

args (java.io.Serializable,..)         任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数的方法执行,args指定的参数类型是在运行时动态匹配的

@Around("@args(注解列表)")

@args (cn.javass.spring.chapter6.Secure)   任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解 cn.javass.spring.chapter6.Secure;动态切入点,类似于arg指示符;

@Around("@annotation(注解类型)")

@annotation(cn.javass.spring.chapter6.Secure )    当前执行方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配

@Around("this(类型全限定名)")

this(cn.javass.spring.chapter6.service.IPointcutService)         当前AOP对象实现了 IPointcutService接口的任何方法
this(cn.javass.spring.chapter6.service.IIntroductionService)     当前AOP对象实现了 IIntroductionService接口的任何方法也可能是引入接口

关于JoinPoint对象介绍:
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。

补充aop相关概念
    连接点(Join point)
        连接点是在应用执行过程中能够插入切面的一个点。
    切点(Pointcut)
        一个切面并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点范围。如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。
        因此,切点其实就是定义了需要执行在哪些连接点上执行通知。
    切面(Aspect)
        切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和在何处完成其功能。
    织入(Weaving)
        织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。

public interface JoinPoint {  
   String toString();          //连接点所在位置的相关信息  
   String toShortString();     //连接点所在位置的简短相关信息  
   String toLongString();      //连接点所在位置的全部相关信息  
   Object getThis();           //返回AOP代理对象  
   Object getTarget();         //返回目标对象  
   Object[] getArgs();         //返回被通知方法参数列表  
   Signature getSignature();   //返回当前连接点签名  
   SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置  
   String getKind();           //连接点类型  
   StaticPart getStaticPart(); //返回连接点静态部分  
  }  

关于ProceedingJoinPoint对象介绍:
ProceedingJoinPoint对象是JoinPoint的子接口。

主要多了如下两个方法:
Object proceed() throws Throwable              //执行目标方法 
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法 

关于Signature对象介绍:
Signature对象:是一个接口。用于获取或记录有关连接点的反射信息。他的子接口还提供了很多额外的实用方法。
常用子接口如下:

UnlockSignature       实现类     UnlockSignatureImpl
LockSignature         实现类     LockSignatureImpl
CatchClauseSignature  实现类     CatchClauseSignatureImpl
MemberSignature       
      子接口     FieldSignature                     实现类   FieldSignatureImpl
      子接口     CodeSignature[它还有很多子接口]      实现类   CodeSignatureImpl

常用方法如下:

String toString();         
String toShortString();        //返回此签名的字符串缩写形式
String toLongString();         //返回此签名的字符串扩展形式
String getName();              //返回此签名的方法【即返回方法名】
int    getModifiers();         //以整数形式返回此签名方法的修饰符。 
Class  getDeclaringType();     //返回一个java.lang.Class对象,该对象表示声明此成员的类或接口。
String getDeclaringTypeName(); //返回声明类型的全限定名称

关于签名咋个理解看下面的代码

5. SpringBoot整合AspectJ实现日志记录

★也可以AspectJ+自定义注解来做,这里没有展示这种★

5.1 导入依赖

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

5.2 定义切面类

① 在类上使用 @Component 注解 把切面类加入到IOC容器中
② 在类上使用 @Aspect 注解 使之成为切面类

@Aspect
@Component
public class LogAspect {
	private Logger logger = LoggerFactory.getLogger(LogAspect.class);
	
	/**
	 * 定义切入点,切入点为com.jsy.community下的函数
	 */
	@Pointcut("execution( * com.lihao.community..*.*(..)) && !execution(* com.lihao.community.intercepter..*.*(..)) && !execution(* com.lihao.community.exception..*.*(..))")
	public void webLog() {
	}
	
	/**
	 * 前置通知:在连接点之前执行的通知
	 *
	 * @param joinPoint
	 * @throws Throwable
	 */
	@Before("webLog()")
	public void doBefore(JoinPoint joinPoint) throws Throwable {
		// 接收到请求,记录请求内容
		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		if (attributes != null) {
			HttpServletRequest request = attributes.getRequest();
			// 记录下请求内容    
			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ");
			Calendar ca = Calendar.getInstance();
			String time = df.format(ca.getTime());
			logger.info("");
			logger.info("访问时间 : " + time);
			logger.info("访问路径 : " + request.getRequestURL().toString());
			logger.info("请求方式 : " + request.getMethod());
			logger.info("访问方法 : " + joinPoint.getSignature().getName());    //签名?
			logger.info("访问IP : " + request.getRemoteAddr());
			logger.info("方法参数 : " + Arrays.toString(joinPoint.getArgs()));
		}
	}
	
	/**
	 * @return void
	 * @Author lihao
	 * @Description 后置通知
	 * @Date 2021/1/19 17:09
	 * @Param [ret]
	 **/
	@AfterReturning(returning = "ret", pointcut = "webLog()")
	public void doAfterReturning(Object ret) throws Throwable {
		// 处理完请求,返回内容
		logger.info("返回结果 : " + ret);
	}
	
	/**
	 * @return void
	 * @Author lihao
	 * @Description 异常通知
	 * @Date 2021/1/19 17:43
	 * @Param [joinPoint, ex]
	 **/
	@AfterThrowing(pointcut = "webLog()", throwing = "ex")
	public void afterThrowing(JoinPoint joinPoint, Exception ex) {
		String methodName = joinPoint.getSignature().getName();
		logger.info("异常信息 : " + methodName + "() 出现了异常——————" + ex.getMessage());
	}
}


6 SpringBoot整合AspectJ实现缓存[利用aop做延时双删]

实现原理:
热点数据(经常会被查询,但是不经常被修改或者删除的数据),首选是使用redis缓存,毕竟强大到冒泡的QPS和极强的稳定性不是所有类似工具都有的,而且相比于memcached还提供了丰富的数据类型可以使用,另外,内存中的数据也提供了AOF和RDB等持久化机制可以选择,要冷、热的还是忽冷忽热的都可选。

结合具体应用需要注意一下:很多人用spring的AOP来构建redis缓存的自动生产和清除,过程可能如下:

Select 数据库前查询redis,有的话使用redis数据,放弃select 数据库,没有的话,select 数据库,然后将数据插入redis

update或者delete数据库钱,查询redis是否存在该数据,存在的话先删除redis中数据,然后再update或者delete数据库中的数据

上面这种操作,如果并发量很小的情况下基本没问题,但是高并发的情况请注意下面场景:

为了update先删掉了redis中的该数据,这时候另一个线程执行查询,发现redis中没有,瞬间执行了查询SQL,并且插入到redis中一条数据,回到刚才那个update语句,这个悲催的线程压根不知道刚才那个该死的select线程犯了一个弥天大错!于是这个redis中的错误数据就永远的存在了下去,直到下一个update或者delete。

原文地址:https://www.cnblogs.com/itlihao/p/14298681.html