SpringAOP在项目中的应用
SpringAOP项目应用
1、切入点表达式
首先是切入点表达式的书写方式,AspectJ定义了专门的表达式用于指定切入点。表达式原型是:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
modifiers-pattern] :访问权限类型
ret-type-pattern :返回值类型
declaring-type-pattern :包名类名
name-pattern(param-pattern) :方法名(参数类型和参数个数)
throws-pattern:抛出异常类型
?:表示可选的部分
以上信息描述的信息是:
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。
在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
表达式 | execution (* .add(int,..)) || execution( *.sub(int,..)) |
---|---|
含义 | 任意类中第一个参数为int类型的add方法或sub方法 |
通过@Pointcut注解配置切入点。该注解作用在方法上。我们在编写增强方法的时候,切入点的名字就是该方法的方法名。@Pointcut注解中的切入点表达式,就是我们要对哪些方法进行增强。
/**
* 配置切入点,编写切入点表达式. 我们要增强什么
*/
@Pointcut("execution(* com.xue.demo.service.impl.*.*(..))")
public void pointCut() {
}
/**
* 前置通知
*/
@Before(value = "pointCut()")
public void beforeRun() {
System.out.println("执行之前执行,前置通知");
}
这里我们对com.xue.demo.service.impl包下的所有类的所有的方法进行了增强。我们在调用com.xue.demo.service.impl包下的所有类的所有的方法前,会先执行我们的前置通知方法。
对切入点的配置,还有其他的方式。
如果我们要配置两个切入点,比如需要对两个包下的方法进行增强,我们就需要编写两个切入点表达式。因此,我们的切入点表达式支持使用&&、|| 和 !。
我们可以配置两个切入点,用两个不同的方法名。然后我们再配置一个切入点,该切入点的切入点表达式就是那两个切入点的方法名组合而成的。
@Pointcut("execution(* com.xue.demo.service.impl.*.ru*(..))")
public void pointCutR() {
}
@Pointcut("execution(* com.xue.demo.service.impl.*.ju*(..))")
public void pointCutJ() {
}
@Pointcut(value = "pointCutJ() || pointCutR()")
public void pointCut() {
}
这里配置了两个切入点,方法分别是pointCutR()和pointCutJ()。这两个方法分别对com.xue.demo.service.impl包下的所有类的ru开头的方法和com.xue.demo.service.impl包下的ju开头的方法进行了增强。我们可以再配置一个切入点,该切入点的表达式就是pointCutR()和pointCutJ()两个方法的方法名。使用 || 连接。表示 com.xue.demo.service.impl包下的所有类的ru开头的方法和ju开头的方法都会被增强。
切入点表达式:
切入点表达式主要为了帮助我们增强我们要增强的方法。根据切入点表达式,可以找到我们要增强的方法。个人理解,感觉就像是一个导航,通过切入点表达式,就可以知道哪些方法需要被增强。
切入点表达式由 访问修饰符 返回值 报名 类名 方法名 以及我们的方法的参数组成。其中访问修饰符是可以省略不写的。返回值我们可以根据我们要增强的方法进行选择。也可以使用通配符 * ,表示所有返回类型的方法全部都被增强。
切入点表达式中的包名,表示了指定包下的类需要被增强。包名也可以使用通配符 * 表示。
类名和方法名也都可以使用我们的通配符 * 表示。但是方法中的参数,我们使用的是(…)表示多个参数。还可以指定增强方法的参数类型。
在进行切入点配置的时候,支持多种配置方式。可以根据我们的需求,对指定的方法进行增强。
execution:用于匹配方法执行的连接点。
within:用于匹配指定类型内的方法执行。
this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配。
target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配。
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法。
@within:用于匹配所以持有指定注解类型内的方法。
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解。
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行。
@annotation:用于匹配当前执行方法持有指定注解的方法。
对应链接:https://blog.csdn.net/Mr_97xu/article/details/115795608
2、切入点表达式的多个示例
我自己写了一个具体的例子:
public interface UserService{
String annotate1( String name);
String annotate2( String name);
}
// 经过实验发现,将注解标注在接口、方法参数上是无效的。方法应该放置在具体的实例之上
@Service
public class UserServiceImpl implements UserService {
@Override
@LogCustom
public String annotate1( String name) {
System.out.println("方法在参数上");
return null;
}
@Override
@LogCustom
public String annotate2(String name) {
System.out.println("方法在方法上");
return null;
}
}
@Aspect
@Component("myAdvice2")
public class MyAdvice2 {
@Pointcut("@annotation(com.guang.springaop.annotate.LogCustom)")
public void pointCut() {
}
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
System.out.println("---------hello,before-----"+name);
}
}
切入点表达式示例:
execution(public void com.guang.dao.impl.UserDao.save())
execution(void com.guang.dao.impl.UserDao.*(..))
execution(* com.guang.dao.impl.*.*(..))
execution(* com.guang.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用
示例一:
package com.example.ba01;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
/**
* @Aspect:是aspectj框架中的注解。
* 作用:表示当前类是切面类。
* 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
* 使用位置:在类定义的上面
*/
@Aspect
@Component
public class MyAspect {
/**
* 定义方法,方法是实现切面功能的
* 方法的定义要求:
* 1.公共方法public
* 2.方法没有返回值
* 3.方法名称自定义
* 4.方法可以有参数,也可以没有参数
* 如果有参数,参数不是自定义的,有几个参数类型可以使用
*/
/**
* @Before:前置通知注解
* 属性:value,是切入点表达式,表示切面的功能执行的位置
* 位置:在方法的上面
* 特点:
* 1.在目标方法之前先执行
* 2.不会改变目标放方法的执行结果
* 3.不会影响目标方法的执行
*
*/
/*@Before(value = "execution(public void com.example.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore(){
//就是你切面要执行的功能代码
System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
}*/
/*@Before(value = "execution(void *..SomeServiceImpl.doSome(String, Integer))")
public void myBefore(){
//就是你切面要执行的功能代码
System.out.println("1前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
}*/
/*@Before(value = "execution(void *..SomeServiceImpl.doSome(..))")
public void myBefore(){
//就是你切面要执行的功能代码
System.out.println("2前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
}*/
/*@Before(value = "execution(void *..SomeServiceImpl.do*(..))")
public void myBefore(){
//就是你切面要执行的功能代码
System.out.println("3前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
}*/
@Before(value = "execution(* *..SomeServiceImpl.do*(..))")
public void myBefore(){
//就是你切面要执行的功能代码
System.out.println("4前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
}
}
对于上面的方法中,可以在方法参数上添加一个参数JoinPoint。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。
/**
* 指定通知方法中的参数:JoinPoint
* JoinPoint:业务方法,要加入切面功能的业务方法
* 作用:可以在通知方法中获取方法执行时的信息,例如方法名称,方法的实参
* 如果切面功能中需要用到方法的信息,就加入JoinPoint
* 这个JoinPoint参数的值是由框架赋予,必须是第一位置的参数
*/
@Before(value = "execution(* *..SomeServiceImpl.do*(..))")
public void myBefore(JoinPoint jp){
//获取方法的完整定义
System.out.println("方法的签名(定义)="+jp.getSignature());
System.out.println("方法的签名(定义)="+jp.getSignature().getName());
//获取方法的实参
Object args[] = jp.getArgs();
for(Object arg : args){
System.out.println("参数="+arg);
}
//就是你切面要执行的功能代码
System.out.println("4前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
}
对应的相应信息:
方法的签名(定义)=void com.guang.springbootone.controller.LoggerController.getuser(String,Integer)
方法的签名(定义)= getuser
后置通知
在目标方法执行之后执行,是目标方法正常执行之后进行的操作,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
package com.example.ba02;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
/**
* @Aspect:是aspectj框架中的注解。
* 作用:表示当前类是切面类。
* 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
* 使用位置:在类定义的上面
*/
@Aspect
@Component
public class MyAspect {
/**
* 后置通知定义方法,方法是实现切面功能的
* 方法的定义要求:
* 1.公共方法public
* 2.方法没有返回值
* 3.方法名称自定义
* 4.方法有参数,推荐是Object,参数名自定义
*/
/**
* @AfterReturning:后置通知
* 属性:1.value 切入点表达式
* 2.returning 自定义的变量,表示目标方法的返回值的
* 自定义变量名必须和通知方法的形参名一样。
* 位置:在方法定义的上面
* 特点:
* 1.在目标方法之后执行的
* 2.能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
* Object res = doOther();
* 3.可以修改这个返回值
*
* 后置通知的执行
* Object res = doOther();
* myAfterReturning(res);
*/
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
returning = "res")
public void myAfterReturning(Object res){
//Object res : 是目标方法执行后的返回值,根据返回值做切面的功能处理
System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:"+res);
}
}
在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。
package com.example.ba03;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import java.util.Date;
/**
* @Aspect:是aspectj框架中的注解。
* 作用:表示当前类是切面类。
* 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
* 使用位置:在类定义的上面
*/
@Aspect
public class MyAspect {
/**
* 环绕通知方法的定义格式
* 1.public
* 2.必须有一个返回值,推荐使用Object
* 3.方法名称自定义
* 4.方法有参数,固定的参数 ProceedingJoinPoint
*/
/**
* @Around 环绕通知
* 属性:value 切入点表达式
* 位置:在方法的定义上面
* 特点:
* 1.它是功能最强的通知
* 2.在目标方法的前和后都能增强功能
* 3.控制目标方法是否被调用执行
* 4.修改原来的目标方法的执行结果,影响最后的调用结果
*
* 环绕通知,等同于jdk动态代理的 InvocationHandler接口
*
* 参数:ProceedingJoinPoint 就等同于Method
* 作用:执行目标的方法
* 返回值:就是目标方法的执行结果,可以被修改
*
* 环绕通知:经常做事务,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务
*/
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjd) throws Throwable {
//实现环绕通知
Object result = null;
System.out.println("环绕通知:在目标方法之前,输出时间" +new Date());
//1.目标方法的调用
result = pjd.proceed(); //相当于method.invoke(); Object result = doFirst();
System.out.println("环绕通知:在目标方法之后,提交事务");
//2.在目标方法的前或者后加入功能
if(result != null){
result = "Hello AspectJ AOP";
}
//返回值目标方法的执行结果
return result;
}
}
还可以有参数的来进行执行:
@Around(value = "pointCut()")
public Object around(JoinPoint pjp) throws Throwable {
System.out.println("环绕:前置通知...");
Object[] args = pjp.getArgs();
System.out.println("获取得到参数值对象"+ Arrays.asList(args).toString()); // [hello, 22]
System.out.println("获取得到被代理的对象"+pjp.getTarget()); // com.guang.springaop.service.impl.UserServiceImpl@74455848
Signature signature = pjp.getSignature();
Class declaringType = signature.getDeclaringType();
System.out.println("默认的类是:"+declaringType); // interface com.guang.springaop.service.UserService
System.out.println("默认类型的名称"+signature.getDeclaringTypeName()); // com.guang.springaop.service.UserService
System.out.println("获取得到方法签名"+ pjp.getSignature()); // String com.guang.springaop.service.UserService.sayHello(String,String)
//切入点方法执行
ProceedingJoinPoint joinPoint = (ProceedingJoinPoint)pjp;
Object proceed = joinPoint.proceed(args);
System.out.println("环绕:后置通知...");
return proceed;
}
使用对象来进行操作:
@Around(value = "pointCut()")
public Object around(JoinPoint pjp) throws Throwable {
System.out.println("环绕:前置通知...");
Object[] args = pjp.getArgs();
System.out.println("获取得到参数值对象"+ Arrays.asList(args).toString()); // [hello, 22]
System.out.println("获取得到被代理的对象"+pjp.getTarget()); // com.guang.springaop.service.impl.UserServiceImpl@74455848
Signature signature = pjp.getSignature();
Class declaringType = signature.getDeclaringType();
System.out.println("默认的类是:"+declaringType); // interface com.guang.springaop.service.UserService
System.out.println("默认类型的名称"+signature.getDeclaringTypeName()); // com.guang.springaop.service.UserService
System.out.println("获取得到方法签名"+ pjp.getSignature()); // String com.guang.springaop.service.UserService.sayHello(String,String)
//切入点方法执行
ProceedingJoinPoint joinPoint = (ProceedingJoinPoint)pjp;
Object proceed = joinPoint.proceed(args);
System.out.println("环绕:后置通知...");
return proceed;
}
我想在所有的controller在接收到对应的参数的时候来打印请求进来的参数:
package com.guang.springbootone.aspect;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Aspect
@Component
public class AopLogAspect {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* execution 指定controller
* !execution 排查指定controller下的方法
@Pointcut("(execution(public * com.guang.springbootone.controller.*.*(..)) && !execution(public * com.guang.springbootone.controller.CdpUploadController.upload(..)))")
public void aopLogAspect() {
}
*/
/**
* 还可以引入其他类中的方法来进行执行
*/
@Pointcut("(execution(public * com.guang.springbootone.controller.Log*.*(..)))")
public void aopLogAspect() {
}
/**
* 这里我觉得使用环切是不合适的
*
* @param joinPoint
* @throws Throwable
*/
@Before(value = "aopLogAspect()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
logger.info("请求开始 : ====================================================================================");
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 记录下请求内容
logger.info("请求类型 :" + request.getMethod() + " " + "请求URL : " + request.getRequestURL());
logger.info("请求IP : " + request.getRemoteAddr());
logger.info("请求方法 : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
LocalVariableTableParameterNameDiscoverer realParamNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = realParamNameDiscoverer.getParameterNames(method);
//方法 1 请求的方法参数值 JSON 格式 null不显示
Object[] args = joinPoint.getArgs();
if (args.length > 0 && joinPoint.getArgs().length > 0) {
for (int i = 0; i < args.length; i++) {
//请求参数类型判断过滤,防止JSON转换报错
if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse || args[i] instanceof MultipartFile) {
continue;
}
logger.info("请求参数名称 :" + paramNames[i] + ", 内容 :" + JSON.toJSONString(args[i]));
}
}
}
/**
* 如果是流数据,不应该将其打印出来!做个判断
*
* @param ret
* @throws Throwable
*/
@AfterReturning(returning = "ret", pointcut = "aopLogAspect()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("返回内容 : " + JSON.toJSONString(ret));
logger.info("请求结束 :====================================================================================");
}
}
3、在SpringBoot中使用纯注解开发
导入aop坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写配置类,开启自动AOP自动代理和包扫描(@SpringBootApplication中已经指定),下面说的是在Spring项目中来进行开发的。
@Configuration //标记当前类是:配置类
@ComponentScan(basePackage="com.itheima") //配置注解扫描
@EnableAspectJAutoProxy //开启AOP自动代理
public class AppConfig{
}