Springboot 自定义注解、切面

本文实现的是使用自定义注解作为切入点。

1、创建springboot工程,引入依赖

本次任务实例主要引入以下两个依赖即可。

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
	</dependencies>
2、自定义注解 WebLog
package com.qqxhb.mybatis.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;
/**
 * Web 日志注解
 *
 */
//运行时使用
@Retention(RetentionPolicy.RUNTIME)
//注解用于方法
@Target({ ElementType.METHOD })
//注解包含在JavaDoc中
@Documented
public @interface WebLog {

	String value() default "";
}

知识补充

  • Retention注解
    Reteniton的作用是定义被它所注解的注解保留多久,一共有三种策略,定义在RetentionPolicy枚举中。
public enum RetentionPolicy {
    /**
     *被编译器忽略
     */
    SOURCE,

    /**
     * 注解将会被保留在Class文件中,但在运行时并不会被VM保留。这是默认行为,所有没有      * 用Retention注解的注解,都会采用这种策略。
     */
    CLASS,

    /**
     * 保留至运行时。所以我们可以通过反射去获取注解信息
     */
    RUNTIME
}
  • Target注解
    说明了Annotation所修饰的对象范围,常用的是TYPE、FIELD、METHOD,具体策略在枚举ElementType中定义。
public enum ElementType {
    /** 类, 接口 (包括注释类型), 或 枚举 声明 */
    TYPE,
 
    /** 字段声明(包括枚举常量) */
    FIELD,
 
    /** 方法声明(Method declaration) */
    METHOD,
 
    /** 正式的参数声明 */
    PARAMETER,
 
    /** 构造函数声明 */
    CONSTRUCTOR,
 
    /** 局部变量声明 */
    LOCAL_VARIABLE,
 
    /** 注释类型声明 */
    ANNOTATION_TYPE,
 
    /** 包声明 */
    PACKAGE,
 
    /**
     * 类型参数声明
     *
     * @since 1.8
     */
    TYPE_PARAMETER,
 
    /**
     * 使用的类型
     *
     * @since 1.8
     */
    TYPE_USE
}
3、自定义切面
package com.qqxhb.mybatis.aspect;

import java.lang.reflect.Method;
import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.qqxhb.mybatis.annotation.WebLog;

/**
 * web日志切面
 *
 */
//标识这是一个切面
@Aspect
//交给spring容器管理
@Component
public class WebLogAspect {

	private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

	// 以自定义 @WebLog 注解为切点
	@Pointcut("@annotation(com.qqxhb.mybatis.annotation.WebLog)")
	public void webLog() {
	}

	/**
	 * 切点之前
	 * 
	 * @param joinPoint
	 * @throws Throwable
	 */
	@Before("webLog()")
	public void before(JoinPoint joinPoint) throws Throwable {
		// 得到 HttpServletRequest
		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = attributes.getRequest();
		logger.info("============ before ==========");
		// 获取WebLog注解信息
		String info = getWebLogInfo(joinPoint);
		logger.info("Point Info    : {}", info);
		// 请求地址URL
		logger.info("URL	: {}", request.getRequestURL().toString());
		// 请求方法
		logger.info("HTTP Method : {}", request.getMethod());
		// 具体切入执行方法
		logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(),
				joinPoint.getSignature().getName());
		// 请求IP
		logger.info("IP	: {}", request.getRemoteAddr());// 打印描述信息
		// 请求参数
		logger.info("Input Parameter : {}", Arrays.asList(joinPoint.getArgs()));
	}

	/**
	 * 切点之后
	 * 
	 * @throws Throwable
	 */
	@After("webLog()")
	public void after() throws Throwable {
		logger.info("============ after ==========");
	}

	/**
	 * 切点返回内容后
	 * 
	 * @throws Throwable
	 */
	@AfterReturning("webLog()")
	public void afterReturning() throws Throwable {
		logger.info("============ afterReturning ==========");
	}

	/**
	 * 切点抛出异常后
	 * 
	 * @throws Throwable
	 */
	@AfterThrowing("webLog()")
	public void afterThrowing() throws Throwable {
		logger.info("============ afterThrowing ==========");
	}

	/**
	 * 环绕
	 * 
	 * @param proceedingJoinPoint
	 * @return
	 * @throws Throwable
	 */
	@Around("webLog()")
	public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		logger.info("============ doAround ==========");
		long startTime = System.currentTimeMillis();
		Object result = proceedingJoinPoint.proceed();
		// 打印出参
		logger.info("Output Parameter : {}", result);
		// 执行时间
		logger.info("Execution Time : {} ms", System.currentTimeMillis() - startTime);
		return result;
	}

	/**
	 * 获取web日志注解信息
	 * 
	 * @param joinPoint
	 * @return
	 * @throws Exception
	 */
	public String getWebLogInfo(JoinPoint joinPoint) throws Exception {
		// 获取切入点的目标类
		String targetName = joinPoint.getTarget().getClass().getName();
		Class<?> targetClass = Class.forName(targetName);
		// 获取切入方法名
		String methodName = joinPoint.getSignature().getName();
		// 获取切入方法参数
		Object[] arguments = joinPoint.getArgs();
		// 获取目标类的所有方法
		Method[] methods = targetClass.getMethods();
		for (Method method : methods) {
			// 方法名相同、包含目标注解、方法参数个数相同(避免有重载)
			if (method.getName().equals(methodName) && method.isAnnotationPresent(WebLog.class)
					&& method.getParameterTypes().length == arguments.length) {
				return method.getAnnotation(WebLog.class).value();
			}
		}
		return "";
	}
}

知识点补充

  • 指定切面有效环境
    可以在切面类上加Profile注解指定运行环境。
    spring在确定那个profile处于激活状态的时,需要依赖两个独立的属性:spring.profiles.active和spring.profile.default。如果设置了spring.profiles.active属性,那么它的值就会用来确定那个profile是激活的。如果没有设置spring.profiles.active属性的话,那spring将会查找spring.profiles.default的值。
//指定在开发、测试环境使用
@Profile({ "dev", "test" })

//此时需要设置spring.profiles.active的值为dev或者test 切面才会生效。
  • 切点指示符
    切点指示符是切点定义的关键字,切点表达式以切点指示符开始。开发人员使切点指示符来告诉切点将要匹配什么,有以下9种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation。
    execution
    execution是一种使用频率比较高比较主要的一种切点指示符,用来匹配方法签名,方法签名使用全限定名,包括访问修饰符(public/private/protected)、返回类型,包名、类名、方法名、参数,其中返回类型,包名,类名,方法,参数是必须的,如下面代码片段所示:
@Pointcut("execution(public String org.baeldung.dao.FooDao.findById(Long))")

上面的代码片段里的表达式精确地匹配到FooDao类里的findById(Long)方法,但是这看起来不是很灵活。假设我们要匹配FooDao类的所有方法,这些方法可能会有不同的方法名,不同的返回值,不同的参数列表,为了达到这种效果,我们可以使用通配符。如下代码片段所示:

@Pointcut("execution(* org.baeldung.dao.FooDao.*(..))")

第一个通配符匹配所有返回值类型,第二个匹配这个类里的所有方法,()括号表示参数列表,括号里的用两个点号表示匹配任意个参数,包括0个
within
使用within切点批示符可以达到上面例子一样的效果,within用来限定连接点属于某个确定类型的类。如下面代码的效果与上面的例子是一样的:

@Pointcut("within(org.baeldung.dao.FooDao)")

我们也可以使用within指示符来匹配某个包下面所有类的方法(包括子包下面的所有类方法),如下代码所示:

@Pointcut("within(org.baeldung..*)")

this 和 target
this用来匹配的连接点所属的对象引用是某个特定类型的实例,target用来匹配的连接点所属目标对象必须是指定类型的实例;那么这两个有什么区别呢?原来AspectJ在实现代理时有两种方式:
1、如果当前对象引用的类型没有实现自接口时,spring aop使用生成一个基于CGLIB的代理类实现切面编程
2、如果当前对象引用实现了某个接口时,Spring aop使用JDK的动态代理机制来实现切面编程
this指示符就是用来匹配基于CGLIB的代理类,通俗的来讲就是,如果当前要代理的类对象没有实现某个接口的话,则使用this;target指示符用于基于JDK动态代理的代理类,通俗的来讲就是如果当前要代理的目标对象有实现了某个接口的话,则使用target.:

public class FooDao implements BarDao {
    ...
}

比如在上面这段代码示例中,spring aop将使用jdk的动态代理来实现切面编程,在编写匹配这类型的目标对象的连接点表达式时要使用target指示符, 如下所示:

@Pointcut("target(org.baeldung.dao.BarDao)")

如果FooDao类没有实现任何接口,或者在spring aop配置属性:proxyTargetClass设为true时,Spring Aop会使用基于CGLIB的动态字节码技为目标对象生成一个子类将为代理类,这时应该使用this指示器:

@Pointcut("this(org.baeldung.dao.FooDao)")

@Target
这个指示器匹配指定连接点,这个连接点所属的目标对象的类有一个指定的注解:

@Pointcut("@target(org.springframework.stereotype.Repository)")

@args
这个指示符是用来匹配连接点的参数的,@args指出连接点在运行时传过来的参数的类必须要有指定的注解,假设我们希望切入所有在运行时接受实@Entity注解的bean对象的方法:

@Pointcut("@args(org.baeldung.aop.annotations.Entity)")

@within
这个指示器,指定匹配必须包括某个注解的的类里的所有连接点:

@Pointcut("@within(org.springframework.stereotype.Repository)")

上面的切点跟以下这个切点是等效的:

@Pointcut("within(@org.springframework.stereotype.Repository *)")

@annotation
这个指示器匹配那些有指定注解的连接点,比如,我们可以新建一个这样的注解@Loggable:

@Pointcut("@annotation(org.baeldung.aop.annotations.Loggable)")

切点表达式 可以使用&&、||、!、三种运算符来组合切点表达式,表示与或非的关系。

@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}

@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}

@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}
4、使用注解切面

在需要使用该切面的方法上添加自定义的注解即可。

/**
	 * 查询新闻
	 * 
	 * @param title
	 * @return
	 */
	@GetMapping
	@WebLog("查询新闻列表接口")
	public List<News> getNews(String title, @RequestParam(defaultValue = "1") int pageIndex,
			@RequestParam(defaultValue = "5") int pageSize) {
		return newsServiceImpl.selectNews(title, pageIndex, pageSize);
	}
5、访问接口测试效果

在这里插入图片描述
源码地址:https://github.com/qqxhb/springboot-mybatis-demo
中的springboot-aspect

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值