菜单开关

周梦康 发表于 2022-05-14 23 次浏览 标签 : 原创《Java Web 最佳学习路径》

上节回顾

每写一个 servlet 就需要到 tomcat 里配置一次 servlet 的全路径和对应的路由,显得不是那么敏捷。更重要的一点,当我们在做一些项目的时候,有很多公共逻辑,如果独立的 servlet 则需要每次都要显性的引入,比如私有 api 接口的鉴权。

当我们配置的/*,所以我们访问任何地址 http://localhost:8080/abcdefg 都是一样的结果,这样就把路由的功能后置到 web 框架中来了。下面我们一起实现一个简单的路由逻辑

不能生产环境使用,一些空指针未处理,一些复杂路由不考虑,仅做演示

思路

  1. 修改上节的 DemoServletDispatcherServlet
  2. DispatcherServlet 初始化的时候,根据指定的控制器包名扫描路由和控制器方法的映射关系,并且存放在路由表mappingRegistry
  3. 当访问一个已经定义的路由时,则在路由表mappingRegistry中查询到对应的方法,然后通过反射执行对应的方法
  4. 最后新增两个 controller 做测试

代码细节

代码目录结构

.
└── net
    └── mengkang
        ├── DispatcherServlet.java
        ├── MkTomcat.java
        ├── RequestMapping.java
        ├── RequestMethod.java
        └── controller
            ├── IndexController.java
            └── UserController.java

3 directories, 6 files

MkTomcat 修改一行

public class MkTomcat {

    public static void main(String[] args) throws LifecycleException {

        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);

        Context ctx = tomcat.addContext("", new File(".").getAbsolutePath());

-       Tomcat.addServlet(ctx, "MkServlet", new DemoServlet());
+       Tomcat.addServlet(ctx, "MkServlet", new DispatcherServlet("net.mengkang.controller"));

        ctx.addServletMappingDecoded("/*", "MkServlet");

        tomcat.start();
        tomcat.getServer().await();
    }
}
public class DispatcherServlet extends HttpServlet {

    private String controllerScan;

    private Map<String, Method> mappingRegistry = new HashMap<>();

    public DispatcherServlet(String controllerScan) {
        this.controllerScan = controllerScan;
        initMappingRegistry();
    }

    private void initMappingRegistry() {
        ClassLoader classLoader = this.getClass().getClassLoader();
        URL url = classLoader.getResource(controllerScan.replace(".", "/"));
        File file = new File(url.getFile());

        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File controller : files) {
                String fileName = controller.getAbsolutePath();
                String className = fileName.substring(fileName.indexOf(controllerScan.replace(".", "/")), fileName.indexOf(".class")).replace("/", ".");

                try {
                    Class<?> clazz = classLoader.loadClass(className);
                    Method[] methods = clazz.getDeclaredMethods();
                    for (Method method : methods) {
                        System.out.println(fileName + " " + method.getName());

                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);

                        if (requestMapping == null) {
                            continue;
                        }

                        mappingRegistry.put(requestMapping.method().name() + requestMapping.value(), method);
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String requestInfo = RequestMethod.GET.name() + request.getRequestURI();

        System.out.println(requestInfo);

        if (mappingRegistry.containsKey(requestInfo)) {
            Method method = mappingRegistry.get(requestInfo);

            try {
                Object res = method.invoke(method.getDeclaringClass().newInstance());

                PrintWriter printWriter = response.getWriter();
                printWriter.println(res.toString());

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }

}

其他文件,RequestMapping注解、RequestMethod枚举和两个控制器

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {

    String value();

    RequestMethod method();
}
public enum RequestMethod {
    GET,
    HEAD,
    POST,
    PUT,
    PATCH,
    DELETE,
    OPTIONS,
    TRACE;

    private RequestMethod() {
    }
}
public class IndexController {

    @RequestMapping(value = "/abc", method = RequestMethod.GET)
    public String index() {
        return "hello world";
    }

    /**
     * 没注解不会被扫描注册到路由表
     * @return
     */
    public String test() {
        return "test";
    }
}

public class UserController {

    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public String detail() {
        return "user detail";
    }
}

启动应用,访问 http://localhost:8080/abchttp://localhost:8080/user 都符合预期。

总结

核心主要是DispatcherServlet 代码,initMappingRegistry 中主要依赖 classLoader 找到编译之后运行时的文件,然后推导出控制器方法名和方法名;doGet则根据路由表去反射调用。

代码不多,比较适合下载运行学习,公众号回复 001 获取代码地址

原创博客,如需转载,请联系 i@mengkang.net
链接://mengkang.net/1516.html

👇 下面是我的公众号,高质量的博文我会第一时间同步到公众号,给个关注吧!

评论列表