
javalin实现类似Springboot批量扫描controller注册action的效果
背景
javalin 太轻量,没有像 springboot 那样的 controller 扫描机制,要注册一个路由,需要手动调用方法来添加,如下
app.get("/output", ctx -> {
// some code
ctx.json(object);
});
app.post("/input", ctx -> {
// some code
ctx.status(201);
});
如果我们要注册的路由很多,而且在不同的类中定义,一个一个添加就很不方便
所以我们可以定义 @Controller、@RequestMapping 两个注解,再做一个工具类,扫描某个包下的所有类,找到标注了 @Controller 的类,然后找到标注了 @RequestMapping 的方法,最后将路由绑定到这个类的这个方法上
话不多说,马上开始!
开始编码
项目结构
org.kk
|-annotation/
|-Controller.java
|-RequestMapping.java
|-controller/
|-TestController.java
|-util/
|-ControllerScanUtil.java
|-Main.java
定义注解
在 org.kk.annotation 包,定义 @Controller、@RequestMapping 两个注解
org.kk.annotation.Controller
package org.kk.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited
public @interface Controller {
}
org.kk.annotation.RequestMapping
package org.kk.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Inherited
public @interface RequestMapping {
// 路由
String value();
// 请求方法,不填则监听所有方法
String method() default "";
}
定义 Controller 扫描工具类
org.kk.util.ControllerScanUtil
package org.kk.util;
import io.javalin.config.JavalinConfig;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.example.annotation.Controller;
import org.example.annotation.RequestMapping;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import static io.javalin.apibuilder.ApiBuilder.*;
@Slf4j
public class ControllerScanUtil {
/**
* 定义支持的请求方法
*/
public static final List<String> METHOD_LIST = Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD");
/**
* 扫描指定包下所有标注了@Controller注解的类
*
* @param config javalin配置
* @param basePackage 扫描的包路径
*/
@SneakyThrows
public static void buildRequestMapping(JavalinConfig config, String basePackage) {
List<Class<?>> controllerClasses = scanControllers(basePackage);
if (controllerClasses.isEmpty()) {
return;
}
Map<String, UrlBinding> urlMapping = new HashMap<>();
for (Class<?> clazz : controllerClasses) {
Object controller = null;
Method[] methods = clazz.getMethods();
for (Method action : methods) {
if (action.isAnnotationPresent(RequestMapping.class)) {
if (controller == null) {
controller = clazz.newInstance();
}
RequestMapping requestMapping = action.getAnnotation(RequestMapping.class);
String path = requestMapping.value();
String requestMethod = requestMapping.method();
if (requestMethod == null || requestMethod.trim().isEmpty()) {
for (String s : METHOD_LIST) {
String key = s + '|' + path;
urlMapping.put(key, UrlBinding.builder()
.url(path)
.method(s)
.controller(controller)
.action(action)
.build());
}
} else {
requestMethod = requestMethod.toUpperCase();
if (!METHOD_LIST.contains(requestMethod)) {
throw new IllegalArgumentException("Unsupported request method: " + requestMethod);
}
String key = requestMethod + '|' + path;
urlMapping.put(key, UrlBinding.builder()
.url(path)
.method(requestMethod)
.controller(controller)
.action(action)
.build());
}
}
}
}
if (urlMapping.isEmpty()) {
return;
}
urlMapping.forEach((k, v) -> {
log.info("listen [{}] {}", v.getMethod(), v.getUrl());
});
config.router.apiBuilder(() -> {
urlMapping.forEach((k, v) -> {
String requestMethod = v.getMethod();
String path = v.getUrl();
Object controller = v.getController();
Method action = v.getAction();
switch (requestMethod) {
case "GET":
get(path, (ctx) -> action.invoke(controller, ctx));
break;
case "POST":
post(path, (ctx) -> action.invoke(controller, ctx));
break;
case "PUT":
put(path, (ctx) -> action.invoke(controller, ctx));
break;
case "DELETE":
delete(path, (ctx) -> action.invoke(controller, ctx));
break;
case "PATCH":
patch(path, (ctx) -> action.invoke(controller, ctx));
break;
case "HEAD":
head(path, (ctx) -> action.invoke(controller, ctx));
break;
default:
throw new IllegalArgumentException("Unsupported request method: " + requestMethod);
}
});
});
}
/**
* 扫描指定包下所有标注了@Controller注解的类
*
* @param basePackage 要扫描的基础包名
* @return 标注了@Controller注解的类的列表
*/
public static List<Class<?>> scanControllers(String basePackage) {
List<Class<?>> controllerClasses = new ArrayList<>();
try {
// 获取类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 将包名转换为路径
String path = basePackage.replace('.', '/');
System.out.println("path: " + path);
String jarPath = ControllerScanUtil.class.getProtectionDomain().getCodeSource().getLocation().getPath();
if (jarPath.endsWith(".jar")) {
// 如果是 JAR 包,使用 JarFile 读取资源
try (JarFile jarFile = new JarFile(URLDecoder.decode(jarPath, "UTF-8"))) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.startsWith(path) && entryName.endsWith(".class")) {
String className = entryName.replace('/', '.').substring(0, entryName.length() - 6);
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(Controller.class)) {
controllerClasses.add(clazz);
}
}
}
}
} else {
// 获取指定路径下的所有资源
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
// 将资源转换为文件
File directory = new File(resource.getFile());
if (directory.exists()) {
// 递归扫描目录下的所有类
findClasses(directory, basePackage, controllerClasses);
}
}
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return controllerClasses;
}
/**
* 递归查找指定目录下的所有类,并筛选出标注了@Controller注解的类
*
* @param directory 要扫描的目录
* @param packageName 当前包名
* @param controllerClasses 存储标注了@Controller注解的类的列表
* @throws ClassNotFoundException 如果找不到类
*/
private static void findClasses(File directory, String packageName, List<Class<?>> controllerClasses) throws ClassNotFoundException {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 递归扫描子目录
findClasses(file, packageName + "." + file.getName(), controllerClasses);
} else if (file.getName().endsWith(".class")) {
// 获取类名
String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
// 加载类
Class<?> clazz = Class.forName(className);
// 检查类是否标注了@Controller注解
if (clazz.isAnnotationPresent(Controller.class)) {
controllerClasses.add(clazz);
}
}
}
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class UrlBinding {
private String url;
private String method;
private Object controller;
private Method action;
}
}
马上使用
定义 TestController 测试
定义 TestController,写两个测试方法
org.kk.controller.TestController
package org.kk.controller;
import io.javalin.http.Context;
import org.example.annotation.Controller;
import org.example.annotation.RequestMapping;
@Controller
public class TestController {
/**
* method为GET,显式绑定GET方法
* @param ctx
*/
@RequestMapping(value = "/test001", method = "GET")
public void test001(Context ctx) {
ctx.result("这是test001");
}
/**
* method为空,绑定所有请求方法
* @param ctx
*/
@RequestMapping(value = "/test002", method = "")
public void test002(Context ctx) {
ctx.result("这是test002");
}
}
定义启动类
org.kk.Main
package org.kk;
import io.javalin.Javalin;
import org.kk.util.ControllerScanUtil;
public class Main {
public static void main(String[] args) {
Javalin.create(config -> {
ControllerScanUtil.buildRequestMapping(config, "org.kk.controller");
}).start(8686);
}
}
启动!
启动完之后,在控制台可以看到路由的打印输出
[main] INFO org.kk.util.ControllerScanUtil - listen [PUT] /test002
[main] INFO org.kk.util.ControllerScanUtil - listen [GET] /test002
[main] INFO org.kk.util.ControllerScanUtil - listen [DELETE] /test002
[main] INFO org.kk.util.ControllerScanUtil - listen [GET] /test001
[main] INFO org.kk.util.ControllerScanUtil - listen [PATCH] /test002
[main] INFO org.kk.util.ControllerScanUtil - listen [POST] /test002
[main] INFO org.kk.util.ControllerScanUtil - listen [HEAD] /test002
[main] INFO io.javalin.Javalin - Starting Javalin ...
...
...
如果不想打印出路由,那就在 ControllerScanUtil 类里,将下面这几行注释掉即可
urlMapping.forEach((k, v) -> {
log.info("listen [{}] {}", v.getMethod(), v.getUrl());
});
可以尝试请求
curl --request GET http://localhost:8686/test001
curl --request GET http://localhost:8686/test002
curl --request POST http://localhost:8686/test002
curl --request PUT http://localhost:8686/test002
本文是原创文章,采用 CC 4.0 BY-SA 协议,完整转载请注明来自 KK元空间
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果