使用AbstractProcessor自动生成角色权限数据

项目开发中遇到一个关于RBAC中数据入库问题。权限主要包含页面权限,接口权限及数据权限。页面权限一般来讲是需要手动添加的,而接口权限则是通过swagger导入,或者手工录入,这样存在很多问题。比如接口书写不规范,运营看不懂。每次迭代,都需要手动角色绑定权限,这样人工错误率非常高。页面权限问题是没法解决的,但是我们可以解决接口权限问题。

目标

  1. 期望编译时自动生成接口权限数据
  2. 期望编译时自动生成角色绑定权限数据
  3. 通过自动化的方式,减少人工操作出错。

解决方案

接口声明可能会是下面这样

1
2
3
4
5
@Operation(summary = "新增管理员用户(管理端)")
@PostMapping(value = "/add")
public TpsResult add(@RequestBody AdminAddOrUpdateDTO adminAddOrUpdateDTO){
return adminService.add(adminAddOrUpdateDTO);
}

会使用到Spring的接口注解,但是Spring所有的AOP操作都是基于运行时的,如下图所示

所以Spring并不能满足我们的要求,同样Swagger也是如此

从上面可以看出,我们无法使用现有的注解进行自动生成数据。只能自定义注解实现编译时生成接口权限数据。Java中提供了AbstractProcessor实现编译时对代码进行扫描并回调,在回调中你可以执行自定义操作。

代码实现

我们将获取到的数据存储为JSON文件,供程序员直接读取并写库。对于接口权限需要的字段,下面简单整理一些

  • 接口地址
  • 接口方法,POST,GET等
  • 模块名称
  • Controller名称
  • 接口实现的功能
  • 概要描述
  • 支持的角色列表

基于上面的需求,我们需要声明三个注解

  • 模块级别注解,获取模块名称,上下文等

  • Controller级别注解,获取Controller名称,上下文等

  • 方法级别注解,获取接口实现的功能,支持的角色,概要描述我们期望从Swagger中获取

    模块级别注解及实现

  • 注解声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author Jon
* @desc 注解于应用上的文档处理器
* @date 2022年06月11日 8:42
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Documented
public @interface ApplicationDoc {
/**
* servlet:
* context-path: /tps-uac
* @return context-path
*/
String context() default "";

/**
* 模块名称
* @return 指定模块名称
*/
String tag() default "";
}
  • 注解处理器实现
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
/**
* @author 姜陶
* @date 2022/6/10 11:35
* @describe 角色文档处理器
**/
public class ApplicationDocProcessor extends AbstractProcessor {

private Messager messager;
private Elements elementUtils;
private Filer filer;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elementUtils = processingEnv.getElementUtils();
}

// 处理过程
@SneakyThrows
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
ApplicationDoc applicationDoc = element.getAnnotation(ApplicationDoc.class);
putAppHashMap(element, applicationDoc);
}
}
return true;
}

// 将获取到的当前的应用存入list,供后续拼接url
public void putAppHashMap(Element element, ApplicationDoc applicationDoc) throws IOException {
DictDTO dictDTO = DictDTO.builder()
.context(applicationDoc.context())
.dict(element.toString())
.tag(applicationDoc.tag())
.build();
DocUtil.APP.add(dictDTO);
}

@Override
public Set<String> getSupportedOptions() {
return super.getSupportedOptions();
}

// 处理的注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new HashSet<>();
annotationTypes.add(ApplicationDoc.class.getCanonicalName());
return annotationTypes;
}

// 支持的版本
@Override
public SourceVersion getSupportedSourceVersion() {
if (SourceVersion.latest().compareTo(SourceVersion.RELEASE_8) > 0) {
return SourceVersion.latest();
}
return SourceVersion.RELEASE_8;
}
}

Controller级别注解及实现

  • 注解声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author Jon
* @desc Controller描述
* @date 2022年06月11日 8:48
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface ControllerDoc {
/**
* servlet:
* context-path: /tps-uac
* @return context-path
*/
String context() default "";

/**
* 模块名称
* @return 指定模块名称
*/
String tag() default "";

}
  • 注解处理器实现
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
/**
* @author 姜陶
* @date 2022/6/10 11:35
* @describe 角色文档处理器
**/
public class ControllerDocProcessor extends AbstractProcessor {

private Messager messager;
private Elements elementUtils;
private Filer filer;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elementUtils = processingEnv.getElementUtils();
}

@SneakyThrows
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
ControllerDoc roleDoc = element.getAnnotation(ControllerDoc.class);
putControllerHashMap(element,roleDoc);
}
}
return true;
}

// 将Controller注解数据存入HashMap,供后续拼接url
public void putControllerHashMap(Element element, ControllerDoc controllerDoc) throws IOException {
DictDTO dictDTO = DictDTO.builder()
.context(controllerDoc.context())
.dict(element.toString())
.tag(controllerDoc.tag())
.build();
DocUtil.CONTROLLER.put(element.toString(), dictDTO);
}


@Override
public Set<String> getSupportedOptions() {
return super.getSupportedOptions();
}


@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new HashSet<>();
annotationTypes.add(ControllerDoc.class.getCanonicalName());
return annotationTypes;
}

@Override
public SourceVersion getSupportedSourceVersion() {
if (SourceVersion.latest().compareTo(SourceVersion.RELEASE_8) > 0) {
return SourceVersion.latest();
}
return SourceVersion.RELEASE_8;
}
}

方法级别注解及实现

  • 注解声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface RolePermissionDoc {

/**
* 操作 CRUD
* @return CRUD
*/
String operation();

/**
* 支持哪些角色操作
* @return 角色数组
*/
RoleEnum[] roles();
}
  • 方法处理器实现
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
/**
* @author 姜陶
* @date 2022/6/10 11:35
* @describe 角色文档处理器
**/
public class RoleDocProcessor extends AbstractProcessor {
public static final String DOCUMENT = "rp_doc";
public static final String PERMISSION_FILE_NAME = "PERMISSION.json";
public static final String STRING_HTTP_POST = "PostMapping";
public static final String STRING_HTTP_GET = "GetMapping";
public static final String STRING_HTTP_DELETE = "DeleteMapping";
public static final String STRING_HTTP_PUT = "PutMapping";
public static final String STRING_DOC = "Operation";
private Messager messager;
private Elements elementUtils;
private Filer filer;

@SneakyThrows
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elementUtils = processingEnv.getElementUtils();
String pathStr = System.getProperty("user.dir")+ File.separator + DOCUMENT;
Path dict = Paths.get(pathStr);
//deleteRpDoc(dict);
}

/**
* 删除目录及文件
* @param dict
*/
@SneakyThrows
public void deleteRpDoc(Path dict){
Files.walkFileTree(dict, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}

@SneakyThrows
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.size()>0) {
messager.printMessage(Diagnostic.Kind.NOTE, "开始生成角色权限文档...");
}
for (TypeElement annotation : annotations) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
RolePermissionDoc roleDoc = element.getAnnotation(RolePermissionDoc.class);
generateRoleJson(element, roleDoc);
}
}
if (annotations.size()>0) {
messager.printMessage(Diagnostic.Kind.NOTE, "角色权限文档生成结束...");
}
return true;
}

// 生成角色权限对应数据
public void generateRoleJson(Element element, RolePermissionDoc roleDoc) throws IOException {
// 当前所属类全名
String pathStr = System.getProperty("user.dir")+ File.separator + DOCUMENT;
Path dict = Paths.get(pathStr);
if (!Files.exists(dict)) {
Files.createDirectory(dict);
}
Path pp = Paths.get(pathStr+ File.separator +PERMISSION_FILE_NAME);
if (!Files.exists(pp)) {
Files.createFile(pp);
}
String clz = element.getEnclosingElement().toString();
HttpMethod httpMethod = HttpMethod.GET;;
String url = null;
String summary = null;
// 当前方法上的所有注解
for (AnnotationMirror annotationMirror: element.getAnnotationMirrors()){
String mirror = annotationMirror.toString();
if (!mirror.contains(clz)){
if (mirror.contains(STRING_DOC)){
summary = DocUtil.matchCut(mirror);
} else if (mirror.contains(STRING_HTTP_POST)){
httpMethod = HttpMethod.POST;
url = DocUtil.matchCut(mirror);
}else if (mirror.contains(STRING_HTTP_GET)){
httpMethod = HttpMethod.GET;
url = DocUtil.matchCut(mirror);
}else if (mirror.contains(STRING_HTTP_DELETE)){
httpMethod = HttpMethod.DELETE;
url = DocUtil.matchCut(mirror);
}else if (mirror.contains(STRING_HTTP_PUT)){
httpMethod = HttpMethod.PUT;
url = DocUtil.matchCut(mirror);
}else {
httpMethod = HttpMethod.GET;
}
}
}
DictDTO app = DocUtil.APP.get(0);
DictDTO controller = DocUtil.CONTROLLER.get(clz);
StringBuilder roleUrl = new StringBuilder();
roleUrl.append(app.getContext());
roleUrl.append(controller.getContext());
if (url!=null && !url.startsWith("/")){
url = "/"+url;
}
roleUrl.append(url);
RoleDocDTO docDTO = RoleDocDTO.builder()
.module(app.getTag())
.service(controller.getTag())
.operation(roleDoc.operation())
.method(httpMethod)
.summary(summary)
.url(roleUrl.toString())
.build();
generatePermissionJson(docDTO,pp);
for (RoleEnum role : roleDoc.roles()){
Path path = Paths.get(pathStr + File.separator +role.getRole()+".json");
if (!Files.exists(path)) {
Files.createFile(path);
}
ArrayNode arrayNode =JsonUtil.getInstance().arrayNode();
if (Files.lines(path).findFirst().isPresent()){
arrayNode = JsonUtil.getInstance().strToArrayNode(new String(Files.readAllBytes(path)));
}
arrayNode.add(JsonUtil.getInstance().objToNode(docDTO));
Files.write(path, JsonUtil.getInstance().str(arrayNode)
.getBytes(StandardCharsets.UTF_8));
}
}

// 生成权限数据
public void generatePermissionJson(RoleDocDTO permissions,Path permissionStr) throws IOException {
ArrayNode arrayNode =JsonUtil.getInstance().arrayNode();
if (Files.lines(permissionStr).findFirst().isPresent()){
arrayNode = JsonUtil.getInstance().strToArrayNode(new String(Files.readAllBytes(permissionStr)));
}
arrayNode.add(JsonUtil.getInstance().objToNode(permissions));
Files.write(permissionStr, JsonUtil.getInstance().str(arrayNode)
.getBytes(StandardCharsets.UTF_8));
}

@Override
public Set<String> getSupportedOptions() {
return super.getSupportedOptions();
}


@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new HashSet<>();
annotationTypes.add(RolePermissionDoc.class.getCanonicalName());
return annotationTypes;
}

@Override
public SourceVersion getSupportedSourceVersion() {
if (SourceVersion.latest().compareTo(SourceVersion.RELEASE_8) > 0) {
return SourceVersion.latest();
}
return SourceVersion.RELEASE_8;
}
}

最后我们需要在resource目录下创建META-INF/services文件夹,并创建javax.annotation.processing.Processor文件,将刚才编写的注解处理器全名放在文件下,如果希望有序,则按照想要的顺序依次添加即可。

测试


下面是生成的文件,包含了权限文档,不同的角色权限文档,如下图所示

复制json内容到bejson.com

现在我们可以通过读取,权限json文档导入权限表,角色及权限接口文档导入角色权限关联表,也可以启动开启任务执行即可,再也不用人工操作了。

总结

本文使用AbstractProcessor生成了角色权限文档数据,如果需要生成Java代码,可以参考
javapoet仓库,还是比较好用的。文档中有很多现有项目的约束,后续可以不断完善。

作者

Labradors

发布于

2022-06-12

更新于

2022-06-12

许可协议

评论