背景:
当需要为多个不具有继承关系的对象引入一个公共行为,例如日志、权限验证、事务等功能时。如果使用OOP,需要为每个Bean引入这些公共
行为。会产生大量重复代码,并且不利用维护,AOP就是为了解决这个问题。
AOP:
就是上面的解释,可以理解一种思想,不是Java独有的,作用是对方法进行拦截处理或增强处理。而在Java中我们使用Spring AOP和AspectJ。
Spring AOP:
基于动态代理实现,如果目标对象有实现接口,使用jdk proxy,如果目标对象没有实现接口,使用cglib。然后从容器获取代理后的对象,
在运行期植入“切面”类的方法。Spring AOP需要依赖于IOC容器来管理,只能作用于Spring容器中的Bean。
Spring AOP可以使用注解或者XML配置的方式,而类似@Aspect、@Pointcut等都是AspectJ的注解,但是通过Spring去实现,只是沿用AspectJ
的概念。理论上,Spring AOP足够日常开发,一般使用不到AspectJ。Spring AOP在运行时生成代理对象来织入的,还可以在编译期、类加载期织入
,比如AspectJ。
AspectJ:
AspectJ在实际代码运行前完成了织入,所以它生成的类是没有额外运行时开销的。而Spring AOP基于动态代理,生成一个代理类,这样栈深度
更深,效率理论上要差于AspectJ。AspectJ功能更强大,是AOP的完整解决方案。
AspectJ除了注解,个人不太了解,这里就不细讲了。
动态代理请参考:https://www.cnblogs.com/huigelaile/p/10980045.html
Spring AOP应用:
1、Controller层的参数校验:参考Spring AOP拦截Controller做参数校验
2、使用Spring AOP实现MySQL数据库读写分离案例分析
3、在执行方法前,判断是否具有权限
4、对部分函数的调用进行日志记录:监控部分重要函数,若抛出指定的异常,可以以短信或邮件方式通知相关人员
5、信息过滤,页面转发等等功能
Spring AOP术语:
在一个或多个连接点上,可以把切面的功能(通知)织入到程序的执行过程中
1、增强Advice:
方法层面的增强。对某个方法进行增强的方法,分为:Before、After、After-returning、After-throwing、Around。
try { //@Before result = method.invoke(target, args); //@After return result; } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); //@AfterThrowing throw targetException; } finally { //@AfterReturning }
2、连接点Join Point:
可以被拦截到的点。也就是可以被增强的方法都是连接点。
2、切点Pointcut:
joint point的组合,通常使用类和方法名称或者正则表达式匹配来指定切点。
3、切面Aspect:
切面是Advice和Pointcut的结合,他们共同定义了在何时和何处完成其功能。
4、引入Introduction:
向现有的类添加新方法或属性。
5、织入Weaving:
把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命里有多个点可以进行织入:
编译器、类加载期、运行期。
6、目标对象Target:
织入 Advice 的目标对象.。
Spring对AOP的支持:由于基于动态代理实现,所以只支持方法级别的连接点
1、基于代理的经典Spring AOP:虽然带有经典二字,但是现在看来这种方式已经有点捞。。。
2、纯POJO切面
3、@AspectJ注解驱动的切面
4、注入式AspectJ切面(适用于Spring各版本)
Spring AOP实现:
1、添加Maven依赖:
1).aspectjweaver
2).如果使用Spring Boot:spring-boot-starter-aop
2、首先开启Spring AOP支持
XML方式:<aop:aspectj-autoproxy/>
注解方式:@EnableAspectJAutoProxy,所有被@aspect配置的Bean,都是Aspect
3.1、基于XML(schema-based)
Spring AOP要使用AspectJ的切点表达式定义切点:
@execution:上面使用了execution来正则匹配方法,是最常用的。也可以使用其他的指示器。
execution表达式以*开始,表示不关心方法返回值的类型,两个点号(..)表名切点要选择任意的perform()方法,无论方法的参数是什么。
@within:指定所在类或所在包下面的方法
例如:@Pointcut("within(com.it.aop.AService..*)")
@annotation:方法上具有特定的注解,如@Subscribe用于订阅特定的事件。
例如:@Pointcut("execution(* .(..)) && @annotation(com.javadoop.annotation.Subscribe)")
@bean:匹配bean的名字
例如:@Pointcut("bean(*Service)")
PS:通常 "." 代表一个包名,".." 代表包及其子包,方法参数任意匹配使用两个点 ".."。
举个栗子:
public class AService { public void add() { System.out.println("add"); } }
public class LogRecord { public void log() { System.out.println("record log"); } public void transaction() { System.out.println("transaction"); } public void permission() { System.out.println("permission"); } }
<aop:config> <!--顶层的AOP配置元素。大多数aop元素都在这内部 --> <!--声明一个切面 --> <aop:aspect ref="logRecord"> <!--前置通知 --> <aop:before pointcut="execution(** com.it.aop.AService.add(..))" method="log" /> <aop:before pointcut="execution(** com.it.aop.AService.add(..))" method="permission" /> <!--返回通知 --> <aop:after-returning pointcut="execution(** com.it.aop.AService.add(..))" method="log" /> <!--异常通知 --> <aop:after-throwing pointcut="execution(** com.it.aop.AService.add(..))" method="transaction" /> </aop:aspect> </aop:config>
上述代码中,Pointcut都是相同的我们就可以声明<aop:pointcut>,如果把<aop:pointcut>作为<aop:config>的直接子元素,将作为全局Pointcut
<aop:config> <!--顶层的AOP配置元素。大多数aop元素都在这内部 --> <!--声明一个切面 --> <aop:aspect ref="logRecord"> <aop:pointcut id="add" expression="execution(** com.it.aop.AService.add(..))" /> <aop:before pointcut-ref="add" method="log" /> </aop:aspect> </aop:config>
环绕通知
public void around(MethodInvocationProceedingJoinPoint point) { try { System.out.println("record log"); System.out.println("permission"); point.proceed(); System.out.println("record log"); } catch (Throwable throwable) { throwable.printStackTrace(); } }
<aop:config> <!--顶层的AOP配置元素。大多数aop元素都在这内部 --> <!--声明一个切面 --> <aop:aspect ref="logRecord"> <aop:pointcut id="add" expression="execution(** com.it.aop.AService.add(..))" /> <!--环绕通知 --> <aop:around pointcut-ref="add" method="around" /> </aop:aspect> </aop:config>
3.2、基于注解(@AspectJ)实现:
@AspectJ和AspectJ没多大关系,仅仅是使用了AspectJ中的概念,注解来自于AspectJ的包,但是实现还是Spring AOP来的。
@Aspect public class LogRecord { @Pointcut("execution(** com.it.aop.AService.add(..))") public void add() {} @Before("add()") public void log() { System.out.println("record log"); } @AfterThrowing("add()") public void transaction() { System.out.println("transaction"); } @Before("add()") public void permission() { System.out.println("permission"); } @Around("add()") public void around(MethodInvocationProceedingJoinPoint point) { try { System.out.println("record log"); System.out.println("permission"); point.proceed(); System.out.println("record log"); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
PS:
Spring通常建议创建一个SystemArchitecture类,里面定义Pointcut,然后在需要的地方去引用。例如,在@Aspect的Bean中使用@Before
("com.it.SystemArchitecture.A")
Spring AOP通过@annotation实现权限控制
PS:这里只是校验部分API的登录状态和用户权限,如果系统要求登录过后才能请求,肯定就选择拦截器了。
1、首先定义两个注解
//@CheckLogin通过Cookie是否包含X-Token验证用户是否登录 public @interface CheckLogin { } //@CheckAuthorization("**")校验用户是否登录,权限**是否满足 @Retention(RetentionPolicy.RUNTIME) public @interface CheckAuthorization { String value(); }
2、注解的AOP处理
/* * * Description: 通过校验jwt中token实现登录和用户权限校验 **/ @Aspect //定义为一个切面 @Component //必须声明为Bean @RequiredArgsConstructor(onConstructor = @__(@Autowired)) //lombok实现IOC,相比直接通过@Autowired更有优势 public class AuthAspect { private final JwtOperator jwtOperator; //jwt操作类 //@CheckLogin注解操作 @Around("@annotation(com.diamondshine.auth.CheckLogin)") public Object checkLogin(ProceedingJoinPoint point) throws Throwable { checkToken(); return point.proceed(); } private void checkToken() { try { // 1. 从header里面获取token HttpServletRequest request = getHttpServletRequest(); Cookie[] cookies = request.getCookies(); String token = ""; for (Cookie cookie : cookies) { if (StringUtils.equals("X-Token", cookie.getName())) { token = cookie.getValue(); break; } } // 2. 校验token是否合法&是否过期;如果不合法或已过期直接抛异常;如果合法放行 Boolean isValid = jwtOperator.validateToken(token); if (!isValid) { throw new SecurityException("Token不合法!"); } // 3. 如果校验成功,那么就将用户的信息设置到request的attribute里面 Claims claims = jwtOperator.getClaimsFromToken(token); request.setAttribute("id", Long.valueOf(claims.get("id").toString())); request.setAttribute("role", claims.get("role")); } catch (Throwable throwable) { throw new SecurityException("Token不合法!"); } } private HttpServletRequest getHttpServletRequest() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; return attributes.getRequest(); } //@CheckAuthorization注解操作 @Around("@annotation(com.diamondshine.auth.CheckAuthorization)") public Object checkAuthorization(ProceedingJoinPoint point) throws Throwable { try { // 1. 验证token是否合法; this.checkToken(); // 2. 验证用户角色是否匹配 HttpServletRequest request = getHttpServletRequest(); List<String> list = (List<String>) request.getAttribute("role"); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); //获取@CheckAuthorization注解值 CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class); String value = annotation.value(); //ROLE_ADMIN,用户权限必须为admin。如果注解为ROLE_ADMIN_USER,用户权限为admin/user都可以 list.forEach(role -> { if (!(("ROLE_ADMIN".equals(value) && "ROLE_ADMIN".contains(role)) || ("ROLE_ADMIN_USER".equals(value) && "ROLE_ADMIN_USER".contains(role)))) { throw new SecurityException("用户无权访问!"); } }); } catch (Throwable throwable) { if (StringUtils.isNotBlank(throwable.getMessage())) { throw new SecurityException(throwable.getMessage(), throwable); } else { throw new SecurityException("用户无权访问!", throwable); } } return point.proceed(); } }
PS:内部使用jwt获取token中的信息,这段不重要,根据要求去实现代码,参考这种AOP实现方式
3、简单使用
/* * * Description: 用户必须登录,权限为ROLE_ADMIN_USER **/ @CheckAuthorization("ROLE_ADMIN_USER") @PostMapping(value = "/hahaha") @ResponseBody public CustomizeResponse subscribeHouse(@RequestParam(value = "house_id") Long houseId) { //*** } /* * * Description: 只需要用户登录状态 **/ @CheckLogin @GetMapping("rent/house/show/{id}") public String showHouseDetail(@PathVariable(value = "id") Long houseId, Model model) { //*** }
4、SecurityException异常处理,返回json数据
@Slf4j @RestControllerAdvice public class GlobalExceptionErrorHandler { @ExceptionHandler(SecurityException.class) public ResponseEntity<ErrorBody> error(SecurityException e) { log.warn("发生SecurityException异常", e); return new ResponseEntity<>( ErrorBody.builder() .body(e.getMessage()) .status(HttpStatus.UNAUTHORIZED.value()) .build(), HttpStatus.UNAUTHORIZED ); } } @Data @Builder @AllArgsConstructor @NoArgsConstructor class ErrorBody { private String body; private int status; }
如果权限不合法,页面返回
权限验证思路来自:面向未来微服务:Spring Cloud Alibaba从入门到进阶 第11章内容