一. 注解介绍
注解Annotation是一种引用数据类型,编译之后也是生成.class类型的java文件
语法:修饰符 @interface 注解类型名
- 注解可以出现在类上、方法上、属性上、甚至注解上等…
- JDK中内置的注解@Override(复写)、@SuppressWarnings(忽略编译器的警告)等
元注解
元注解就是用来修饰注解的,是注解上的注解;常见的元注解有@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();
}
}
}
}
}