项目开发中遇到一个关于RBAC中数据入库问题。权限主要包含页面权限,接口权限及数据权限。页面权限一般来讲是需要手动添加的,而接口权限则是通过swagger导入,或者手工录入,这样存在很多问题。比如接口书写不规范,运营看不懂。每次迭代,都需要手动角色绑定权限,这样人工错误率非常高。页面权限问题是没法解决的,但是我们可以解决接口权限问题。
目标
- 期望编译时自动生成接口权限数据
- 期望编译时自动生成角色绑定权限数据
- 通过自动化的方式,减少人工操作出错。
解决方案
接口声明可能会是下面这样
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名称
- 接口实现的功能
- 概要描述
- 支持的角色列表
基于上面的需求,我们需要声明三个注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@Retention(RetentionPolicy.SOURCE) @Target(ElementType.TYPE) @Documented public @interface ApplicationDoc {
String context() default "";
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
|
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; }
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
|
@Retention(RetentionPolicy.SOURCE) @Target(ElementType.TYPE) @Documented @Inherited public @interface ControllerDoc {
String context() default "";
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
|
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; }
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 {
String operation();
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
|
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); }
@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仓库,还是比较好用的。文档中有很多现有项目的约束,后续可以不断完善。