背景

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