无Tomcat实现@Controller和@RequestMapping及HTTP服务

tomcat、undertown等中间件相信大家并不陌生,基于ServeletAPI进一步简化的SpringMVC以及SpringBoot可以更方便的通过注解的方式创建一个接口函数供HTTP调用,这个过程是如何实现的?今天鹏叔使用纯粹的Java 代码脱离任何ServletAPI进行Controller和RequestMapping的仿真,相信通过阅读今天的文章大家可以对Java的HTTP服务本质工作流程有进一步的理解,也会对反射和注解有进一步的认识。

准备工作

首先要准备一个纯粹的Java项目like this!

在这里插入图片描述

controller中需要准备的代码就是极简的Controller代码like this!

package com.leozhang.server.controller;


import com.leozhang.server.descriptions.Controller;
import com.leozhang.server.descriptions.RequestMapping;

@RequestMapping("/hello")
@Controller
public class HelloController {

    @RequestMapping("/index")
    public String index(String name,String age,String sex){

        System.out.println(1);
        System.out.println(name);
        System.out.println(age);
        System.out.println(sex);
        return "get param:name="+name+";age="+age+";sex="+sex;
    }

    @RequestMapping("/index1")
    public String index1(String id,String name){
        System.out.println(id);
        System.out.println(name);
        return "hello1";
    }
}

创建Controller注解

按照上面代码中编写的内容会发现与SpringMVC项目的Controller和RequestMapping注解使用方式大同小异,所以在创建Controller前,首先要确认的一件事情就是Controller本身并没有什么实际功能它本身的作用就是告诉服务器这个类将要按照Controller方式进行解析,服务器在初始化阶段会通过反射找到该类并缓存他的实例。

所以Controller注解的创建非常简单:

package com.leozhang.server.descriptions;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Controller {
}

创建成这样便可以使用了。

创建RequestMapping注解

RequestMapping注解主要的作用是将指定的URL片段与Java类绑定,用户在浏览器中访问对应路径时,会调用RequestMapping所标记的函数,这个就是访问的本质。所以该注解的本身就是用来保存一段URL片段而已。

package com.leozhang.server.descriptions;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Documented
public @interface RequestMapping {
    String value();
}

从服务启动到访问的流程

首先贴出Server.java类的完整代码,请仔细阅读。

package com.leozhang.server;

import com.leozhang.server.descriptions.Controller;
import com.leozhang.server.descriptions.RequestMapping;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.*;
import java.util.*;

public class Server {
//    缓存所有的控制器反射类
    static List<Class> controllers = new ArrayList<>();
//    声明Controller所归属的包名
    static String packageName = "com.leozhang.server.controller";
//    通过url关联反射类对象并缓存
    static Map<String,Class> mappedClasses = new HashMap<>();
//    通过url关联反射类的实例并缓存
    static Map<String,Object> mappedInstance = new HashMap<>();
//    通过url关联反射类的对应函数并缓存
    static Map<String,Method> mappedMethods = new HashMap<>();

    /**
     * 根据包名找到该包下被Controller注解标记的类
     * @param packageName
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public  static void  initControllers(String packageName) throws IOException, ClassNotFoundException {
        //获取包下的文件
        Enumeration<URL> dirs = Thread.currentThread().getContextClassLoader().getResources(packageName.replace(".", "/"));
        while (dirs.hasMoreElements()){

            URL url = dirs.nextElement();
            //获取文件数组
            String[] file = new File(url.getFile()).list();
            //通过文件数量创建反射类数组
            Class[] classList = new Class[file.length];
            for (int i = 0; i < file.length; i++) {
                classList[i] = Class.forName(packageName + "." + file[i].replaceAll("\\.class", ""));
            }
            //找到Controller标记的类并保存到controllers中
            Arrays.stream(classList).forEach(classItem -> {
                Annotation res = classItem.getAnnotation(Controller.class);
               if(res!=null){
                   controllers.add(classItem);
               }
            });
        }
    }

    /**
     * 通过缓存的反射类找到类中标记RequestMapping的函数并组装URL与函数的对应关系
     * @param controllerClasses
     */
    public static void mapControllers(List<Class> controllerClasses){
        //遍历反射类
        controllerClasses.forEach(controllerClass -> {
            try {
//                获取注解对象
                RequestMapping requestMapping = (RequestMapping) controllerClass.getAnnotation(RequestMapping.class);
//                获取标记在类上的URL片段
                String topUrl = requestMapping.value();
//                获取反射类的所有函数
                Method[] methods = controllerClass.getDeclaredMethods();
                Arrays.stream(methods).forEach(method -> {
//                    获取标记了RequestMapping的函数对象
                    RequestMapping methodMapping = method.getAnnotation(RequestMapping.class);
                    if(method!=null){
//                        组装完整的访问路径
                        String fullUrl = topUrl+methodMapping.value();
//                        关联URL和反射类
                        mappedClasses.put(fullUrl,controllerClass);
//                        关联URL和对应的函数
                        mappedMethods.put(fullUrl,method);
//                        输出关联日志
                        System.out.println("mapped "+fullUrl+" to "+method+" with "+controllerClass);
                        try {
//                            关联URL和反射类的实例防止频繁访问的对象创建开销过大
                            Object obj = controllerClass.getDeclaredConstructor().newInstance();
                            mappedInstance.put(fullUrl,obj);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }

        });
    }

    /**
     * 启动服务函数
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
//        记录启动时间
        long begin = System.currentTimeMillis();
//        初始化控制器的反射类
        Server.initControllers(Server.packageName);
//        映射URL和对象函数
        Server.mapControllers(Server.controllers);
//        创建8000端口的HTTP服务
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
//        根据函数和URL的关系创建不同URL的HTTP监听器
        mappedMethods.forEach((url,method) -> {
            Object instance = mappedInstance.get(url);
            server.createContext(url, new RequestHandler(url,method,instance));
        });

        server.setExecutor(null); // creates a default executor
//        启动服务
        server.start();
//        输出启动时间
        System.out.println("server started in "+(System.currentTimeMillis()-begin)+"ms");
    }
//    http请求监听器,当用户访问Controller中定义的URL时就会触发该类的handle函数执行
    static class RequestHandler implements HttpHandler{
        String url;
        Method method;
        Object instance;
//        初始化HTTP监听器并记录关联函数的对象
        public RequestHandler(String url, Method method, Object instance){
            this.url = url;
            this.method = method;
            this.instance = instance;
        }
//        get参数转换器
        public Map<String,String> getQueryString(String qs){
            if(qs == null){
                return null;
            }
            Map<String ,String> queryMap = new HashMap<>();
            if(qs.contains("&")){
                String[] keyValueArr = qs.split("&");
                Arrays.stream(keyValueArr).forEach(keyValue -> {
                    String key = keyValue.split("=")[0];
                    String value = keyValue.split("=")[1];
                    queryMap.put(key,value);
                });
            }else{
                String key = qs.split("=")[0];
                String value = qs.split("=")[1];
                queryMap.put(key,value);
            }
            return queryMap;
        }
        @Override
        public void handle(HttpExchange t) throws IOException {
            try {
//              获取该请求所调用函数的参数信息
                Parameter[] params = this.method.getParameters();
//              创建装有参数的容器,在调用对应method时所传入的参数数组
                List<Object> paramList = new ArrayList<>();
//                获取请求路径
                URI uri = t.getRequestURI();
//                获取QueryString部分字符
                String qs = uri.getQuery();
//                System.out.println(qs);
//                将字符参数整理成Map对象
                Map<String, String> qsMap = getQueryString(qs);
//                System.out.println(qs);

                if(qsMap!=null){
                    //如果传递参数就将其匹配Controller中对应method的参数名并装载结果到参数数组
                    Arrays.stream(params).forEach(param -> {
                        paramList.add(qsMap.get(param.getName()));
                    });
                }else{
//                    如果没有传递任何参数则初始化空数组,防止反射调用函数不执行
                    Arrays.stream(params).forEach(item -> {
                        paramList.add(null);
                    });
                }
//                反射调用URL对应的函数
                String res = (String) this.method.invoke(this.instance,paramList.toArray());
//                设置返回类型
                t.getResponseHeaders().add("content-type","text/html;charset=utf-8");
//                发送响应体
                t.sendResponseHeaders(200, res.length());
//              写入返回数据
                OutputStream os = t.getResponseBody();
                os.write(res.getBytes());
                os.close();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

}

相信阅读代码后,所有人都会大改明白平时我们所使用的Spring项目启动的大概流程了,在启动服务的过程中控制台上会输出如下日志:

/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=55112:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/zhangyunpeng/Documents/IdeaProjects/server-test/out/production/server-test com.leozhang.server.Server
mapped /test/name to public java.lang.String com.leozhang.server.controller.TestController.getName() with class com.leozhang.server.controller.TestController
mapped /hello/index1 to public java.lang.String com.leozhang.server.controller.HelloController.index1(java.lang.String,java.lang.String) with class com.leozhang.server.controller.HelloController
mapped /hello/index to public java.lang.String com.leozhang.server.controller.HelloController.index(java.lang.String,java.lang.String,java.lang.String) with class com.leozhang.server.controller.HelloController
server started in 69ms

该日志的目的是让开发者明白,服务器在启动的过程中,已经自动的将controller包下标记了@Controller注解的类实例化,并将每个对象的URL和函数的映射关系缓存到了全局。

接下来服务器做的事儿就是监听RequestMapping定义的URL路径访问,并在访问时找到该URL路径所对应的函数,并动态的将需要的参数传递给函数本身,这些过程都不是开发者所需要操作的。

这也是为什么很多人看不起CRUD工程师的原因,因为现今主流的服务端框架本身已经将大部分能自动化处理的流程交给服务器处理了,开发者只需要对服务器描述下一部分的业务如何进行即可。

运行示例

拿HelloController的代码举例,当程序运行后,可以在浏览器中直接访问

http://localhost:8000/hello/index?name=a&sex=b&age=123123

这样在界面上会看到

在这里插入图片描述

控制台会输出如下内容:

/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62322:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/zhangyunpeng/Documents/IdeaProjects/server-test/out/production/server-test com.leozhang.server.Server
mapped /test/name to public java.lang.String com.leozhang.server.controller.TestController.getName() with class com.leozhang.server.controller.TestController
mapped /hello/index1 to public java.lang.String com.leozhang.server.controller.HelloController.index1(java.lang.String,java.lang.String) with class com.leozhang.server.controller.HelloController
mapped /hello/index to public java.lang.String com.leozhang.server.controller.HelloController.index(java.lang.String,java.lang.String,java.lang.String) with class com.leozhang.server.controller.HelloController
server started in 97ms
1
a
123123
b

补充说明

通过反射获取函数的参数名称部分采用的是JDK8的编译方式,需要在开发工具中进行简单的设置,以IDEA为例,需要在perference中找到如下界面并添加-parameters,否则获取的参数名称会变成arg0、arg1…

如图:

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340