本文实现的是使用自定义注解作为切入点。
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、访问接口测试效果