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";
}
}
按照上面代码中编写的内容会发现与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注解主要的作用是将指定的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…
如图: