github地址
本文主要介绍yh_admin模块下,annotion包下的三个自定义注解,利用aop实现请求权限鉴定,避免重复提交,以及异步记录用户操作日志
具体的类如下图:
先来复习一下什么是aop编程
首先要理解‘切’字,需要把对象想象成一个立方体,传统的面向对象变成思维,类定义完成之后(封装)。每次实例化一个对象,对类定义中的成员变量赋值,就相当于对这个立方体进行了一个定义,定义完成之后,那个对象就在那里,不卑不亢,不悲不喜,等着被使用,等着被回收。
面向切面编程则是指,对于一个我们已经封装好的类,我们可以在编译期间或在运行期间,对其进行切割,把立方体切开,在原有的方法里面添加(织入)一些新的代码,对原有的方法代码进行一次增强处理。而那些增强部分的代码,就被称之为切面,如下面代码实例中的通用日志处理代码,常见的还有事务处理、权限认证等等。
要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法。即要切哪些东西。
我们知道了要切哪些方法后,剩下的就是什么时候切,在原方法的哪一个执行阶段加入增加代码,这个就是连接点。如方法调用前,方法调用后,发生异常时等等。
通知被织入方法,改如何被增强。定义切面的具体实现。那么这里面就涉及到一个问题,空间(切哪里)和时间(什么时候切,在何时加入增加代码),空间我们已经知道了就是切入点中定义的方法,而什么时候切,则是连接点的概念,如下面实例中,通用日志处理(切面),@Pointcut规则中指明的方法即为切入点,@Before、@After是连接点,而下面的代码就是对应通知。
@Before("cutMethod()")
public void begin() {
System.out.println("==@Before== lingyejun blog logger : begin");
}
被一个或多个切面所通知的对象,即为目标对象。
AOP代理是AOP框架所生成的对象,该对象是目标对象的代理对象。代理对象能够在目标对象的基础上,在相应的连接点上调用通知。
将切面切入到目标方法之中,使目标方法得到增强的过程被称之为织入。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.8.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author: hzh
* @Date: 2020-08-07
* @Describe:
* @Modified By:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface testAnnotation {
/**
* 何种场景下的通用日志打印
*
* @return
*/
String module();
}
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @Author: hzh
* @Describe:
* 定义日志切面
* @Lazy 注解:容器一般都会在启动的时候实例化所有单实例 bean,如果我们想要 Spring 在启动的时候延迟加载 bean,需要用到这个注解
* value为true、false 默认为true,即延迟加载,@Lazy(false)表示对象会在初始化的时候创建
*
*/
@Aspect
@Component
@Lazy(false)
public class LoggerAspect {
/**
* 定义切入点:对要拦截的方法进行定义与限制,如包、类
*
* 1、execution(public * *(..)) 任意的公共方法
* 2、execution(* set*(..)) 以set开头的所有的方法
//1.定义切入点,即作用在被testAnnotation标签所标注的方法上~
@Pointcut("@annotation(testAnnotation n)")
private void cutMethod() {
}
/**
* 前置通知:在目标方法执行前调用
*/
@Before("cutMethod()")
public void begin() {
System.out.println("==@Before== hzh blog logger : begin");
}
/**
* 后置通知:在目标方法执行后调用,若目标方法出现异常,则不执行
*/
@AfterReturning("cutMethod()")
public void afterReturning() {
System.out.println("==@AfterReturning== hzh blog logger : after returning");
}
/**
* 后置/最终通知:无论目标方法在执行过程中出现一场都会在它之后调用
*/
@After("cutMethod()")
public void after() {
System.out.println("==@After== hzh blog logger : finally returning");
}
/**
* 异常通知:目标方法抛出异常时执行
*/
@AfterThrowing("cutMethod()")
public void afterThrowing() {
System.out.println("==@AfterThrowing== hzh blog logger : after throwing");
}
/**
* 环绕通知:灵活自由的在目标方法中切入代码
*/
@Around("cutMethod()")
......
}
@Component
public class LoggerApply {
@testAnnotation(module = "http://www.cnblogs.com/lingyejun/")
public void lingLogger(String event) throws Exception {
System.out.println("lingLogger(String event) : lingyejun will auth by blog address");
throw new Exception();
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class AnnotationTest {
@Autowired
private LoggerApply loggerApply;
@Test
public void testAnnotationLogger() {
try {
loggerApply.lingLogger("Blog Home");
} catch (Exception e) {
System.out.println("a exception be there");
}
}
}
/**
* 自定义权限校验注解
* @author hzh
* @since 2020-08-07
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorityVerify {
String value() default "";
}
/**
* 权限校验 切面实现
*
*/
/**
* @author hzh
* @since 2020-08-07
*/
@Aspect
@Component
@Slf4j
public class AuthorityVerifyAspect {
@Autowired
AdminService adminService;
@Autowired
RedisUtil redisUtil;
@Pointcut(value = "@annotation(authorityVerify)")
public void pointcut(AuthorityVerify authorityVerify) {
}
@Around(value = "pointcut(authorityVerify)")
public Object doAround(ProceedingJoinPoint joinPoint, AuthorityVerify authorityVerify) throws Throwable {
ServletRequestAttributes attribute = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attribute.getRequest();
//获取请求路径
String url = request.getRequestURI();
// 解析出请求者的ID和用户名
String adminUid = request.getAttribute(SysConf.ADMIN_UID).toString();
String key=RedisConf.ADMIN_VISIT_MENU + SysConf.REDIS_SEGMENTATION + adminUid;
System.out.println("-------------------------------");
System.out.println("begin");
if(!redisUtil.hasKey(key)){
// redis中还没有没有该管理员的权限列表,就查询mysql数据库获取,并保存到redis set集合中
System.out.println("mysql");
String[] urlList= adminService.getAdminAuthorities(adminUid);
for(int i=0;i<urlList.length;i++){
System.out.println(urlList[i]);
}
//保存权限列表到redis set集合中
redisUtil.sAdd(key,urlList);
redisUtil.expire(key,1,TimeUnit.HOURS);
}
// 判断该角色是否能够访问该接口,即url是否为set集合的成员
if(!redisUtil.sIsMember(key,url)){
System.out.println("用户不具有操作权限,访问的路径: "+url );
return ResultUtil.result(ECode.NO_OPERATION_AUTHORITY, MessageConf.RESTAPI_NO_PRIVILEGE);
}else{
System.out.println("用户具有访问权限:"+url);
}
//执行业务
return joinPoint.proceed();
}
}
/**
* 自定义避免重复提交注解
*
*/
/**
* @author hzh
* @since 2020-08-07
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {
/**
* 指定时间内不可重复提交,单位毫秒,默认1秒
*/
long timeout() default 1000;
}
/**
* 避免重复提交aop
*
*/
/**
* @author hzh
* @since 2020-08-07
*/
@Aspect
@Component
@Slf4j
public class AvoidRepeatableCommitAspect {
@Autowired
private RedisUtil redisUtil;
/**
* @param point
*/
@Around("@annotation(AvoidRepeatableCommit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = RequestHolder.getRequest();
String ip = IpUtils.getIpAddr(request);
//获取方法
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//目标类、方法名称
String className = method.getDeclaringClass().getName();
String name = method.getName();
// 类和方法名重组
String ipKey = String.format("%s#%s", className, name);
// 转换成HashCode
int hashCode = Math.abs(ipKey.hashCode());
//重组成key,用于到redis中查询
String key = String.format("%s:%s_%d", RedisConf.AVOID_REPEATABLE_COMMIT, ip, hashCode);
log.info("ipKey={},hashCode={},key={}", ipKey, hashCode, key);
//获取定义的过期时间,默认一秒
AvoidRepeatableCommit avoidRepeatableCommit = method.getAnnotation(AvoidRepeatableCommit.class);
long timeout = avoidRepeatableCommit.timeout();
String value = redisUtil.get(key);
if (StringUtils.isNotBlank(value)) {
log.info("请勿重复提交表单");
return ResultUtil.result(SysConf.ERROR, "请勿重复提交表单");
}
// 设置过期时间
redisUtil.setEx(key, StringUtils.getUUID(), timeout, TimeUnit.MILLISECONDS);
//执行方法
Object object = point.proceed();
return object;
}
}
最后,关于loggerAspect,即异步记录任务,因篇幅原因,这里就补贴出了,代码在github上