一. 注解介绍

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

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

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

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

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

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

      1
      2
      3
      4
      5
      6
      7
      Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型
      这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention搭配 RententionPolicy使用
      RetentionPolicy有3个值:CLASSRUNTIMESOURCE

      @Retention(RetentionPolicy.SOURCE):注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
      @Retention(RetentionPolicy.CLASS):注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
      @Retention(RetentionPolicy.RUNTIME):注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
    • @Documented:加了这个注解的注解,在生成文档的时候,可以在文档中显示出来

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Inherited
      public @interface A{
      }

      @A
      class B(){
      }

      class C extends B{
      }

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

1. 定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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. 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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
    2
    3
    4
    5
    6
    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 - 这个对象会在服务器启动的时候自动生成,而且是唯一一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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. 启动类

1
2
3
4
5
6
7
8
9
10
@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
3
4
//1.找包 - 找类 - 找方法 - 找注解
//2.解析这个注解拿到:sn,name
//3.当前方法的url地址
//4.创建一个Permisson对象 - 添加到数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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();
}
}
}
}

}