衡云blog系列--1.利用springboot自定义注解实现aop切面编程,验证用户请求权限验证,避免重复提交表单,

一 说明

github地址
本文主要介绍yh_admin模块下,annotion包下的三个自定义注解,利用aop实现请求权限鉴定,避免重复提交,以及异步记录用户操作日志
具体的类如下图:
衡云blog系列--1.利用springboot自定义注解实现aop切面编程,验证用户请求权限验证,避免重复提交表单,_第1张图片

二 spring aop

先来复习一下什么是aop编程

1、切面(Aspect)

首先要理解‘切’字,需要把对象想象成一个立方体,传统的面向对象变成思维,类定义完成之后(封装)。每次实例化一个对象,对类定义中的成员变量赋值,就相当于对这个立方体进行了一个定义,定义完成之后,那个对象就在那里,不卑不亢,不悲不喜,等着被使用,等着被回收。

面向切面编程则是指,对于一个我们已经封装好的类,我们可以在编译期间或在运行期间,对其进行切割,把立方体切开,在原有的方法里面添加(织入)一些新的代码,对原有的方法代码进行一次增强处理。而那些增强部分的代码,就被称之为切面,如下面代码实例中的通用日志处理代码,常见的还有事务处理、权限认证等等。

2、切入点(PointCut)

要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法。即要切哪些东西。

3、连接点(JoinPoint)

我们知道了要切哪些方法后,剩下的就是什么时候切,在原方法的哪一个执行阶段加入增加代码,这个就是连接点。如方法调用前,方法调用后,发生异常时等等。

4、通知(Advice)

通知被织入方法,改如何被增强。定义切面的具体实现。那么这里面就涉及到一个问题,空间(切哪里)和时间(什么时候切,在何时加入增加代码),空间我们已经知道了就是切入点中定义的方法,而什么时候切,则是连接点的概念,如下面实例中,通用日志处理(切面),@Pointcut规则中指明的方法即为切入点,@Before、@After是连接点,而下面的代码就是对应通知。

    @Before("cutMethod()")
    public void begin() {
        System.out.println("==@Before== lingyejun blog logger : begin");
    }

5、目标对象(Target Object)

被一个或多个切面所通知的对象,即为目标对象。

6、AOP代理对象(AOP Proxy Object)

AOP代理是AOP框架所生成的对象,该对象是目标对象的代理对象。代理对象能够在目标对象的基础上,在相应的连接点上调用通知。

7、织入(Weaving)

将切面切入到目标方法之中,使目标方法得到增强的过程被称之为织入。

三 实列

1.pom

<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>

2. 自定义定义注解

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();
}  

3.定义aop

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()")
    ......
}

4.运用注解

@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();
    }
}  

5.测试

@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");
        }
    }
}

四 自定义请求权限鉴定注解

4.1AuthorityVerify注解

/**
 * 自定义权限校验注解
 * @author hzh
 * @since 2020-08-07
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorityVerify {
    String value() default "";
}

4.2AuthorityVerifyAspect 切面

/**
 * 权限校验 切面实现
 *
 */
/**
 * @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();
    }

}

五 自定义防止重复提交注解

5.1 自定义 AvoidRepeatableCommit注解

/**
 * 自定义避免重复提交注解
 *
 */
/**
 * @author hzh
 * @since 2020-08-07
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {
    /**
     * 指定时间内不可重复提交,单位毫秒,默认1秒
     */
    long timeout() default 1000;
}

5.2 AvoidRepeatableCommitAspect 切面
/**
 * 避免重复提交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上

你可能感兴趣的:(衡云blog系列,aop,springboot,注解)