AOP的基本概念

  • Aspect(切面):通常是一个类,里面可以定义切入点和通知(切面 = 切点+通知)
  • Pointcut(切点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
  • JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用
  • Advice(通知):AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around
  • AOP代理:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类

连接点和切入点的区别:

  • Jointpoint(连接点) 是具体的某个目标方法
  • Pointcut(切入点) 用于指定 “连接点” 的一个表达式

通知类型

  • Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可
  • AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值
  • AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象
  • After:在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式
  • Around:环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint

通知After和AfterReturning

  • After无论是否异常都一定执行,AfterReturning无异常才执行

1:自定义RequestMapBody 注解

package com.xs.breast.commons.framework.web.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestMapBody {
	String operationType() default "";
}

这里注解类上的三个注解称为元注解,其分别代表的含义如下:

  • @Target:注解作用的位置,ElementType.PARAMETER表示该注解仅能作用于参数上,ElementType.METHOD表示该注解仅能作用于方法上,ElementType.TYPE表示该注解仅能作用于类上。作用在参数或者方法上@Target(ElementType.PARAMETER,ElementType.METHOD)
  • @Retention:注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
    保留的时间范围 (RetentionPolicy)
    SOURCE源文件保留(如@Override保留在源文件,编译后注解消失)
    CLASS编译时保留(如lombok生成get/set)
    RUNTIME运行时保留(如切面记录日志,或验证参数信息等)
  • @Documented:注解信息会被添加到Java文档中
  • @Inherited:子类注解自动继承该注解(更加业务情况选择)

注意:创建这样一个注解,仅仅是一个标志,装饰类、方法、属性的,并没有功能,要想实现功能,需要我们通过拦截器、AOP切面这些地方获取注解标志,然后实现我们的功能

java自定义注解的使用范围

一般我们可以通过注解来实现一些重复的逻辑,就像封装了的一个方法,可以用在一些权限校验、字段校验、字段属性注入、保存日志、缓存

2:创建一个AOP切面类来拦截@RequestMapBody这个注解

@Aspect
@Component
@Slf4j
public class RequestMapBodyAop{
 
	//@Pointcut("execution(* com.xs.breast.commons.framework.web.annotation.RequestMapBody)")
    @Pointcut("@annotation(com.xs.breast.commons.framework.web.annotation.RequestMapBody)")
    private void pointcutOne(){
    }
 
	//@Before("pointcutOne() && @annotation(content) ")
	//@After("pointcutOne()")
	//@AfterRunning("pointcutOne()")
	//@AfterThrowing("pointcutOne()")
    @Around("pointcutOne()")
    public Object aroud(ProceedingJoinPoint joinPoint) throws Throwable {
  		//获取参数,可对参数进行操作
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            if(args[i] instanceof String) {
                args[i] = args[i] + "---";
                break;
            }

		Object result = null;
        //对返回数据进行操作
        Object result = joinPoint.proceed(args);
        if(result instanceof String) {
            String content =result.toString();
            return content;
        }
        return result;
        
        log.info("请求参数:{}",JSON.toJSON(args));
        log.info("返回结果:{}",JSON.toJSON(result));
        return result;
    }
}
  • @Pointcut声明了切点(这里的切点是我们自定义的注解类),切点可以指定到包路径下的类或方法
  • @Before中的@annotation(content) 可以获取自定义的注解对象,所以就能够获取我们在使用注解时赋予的值了
    ( public Object aroud(ProceedingJoinPoint joinPoint,RequestMapBody content) throws Throwable {)
  • JoinPoint能做的ProceedingJoinPoint都能做,ProceedingJoinPoint继承了JoinPoint接口
@Before("pointcutOne() && @annotation(content)")
    public void advice(JoinPoint joinPoint, RequestMapBody content) {
        System.out.println("注解作用的方法名: " + joinPoint.getSignature().getName());
        
        System.out.println("所在类的简单类名: " + joinPoint.getSignature().getDeclaringType().getSimpleName());
        
        System.out.println("所在类的完整类名: " + joinPoint.getSignature().getDeclaringType());
        
        System.out.println("目标方法的声明类型: " + Modifier.toString(joinPoint.getSignature().getModifiers()));
    }

3:使用注解

@RestController
@RequestMapping("/demo")
public class DemoController {
	@RequestMapBody()
	@GetMapping("/getContent")
	public Object getContent( String content) {
	return content;
	}
}

4:示例:角色校验注解(springsecurity中的角色校验)

我们在使用springsecurity有一个注解@PreAuthorize可以作用在类或方法上,用来校验是否有权限访问,我们可以模仿这个注解,写一个我们自定义注解来实现同样的功能

  • 创建一个自定义角色校验注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RoleAuthorize {
    String[] value() default {};
}
  • 创建一个拦截器
    这个拦截器拦截所有访问路径的url,如果访问方法上带有我们创建的自定义注解RoleAuthorize ,则获取这个注解上限定的访问角色,方法没有注解再获取这个类是否有这个注解,如果这个类也没有注解,则这个类的访问没有角色限制,放行,如果有则校验当前用户的springsecurity是否有这个角色,有则放行,没有则抛出和springsecurity一样的异常AccessDeniedException,全局异常捕获这个异常,返回状态码403(表示没有权限访问)
@Component
public class RoleInterceptor extends HandlerInterceptorAdapter{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod)handler;

        //在方法上寻找注解
        RoleAuthorize permission = handlerMethod.getMethodAnnotation(RoleAuthorize.class);
        if (permission == null) {
            //方法不存在则在类上寻找注解则在类上寻找注解
            permission = handlerMethod.getBeanType().getAnnotation(RoleAuthorize.class);
        }

        //如果没有添加权限注解则直接跳过允许访问
        if (permission == null) {
            return true;
        }

        //获取注解中的值
        String[] validateRoles = permission.value();
        //校验是否含有对应的角色
        for(String role : validateRoles){
            //从springsecurity的上下文获取用户角色是否存在当前的角色名称
            if(AuthUserUtils.hasRole("ROLE_"+role)){
                return true;
            }
        }
        throw  new AccessDeniedException("没有权限访问当前接口");
    }

}
  • java配置拦截器注册
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Autowired
    private RoleInterceptor roleInterceptor;
    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(roleInterceptor).addPathPatterns("/**");
        super.addInterceptors(registry);
        
    }
}

备注:
1.这里添加拦截器可以继承WebMvcConfigurerAdapter (已过时,在springboot2.0是继承 WebMvcConfigurationSupport或实现WebMvcConfigurer)
2.WebMvcConfigurationSupport–>不需要返回逻辑视图,可以选择继承此类.WebMvcCofigurer–>返回逻辑视图,可以选择实现此方法,重写addInterceptor方法
3.继承webmvcconfigurationsupport之后就没有springmvc的自动配置了 建议实现WebMvcConfigurer

  • springmvc.xml配置拦截器注册
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

	<aop:aspectj-autoproxy proxy-target-class="true"/>
	<!-- 配置自动扫描的包controller才生效 -->
	<context:component-scan base-package="com.xs.breast.view"></context:component-scan>


	<mvc:interceptors>
		<mvc:interceptor>
			<mvc:mapping path="/**" />
			<!-- 定义在mvc:interceptor下面的表示是对特定的请求才进行拦截的 -->
			<bean
				class="com.xs.breast.commons.framework.interceptor.RoleInterceptor" />
		</mvc:interceptor>
	</mvc:interceptors> 

</beans>

引入依赖

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>