0%

自定义注解实现系统启动时后端权限数据生成

一. 注解介绍

  1. 注解Annotation是一种引用数据类型,编译之后也是生成.class类型的java文件

    语法:修饰符 @interface 注解类型名

    • 注解可以出现在类上、方法上、属性上、甚至注解上等…
    • JDK中内置的注解@Override(复写)、@SuppressWarnings(忽略编译器的警告)等
  2. 元注解

    • 元注解就是用来修饰注解的,是注解上的注解;常见的元注解有@Target、@Retention、@Documented、@Inherited.

    • @Target:表示当前注解使用在什么位置

      例如1:@Target(ElementType.METHOD)
      Target内部的值使用枚举ElementType表示,表示的主要位置有:注解、构造方法、属性、局部变量、函数、包、参数和类(默认值)。
      例如2:@Target({ElementType.METHOD,ElementType.TYPE})
      多个位置使用数组的写法
      
    • @Retention:定义被它所标记的注解能保留多久

      Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型
      这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention搭配 RententionPolicy使用
      RetentionPolicy有3个值:CLASS,RUNTIME,SOURCE
      
      @Retention(RetentionPolicy.SOURCE):注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
      @Retention(RetentionPolicy.CLASS):注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
      @Retention(RetentionPolicy.RUNTIME):注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
      
    • @Documented:加了这个注解的注解,在生成文档的时候,可以在文档中显示出来

      @Documented
      public @interface A{
      }
      
    • @Inherited:加了这个注解的注解,能被继承

      @Inherited
      public @interface A{
      }
      
      @A
      class B(){
      }
      
      class C extends B{
      }
      

二. 自定义权限注解,项目启动时扫描注解加权限

1. 定义注解

package io.coderyeah.basic.annotation;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})//注解能作用在方法上、类上
//Java中的反射:在运行时,动态获取类的各种信息的一种能力
@Retention(RetentionPolicy.RUNTIME)//可以通过反射读取注解
@Inherited//可以被继承
@Documented//可以被javadoc工具提取成文档,可以不加
public @interface PreAuthorize {
    //对应t_permission表中的sn
    String sn(); //department:patchDel
    //对应t_permission表中的name
    String name(); //部门批量删除
}

以后在方法或类上加了这个@PreAuthorize 注解,都会去执行一段业务代码【例如:添加权限到t_promission】,但是要先扫描这个注解,然后解析这个注解,再去执行相应的业务代码

2. 例子

    @Autowired
    private DepartmentService departmentService;

    /**
     * @param deptDTO 部门查询参数
     * @return Result
     */
    @PreAuthorize(name = "部门列表", sn = "department:list")
    @LogAnnotation(module = "部门模块", operate = "分页查询部门列表")
    @ApiOperation("查询部门列表")
    @PostMapping("/list")
    public Result list(@RequestBody(required = false) DeptDTO deptDTO) {
        return departmentService.list(deptDTO);
    }

3. 扫描注解加权限

注解定义之后,需要扫描。就像业务代码中@Service注解,服务启动的时候就会去扫描,生成业务对象。并注入到Controller使用。如果启动的时候业务代码中没有添加@Service注解,启动会报错的。

4. 自定义的注解怎么扫描呢?而且要在服务器启动的时候自动扫描?

  • 可以通过Web三大组件:Servlet、过滤器Filter、监听器Listenter
  • 在SpringBoot项目中,如果想自定义Servlet、Filter、Listenter,我们只需要完成两个步骤:

    1. 自己写一个类实现父接口或者继承父类:Spring提供的的Servlet、Filter、Listenter,并在实现类上打上注解
        1.1. 自定义servlet:继承HttpServlet,打注解@WebServlet
          1.2. 自定义Filter:实现Filter,打注解@WebFilter
        1.3. 自定义Listenter:实现ServletContextListener,打注解@WebListener 
        注意:SpringBoot项目中没有web.xml,不能通过xml配置实现。但是可以通过注解
    2. 交给容器扫描:在启动类上打注解:@ServletComponentScan扫描Servlet、Filter、Listenter的包即可
    

5 .权限注解扫描监听器

监听器:监听四大作用域的变化和属性的变更
application的类型:ServletContext - 这个对象会在服务器启动的时候自动生成,而且是唯一一个。

package io.coderyeah.system.listener;

import io.coderyeah.system.service.PermissionScanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener//申明自定义的web监听器,被容器注册和使用
@Slf4j
public class PermissionScanInitListener implements ServletContextListener {

    @Autowired
    private IPermissionScanService permissionScanService;
    
    //spring容器初始化结束之后被调用
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //这里面的业务随着接口的变多,可能执行时间会非常久,影响性能。影响主线程的启动
        new Thread(new Runnable() {//不用主线程去执行,用一个新的线程去执行
            @Override
            public void run() {
                //可以在这里扫描我们自定义的注解@PreAuthorize,然后将信息存储到t_permission表
                //这样就无需手动录入信息到权限t_permission表了
                log.info("权限初始化开始******************************************");
                System.out.println("权限初始化开始******************************************");
                permissionScanService.scanPermission();
                System.out.println("权限初始化结束******************************************");
            }
        }).start();
    }
 
    //容器销毁的时候执行
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    }
}

6. 启动类

@SpringBootApplication
@MapperScan("io.coderyeah.*.mapper")
//加载Listener - 本来监听器只要服务器一启动就会执行,但是SpringBoot项目中是通过启动类开启服务的,所以要加这个注解去加载listener,listener才会起作用
@ServletComponentScan(value = {"io.coderyeah.system.listener"})
public class PetHomeApplication {
    public static void main(String[] args) {
        SpringApplication.run(PetHomeApplication.class,args);
    }
}
//测试:启动项目就会打印输出信息

7. 业务接口

此业务接口专门用来解析注解@PreAuthorize(name = "部门列表",sn= "department:list")和注解上的参数,并获取出来添加到权限表t_permission。

//1.找包 - 找类 - 找方法 - 找注解
//2.解析这个注解拿到:sn,name
//3.当前方法的url地址
//4.创建一个Permisson对象 - 添加到数据库
package io.coderyeah.system.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.coderyeah.basic.annotation.PreAuthorize;
import io.coderyeah.basic.util.ClassUtils;
import io.coderyeah.system.domain.Permission;
import io.coderyeah.system.mapper.PermissionMapper;
import io.coderyeah.system.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;

import java.io.File;
import java.io.FileFilter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author coderyeah
 * @description 针对表【t_permission】的数据库操作Service实现
 * @createDate 2022-09-21 11:00:07
 */
@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {
    private static final String PKG_PREFIX = "io.coderyeah.";
    private static final String PKG_SUFFIX = ".controller";
    @Autowired
    private PermissionMapper permissionMapper;

    @Override
    public void scanPermission() {
        //获取 io.coderyeah 下面所有的模块目录
        String path = this.getClass().getResource("/").getPath() + "/io/coderyeah/";
        // 当前包路径下的文件对象
        File file = new File(path);
        // 过滤出当前包下所有目录的文件数组
        File[] files = file.listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                return file.isDirectory();
            }
        });

        //获取io.coderyeah.*.controller里面所有的类
        Set<Class> clazzes = new HashSet<>();
        assert files != null;
        for (File fileTmp : files) {
            System.out.println("===============权限注解解析:获取所有的包==============");
            System.out.println(fileTmp.getName());
            // 将所有类对象放进set集合
            clazzes.addAll(ClassUtils.getClasses(PKG_PREFIX + fileTmp.getName() + PKG_SUFFIX));
        }

        // 遍历类对象集合
        for (Class clazz : clazzes) {
            // 获取当前类的所有方法
            Method[] methods = clazz.getMethods();
            // 判断是否有方法存在
            if (methods == null || methods.length < 1) {
                return;
            }
            // 遍历当前类中所有的方法
            for (Method method : methods) {
                // 获取接口执行路径
                String uri = getUri(clazz, method);
                try {
                    PreAuthorize preAuthorizeAnno = method.getAnnotation(PreAuthorize.class);
                    if (preAuthorizeAnno == null) {
                        // 跳出当前循环
                        continue;
                    }
                    String name = preAuthorizeAnno.name();
                    String permissionSn = preAuthorizeAnno.sn();
                    Permission permissionTmp = permissionMapper.selectOne(new LambdaQueryWrapper<Permission>().eq(Permission::getSn, permissionSn));
                    //如果不存在就添加
                    if (permissionTmp == null) {
                        Permission permission = new Permission();
                        permission.setName(name);       //t_permission表中的权限名
                        permission.setSn(permissionSn); //t_permission表中的权限编号
                        permission.setUrl(uri);           //t_permission表中的权限路径
                        permissionMapper.insert(permission);
                    } else {
                        //如果存在就修改
                        permissionTmp.setName(name);
                        permissionTmp.setSn(permissionSn);
                        permissionTmp.setUrl(uri);
                        permissionMapper.updateById(permissionTmp);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }


    //获取t_permission表中的url  //@RequestMapping("/department") //@GetMapping("/{id}")
    private String getUri(Class clazz, Method method) {
        //获取类上的请求路径:/department
        String classPath = "";
        // 获取当前类的@RequestMapping注解
        Annotation annotation = clazz.getAnnotation(RequestMapping.class);
        // 判断注解是否存在
        if (annotation != null) {
            // 类型强转
            RequestMapping requestMapping = (RequestMapping) annotation;
            // 获取@RequestMapping注解的数组值
            String[] values = requestMapping.value();
            // 判断值是否为空
            if (values != null && values.length > 0) {
                // 将请求路径赋值给classPath
                classPath = values[0];
                if (!"".equals(classPath) && !classPath.startsWith("/"))
                    classPath = "/" + classPath;
            }
        }
        //以下是获取方法上的请求路径:/{id}
        GetMapping getMapping = method.getAnnotation(GetMapping.class);
        // 方法上的请求路径
        String methodPath = "";
        if (getMapping != null) {
            // 获取注解上的值
            String[] values = getMapping.value();
            if (values != null && values.length > 0) {
                methodPath = values[0];
                if (!"".equals(methodPath) && !methodPath.startsWith("/"))
                    methodPath = "/" + methodPath;
            }
        }

        PostMapping postMapping = method.getAnnotation(PostMapping.class);
        if (postMapping != null) {
            String[] values = postMapping.value();
            if (values != null && values.length > 0) {
                methodPath = values[0];
                if (!"".equals(methodPath) && !methodPath.startsWith("/"))
                    methodPath = "/" + methodPath;
            }
        }

        DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
        if (deleteMapping != null) {
            String[] values = deleteMapping.value();
            if (values != null && values.length > 0) {
                methodPath = values[0];
                if (!"".equals(methodPath) && !methodPath.startsWith("/"))
                    methodPath = "/" + methodPath;
            }
        }

        PutMapping putMapping = method.getAnnotation(PutMapping.class);
        if (putMapping != null) {
            String[] values = putMapping.value();
            if (values != null && values.length > 0) {
                methodPath = values[0];
                if (!"".equals(methodPath) && !methodPath.startsWith("/"))
                    methodPath = "/" + methodPath;
            }

        }

        PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);
        if (patchMapping != null) {
            String[] values = patchMapping.value();
            if (values != null && values.length > 0) {
                methodPath = values[0];
                if (!"".equals(methodPath) && !methodPath.startsWith("/"))
                    methodPath = "/" + methodPath;
            }
        }

        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            String[] values = requestMapping.value();
            if (values != null && values.length > 0) {
                methodPath = values[0];
                if (!"".equals(methodPath) && !methodPath.startsWith("/"))
                    methodPath = "/" + methodPath;
            }
        }
        return classPath + methodPath;  // /department/{id}
    }

    private String getPermissionSn(String value) {
        String regex = "\\[(.*?)]";
        Pattern p = Pattern.compile("(?<=\\()[^\\)]+");
        Matcher m = p.matcher(value);
        String permissionSn = null;
        if (m.find()) {
            permissionSn = m.group(0).substring(1, m.group().length() - 1);
        }
        return permissionSn;
    }
}

8. 工具类:ClassUtils.java

package io.coderyeah.basic.util;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;

public class ClassUtils {
    /**
     * 从传入的包中获取所有的类的字节码对象:
     * 
     * @author LEIYU
     * @param pack
     * @return
     */
    public static Set<Class<?>> getClasses(String pack) {

        // 第一个class类的集合
        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
        // 是否循环迭代
        boolean recursive = true;
        // 获取包的名字 并进行替换
        String packageName = pack;
        String packageDirName = packageName.replace('.', '/');
        // 定义一个枚举的集合 并进行循环来处理这个目录下的things
        Enumeration<URL> dirs;
        try {
            dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
            // 循环迭代下去
            while (dirs.hasMoreElements()) {
                // 获取下一个元素
                URL url = dirs.nextElement();
                // 得到协议的名称
                String protocol = url.getProtocol();
                // 如果是以文件的形式保存在服务器上
                if ("file".equals(protocol)) {
                    // 获取包的物理路径
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    // 以文件的方式扫描整个包下的文件 并添加到集合中
                    findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return classes;
    }

    // 2.以文件的形式来获取包下的所有Class

    /**
     * 以文件的形式来获取包下的所有Class
     * 
     * @param packageName
     * @param packagePath
     * @param recursive
     * @param classes
     */
    public static void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive,
            Set<Class<?>> classes) {
        // 获取此包的目录 建立一个File
        File dir = new File(packagePath);
        // 如果不存在或者 也不是目录就直接返回
        if (!dir.exists() || !dir.isDirectory()) {
            // log.warn("用户定义包名 " + packageName + " 下没有任何文件");
            return;
        }
        // 如果存在 就获取包下的所有文件 包括目录
        File[] dirfiles = dir.listFiles(new FileFilter() {
            // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
            public boolean accept(File file) {
                return (recursive && file.isDirectory()) || (file.getName().endsWith(".class"));
            }
        });
        // 循环所有文件
        for (File file : dirfiles) {
            // 如果是目录 则继续扫描
            if (file.isDirectory()) {
                findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive,
                        classes);
            } else {
                // 如果是java类文件 去掉后面的.class 只留下类名
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    // 添加到集合中去
                    // classes.add(Class.forName(packageName + '.' +
                    // className));
                    // 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
                    classes.add(
                            Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    // log.error("添加用户自定义视图类错误 找不到此类的.class文件");
                    e.printStackTrace();
                }
            }
        }
    }

}
-------------本文结束感谢您的阅读-------------
you can reward me here. Thank you!

欢迎关注我的其它发布渠道