Aop面向切面编程
AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了将不同的关注点分离出来的效果 。
这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
使用Aop的好处
- 集中处理某一关注点/横切逻辑
- 可以很方便的添加/删除关注点
- 侵入性少,增强代码可读性及可维护性
Aop的使用场景
- 权限控制
- 事物控制
- 日志审查
- 性能监控
- 异常处理
Aop相关概念
- Aspect :切面,切入系统的一个切面。比如事务管理是一个切面,权限管理也是一个切面;
- Join point :连接点,程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅支持方法的连接点。
- Advice :通知,切面在某个连接点执行的操作(分为: Before advice , After returning advice , After throwing advice , After (finally) advice , Around advice );
- Pointcut :切入点,如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
使用Aop的注意事项
- 不要把重要的业务逻辑放到aop里面去处理,容易忽略
- 无法拦截static,final,private方法
- 无法拦截内部方法调用:如一个server的内部的普通方法调用一个有事物的方法,那么普通方法里面执行有事物的方法时,事物并不起作用。
- SpringAOP代理方式,默认是jdk动态代理,我们可以改为cglib代理。
切面表达式
通配符
1)* 匹配任意数量的字符
2)+ 匹配指定类及其子类
3)…(两个.)一般用于匹配任意数的子包或参数
designators(指示器)
匹配方法
execution()
//匹配任何公共方法 @Pointcut("execution(public * com.smxy.service.*.*(..))") //匹配com.smxy包及子包下Service类中无参方法 @Pointcut("execution(* com.smxy..*Service.*())") //匹配com.smxy包及子包下Service类中的任何只有一个参数的方法 @Pointcut("execution(* com.smxy..*Service.*(*))") //匹配com.smxy包及子包下任何类的任何方法 @Pointcut("execution(* com.smxy..*.*(..))") //匹配com.smxy包及子包下返回值为String的任何方法 @Pointcut("execution(String com.smxy..*.*(..))") //匹配异常 execution(public * com.smxy.service.*.*(..) throws java.lang.IllegalAccessException)
匹配注解
@target()
@args()
@within()
@annotation()
//匹配方法标注有AdminOnly的注解方法 @Pointcut("@annotation(com.smxy.security.AdminOnly)") public void annoDemo(){} //匹配标注有Beta的类底下的方法,要求的annotation的Repository级别为CALSS @Pointcut("@within(com.google.common.annotations.Beta)") public void annoDemo3(){} //匹配标注有Beta的类底下的方法,要求的annotation的Repository级别为RUNTIME @Pointcut("@target(org.springframework.stereotype.Repository)") public void annoDemo4(){}
//匹配传入的参数类标注有Repository注解的方法
@Pointcut("@args(org.springframework.stereotype.Repository)")
public void annoArgsDemo(){}
匹配包/类型
within()
//匹配ProdectService类里头所有的方法 @Pointcut("within(com.smxy.service.ProdectService)") public void matchType(){} //匹配com.smxy包及子包下所有类的方法 @Pointcut("within(com.smxy..*)") public void matchPackage(){}
匹配对象
this()
bean()
target()
//匹配Aop对象的目标对象为指定类型的方法,即DemoDao的aop代理对象的方法 @Pointcut("this(com.smxy.DemoDao)") public void thisDemo(){} //匹配实现IDao接口的目标对象(而不是aop代理后的对象)的方 @Pointcut("target(com.smxy.IDao)") public void targetDemo(){} //匹配所有以Servce结尾的bean里头的方法 @Pointcut("bean(*Service)") public void matchPackage(){}
匹配参数
args()
//匹配任何以find开头而且只有一个Long参数的方法 @Pointcut("execution(* *..find*(Long))") public void argsDemo1(){} //匹配任何以有一个Long参数的方法 @Pointcut("args(Long)") public void argsDemo1(){} //匹配任何以find开头而且第一个参数为Long的方法 @Pointcut("execution(* *..find*(Long,..))") public void argsDemo3(){}
5种advice注解
- @before,前置通知
- @After(finally),后置通知,方法执行完之后
- @AfterReturning,返回通知,成功执行之后
- @AfterThrowing,异常通知,抛出异常之后
- @Around,环绕通知
案例
创建一个springboot的工程,引入相关依赖,做一个拦截接口的demo
切面类
/** * @Description: TOTO * @author BushRo * @date 2019-08-01 * @version 1.0 * */ @Aspect @Component public class HttpRequestAspect { private static final Logger log = LoggerFactory.getLogger(HttpRequestAspect.class); public static long startTime; public static long endTime; /*@PointCut注解表示表示横切点,哪些方法需要被横切*/ /*切点表达式*/ @Pointcut("execution(public * com.smxy.aop.controller.*.*(..))") /*切点签名*/ public void print() { } /*@Before注解表示在具体的方法之前执行*/ @Before("print()") public void before(JoinPoint joinPoint) { log.info("前置切面before……"); startTime = System.currentTimeMillis(); ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); String requestURI = request.getRequestURI(); String remoteAddr = request.getRemoteAddr(); //这个方法取客户端ip"不够好" String requestMethod = request.getMethod(); String declaringTypeName = joinPoint.getSignature().getDeclaringTypeName(); String methodName = joinPoint.getSignature().getName(); //获取参数 Object[] args = joinPoint.getArgs(); System.out.println("有"+args.length+"个参数"); for(Object a:args){ System.out.println(a); } log.info("请求url=" + requestURI + ",客户端ip=" + remoteAddr + ",请求方式=" + requestMethod + ",请求的类名=" + declaringTypeName + ",方法名=" + methodName + ",入参=" + args); } /*@After注解表示在方法执行之后执行*/ @After("print()") public void after() { endTime = System.currentTimeMillis() - startTime; log.info("后置切面after……"); } /*@AfterReturning注解用于获取方法的返回值*/ @AfterReturning(pointcut = "print()", returning = "object") public void getAfterReturn(Object object) { log.info("本次接口耗时={}ms", endTime); log.info("afterReturning={}", object.toString()); } /*@PointCut注解表示表示横切点,哪些方法需要被横切*/ /*切点表达式*/ @Pointcut("execution(public * com.smxy.aop.controller.*.*(..))") public void afterPoincut() { } @Around("afterPoincut()") public String afterDemo(ProceedingJoinPoint joinPoint) { System.out.println("环绕通知----before"); String result=null; try { result = String.valueOf(joinPoint.proceed(joinPoint.getArgs())); System.out.println("环绕通知-----方法的返回值为 "+result); } catch (Throwable throwable) { throwable.printStackTrace(); } finally { System.out.println("环绕通知----after"); } return result; } }
controller被切的方法
@RestController public class Test { @RequestMapping("/index/{id}") public String text(@PathVariable(value = "id") String id){ System.out.println("aop测试"); return "hello word!"; } }
ProceedingJoinPoint 与JoinPoint
ProceedingJoinPoint继承了JoinPoint,只能自环绕通知中使用,二者都可以获取方法参数等信息。
ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行并可以得到返回值,这也是环绕通知和前置、后置通知方法的一个最大区别。
运行访问
http://localhost/index/10
内部方法调用的坑
spring cache的原理是基于动态生成proxy代理机制来对方法的调用进行切面,如果对象的方法是内部调用(即this引用)而不不是外部引用,则会导致proxy失效,那么切面就失效。
案例:使用spring的@Cacheable
注解缓存一个查询数据,然后通过内部方法调用这个有加注解的方法。
@Service public class UserService { @Cacheable(cacheNames = "getUser") public String getUser(){ System.out.println("进入查询"); return "user:bushro"; } //内部方法调用 public String aoptest(){ return getUser(); } }
测试
@Test public void aoptest(){ System.out.println(userService.getUser()); System.out.println(userService.aoptest()); }
结果走了两次getUser(),按道理来说第一次查询后应该产生缓存,第二次就可以直接拿出来使用,可是第二次还是再查询了一次。
分析:
问题就出在内部调用那里:return getUser(),其实是return this.getUser();this是没有经过aop代理的,所以就还会查询一次。
解决方法
就是要让内部方法调用代理的类
原理:之所以方法类ApplicationContextHolder能够灵活自如地获取ApplicationContext,就是因为spring能够为我们自动地执行了setApplicationContext。只需要这个类实现了ApplicationContextAware接口并且被spring管理。
@Component public class ApplicationContextHolder implements ApplicationContextAware { private static ApplicationContext ctx; public static ApplicationContext getContext() { return ctx; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ctx = applicationContext; } }
内部调用修改
@Service public class UserService { @Cacheable(cacheNames = "getUser") public String getUser(){ System.out.println("进入查询"); return "user:bushro"; } public String aoptest(){ UserService proxy = ApplicationContextHolder.getContext().getBean(UserService.class); return proxy.getUser(); } }
再次调用aoptest()方法,解决