自己写一个mvc框架吧(四)
写一个请求的入口,以及初始化框架
上一章写了获取方法的入参,并根据入参的参数类型进行数据转换。这时候,我们已经具备了通过反射调用方法的一切必要条件。现在我们缺少一个http请求的入口,就是一个servlet。现在我们开始写吧~
在这一章我们要做的事情有
定义一个配置文件,用来描述什么样的请求映射到哪一个class的哪一个方法上面。
在servlet初始化后,根据上面定义的配置文件加载mvc框架。
在一个http请求进入后,根据其请求路径,找到相应的方法,获取参数,使用反射执行该方法。
得到方法的执行结果后,先以json的形式在浏览器显示出来。
这一步是视图层的功能,先这样写,之后在写各种视图控制器。
现在开始写吧
定义配置文件
这里的配置不一定就必须是一个xml, json,yaml... 之类的文件,也可以是注解的形式。区别就只是在加载框架的时候根据不同的形式进行解析就好了。这里为了写起来方便,就先定义一个json的配置文件(因为json的文件用起来比较方便)。
着这个配置文件中我们需要定义一些参数,这些参数需要满足我们将一个http请求映射到一个方法上的需求。我是这样定义的:
{ "annotationSupport": false, "mapping": [ { "url": "/index", "requestType": [ "get" ], "method": "index", "objectClass": "com.hebaibai.demo.web.IndexController", "paramTypes": [ "java.lang.String" ] } ]}
下面说一下各个属性是干啥用的:
1:annotationSupport:用来描述有没有开启注解的支持,现在还没有写,就给了一个false。
2:mapping:用来描述映射关系的数据,是一个数组的类型。一个对象表示一个映射关系。
3:url:http请求的地址,表示这个映射关系对应的是哪一个请求地址。
4:requestType:这个映射支持的请求类型,数组的形式。说明一个方法支持多种请求方式。
5:objectClass:这个映射一定的是哪一个java对象。
6:method:这个映射关系对应的objectClass中的方法名称。
7:paramTypes:方法的入参类型,这里是一个数组,顺序要和定义的方法中的入参顺序相一致。定义这个参数是因为在通过反射找到一个一个Method的时候需要有两个参数,一是方法名称,另一个就是入参类型。所以这两个是必不可少的。
这里的配置说实话看起来有点复杂,用起来也不是很方便。比如在修改一个方法入参的时候,如果修改了参数类型,就要修改对应的配置。这里以后可以做一些简化处理,比如使用注解的形式,这样就会方便很多。但是现在是在设计并实现的阶段,可以把所有的配置按照最复杂的形式来做,完成功能之后再进行优化,可以添加一些全局的默认配置,这样就可以减少配置文件的编写。
上面的配置文件写完了,开始写怎样加载这个配置文件,并初始化这个mvc框架。
根据约定获取配置文件名称
因为请求的入口我用的是servlet,每一个servlet都需要配置 一个servlet-name,所以我们可以约定配置文件的名称就是就是servlet-name的名称后加上”.json“。例如我定义一个servlet:
mvc com.hebaibai.amvc.MvcServlet mvc /*
这时,配置文件的名称就是mvc.json。那么怎么做呢? 我们这么写:
//先定义一个servletpublic class MvcServlet extends HttpServlet { //重写其中的方法 @Override public void init(ServletConfig config) { //执行父类的init方法 super.init(config); //获取servlet的名称 String servletName = config.getServletName(); //接下来,就可以写别的东西了 }}
在上面的代码中,我只取到了servlet-name,还没有开始读取配置文件。因为我认为读取配置和加载我们的框架这件事请不应该写在一个servlet中,所以我定义了一个类Application.java。在这个类里面用来处理读取配置文件,加载各种配置以及缓存http映射以及别的一些我还没想到的事情。这个Application.java有一个带参数的构造函数,参数是应用名称,就是servlet-name,这样每一个类的功能就可以分开了。接下来我们写这个类里应该有什么东西。
读取配置文件并完成框架加载
先把代码贴出来:
package com.hebaibai.amvc;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.hebaibai.amvc.namegetter.AsmParamNameGetter;import com.hebaibai.amvc.objectfactory.AlwaysNewObjectFactory;import com.hebaibai.amvc.objectfactory.ObjectFactory;import com.hebaibai.amvc.utils.Assert;import com.hebaibai.amvc.utils.ClassUtils;import lombok.NonNull;import lombok.SneakyThrows;import lombok.extern.java.Log;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Method;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/** * aMac * * @author hjx */@Logpublic class Application { private static final String NOT_FIND = "缺少配置!"; //urlMapping节点名称 private static final String MAPPING_NODE = "mapping"; //是否支持注解 private static final String ANNOTATION_SUPPORT_NODE = "annotationSupport"; /** * 映射的工厂类 */ private UrlMethodMappingFactory urlMethodMappingFactory = new UrlMethodMappingFactory(); /** * 生成对象的工厂 */ private ObjectFactory objectFactory; /** * 应用的名称 */ private String applicationName; /** * 应用中的所有urlMapping */ private MapapplicationUrlMapping = new ConcurrentHashMap<>(); /** * 构造函数,通过servletName加载配置 * * @param applicationName */ public Application(String applicationName) { this.applicationName = applicationName; init(); } /** * 初始化配置 */ @SneakyThrows(IOException.class) protected void init() { String configFileName = applicationName + ".json"; InputStream inputStream = ClassUtils.getClassLoader().getResourceAsStream(configFileName); byte[] bytes = new byte[inputStream.available()]; inputStream.read(bytes); String config = new String(bytes, "utf-8"); //应用配置 JSONObject configJson = JSONObject.parseObject(config); boolean annotationSupport = configJson.getBoolean(ANNOTATION_SUPPORT_NODE); //TODO:是否开启注解,注解支持之后写 Assert.isTrue(!annotationSupport, "现在不支持此功能!"); urlMethodMappingFactory.setParamNameGetter(new AsmParamNameGetter()); //TODO:生成对象的工厂类(当先默认为每次都new一个新的对象) this.objectFactory = new AlwaysNewObjectFactory(); JSONArray jsonArray = configJson.getJSONArray(MAPPING_NODE); Assert.notNull(jsonArray, MAPPING_NODE + NOT_FIND); for (int i = 0; i < jsonArray.size(); i++) { UrlMethodMapping mapping = urlMethodMappingFactory.getUrlMethodMappingByJson(jsonArray.getJSONObject(i)); addApplicationUrlMapping(mapping); } } /** * 将映射映射添加进应用 * * @param urlMethodMapping */ protected void addApplicationUrlMapping(@NonNull UrlMethodMapping urlMethodMapping) { RequestType[] requestTypes = urlMethodMapping.getRequestTypes(); String url = urlMethodMapping.getUrl(); for (RequestType requestType : requestTypes) { String urlDescribe = getUrlDescribe(requestType, url); if (applicationUrlMapping.containsKey(urlDescribe)) { throw new UnsupportedOperationException(urlDescribe + "已经存在!"); } Method method = urlMethodMapping.getMethod(); Class aClass = urlMethodMapping.getClass(); log.info("mapping url:" + urlDescribe + " to " + aClass.getName() + "." + method.getName()); applicationUrlMapping.put(urlDescribe, urlMethodMapping); } } /** * 获取Url的描述 * * @param requestType * @param url * @return */ protected String getUrlDescribe(RequestType requestType, @NonNull String url) { return requestType.name() + ":" + url; } /** * 根据url描述获取 UrlMethodMapping * * @param urlDescribe * @return */ protected UrlMethodMapping getUrlMethodMapping(@NonNull String urlDescribe) { UrlMethodMapping urlMethodMapping = applicationUrlMapping.get(urlDescribe); return urlMethodMapping; } /** * 生成对象的工厂 * * @return */ protected ObjectFactory getObjectFactory() { return this.objectFactory; }}
这个类中我用了一些lombok的注解,大家可以先不用管它。
属性的说明:
1:UrlMethodMappingFactory :用来创建url与Method的映射关系:UrlMethodMapping的工厂类,在 自己写一个mvc框架吧(二)这一篇中有说到。
2:applicationName :应用的名称,其实就是servlet的名称(web.xml中servlet-name节点中的值)
3:applicationUrlMapping: url描述与UrlMethodMapping 的一个对应关系。url描述是我自己定义的一个东西,结构基本上是这样的:请求类型+“:”+请求地址。例子:“ GET:/index ”。
4:objectFactory:对象工厂,用来实例化对象用的,在 自己写一个mvc框架吧(二)这一篇中有说道。
方法的说明:
1:init():用来根据应用名称,拼接配置文件的名称,并读取其中的内容,并做一些校验。
2:getUrlDescribe(): 获取前面说道的url描述。
3:addApplicationUrlMapping(UrlMethodMapping urlMethodMapping): 将 applicationUrlMapping 填充起来。
4:getUrlMethodMapping(String urlDescribe):根据url描述获取 urlMethodMapping。
5:getObjectFactory():获取对象工厂,用来在servlet中实例化对象。
现在加载框架的代码写好了,下面开始写Servlet。
写请求的入口:servlet
这个写起来比较简单,需要做的事情有如下几个:
1:在servlet初始化的时候获取servlet的名称,然后加载我们的mvc框架。
2:在得到一次http请求的时候,根据请求地址、请求方式获取对应的Method,也就是urlMethodMapping。
3:根据urlMethodMapping获取对应的参数,转换成相应的类型,并通过反射执行方法。
4:将返回结果转换为Json,并在浏览器显示出来。(这一步是暂时的)
因为在前几章我们已经将很多代码写好了,这里我们只需要将之前写的一些东西拼起来就好了,并不需要写太多的东西,下面吧代码贴出来:
import com.alibaba.fastjson.JSONObject;import com.hebaibai.amvc.objectfactory.ObjectFactory;import lombok.SneakyThrows;import lombok.extern.java.Log;import javax.servlet.ServletConfig;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;/** * mvc的入口 * * @author hjx */@Logpublic class MvcServlet extends HttpServlet { /** * 应用 */ private Application application; /** * 请求中的参数获取器 */ private MethodValueGetter methodValueGetter; /** * 初始化项目 * 1:获取Servlet名称,加载名称相同的配置文件 * 2:加载配置文件中的urlMapping */ @Override @SneakyThrows(ServletException.class) public void init(ServletConfig config) { super.init(config); String servletName = config.getServletName(); log.info("aMvc init servletName:" + servletName); application = new Application(servletName); methodValueGetter = new MethodValueGetter(); } /** * 执行请求 * * @param request * @param response */ @SneakyThrows({IOException.class}) private void doInvoke(HttpServletRequest request, HttpServletResponse response) { RequestType requestType = getRequestType(request.getMethod()); String urlDescribe = application.getUrlDescribe(requestType, request.getPathInfo()); UrlMethodMapping urlMethodMapping = application.getUrlMethodMapping(urlDescribe); //没有找到对应的mapping if (urlMethodMapping == null) { unsupportedMethod(request, response); return; } //方法执行结果 Object result = invokeMethod(urlMethodMapping, request); //TODO:视图处理,先以JSON形式返回 response.setHeader("content-type", "application/json;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.write(JSONObject.toJSONString(result)); writer.close(); } /** * 反射执行方法 * * @param urlMethodMapping * @param request * @return */ @SneakyThrows({IllegalAccessException.class, InvocationTargetException.class}) private Object invokeMethod(UrlMethodMapping urlMethodMapping, HttpServletRequest request) { Object[] methodValue = methodValueGetter.getMethodValue(urlMethodMapping.getParamClasses(), urlMethodMapping.getParamNames(), request); Method method = urlMethodMapping.getMethod(); Class objectClass = urlMethodMapping.getObjectClass(); //通过对象工厂实例化objectClass ObjectFactory objectFactory = application.getObjectFactory(); Object object = objectFactory.getObject(objectClass); return method.invoke(object, methodValue); } /** * 根据http请求方式获取RequestType * * @param requestMethod * @return */ private RequestType getRequestType(String requestMethod) { if (requestMethod.equalsIgnoreCase(RequestType.GET.name())) { return RequestType.GET; } if (requestMethod.equalsIgnoreCase(RequestType.POST.name())) { return RequestType.POST; } if (requestMethod.equalsIgnoreCase(RequestType.PUT.name())) { return RequestType.PUT; } if (requestMethod.equalsIgnoreCase(RequestType.DELETE.name())) { return RequestType.DELETE; } throw new UnsupportedOperationException("请求方式不支持:" + requestMethod); } /** * 不支持的请求方式 * * @param request * @param response */ @SneakyThrows(IOException.class) private void unsupportedMethod(HttpServletRequest request, HttpServletResponse response) { String protocol = request.getProtocol(); String method = request.getMethod(); String errorMsg = "不支持的请求方式:" + method + "!"; if (protocol.endsWith("1.1")) { response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, errorMsg); } else { response.sendError(HttpServletResponse.SC_BAD_REQUEST, errorMsg); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) { doInvoke(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) { doInvoke(request, response); } @Override protected void doPut(HttpServletRequest request, HttpServletResponse response) { doInvoke(request, response); } @Override protected void doDelete(HttpServletRequest request, HttpServletResponse response) { doInvoke(request, response); }}
这里主要说一下 doInvoke(HttpServletRequest request, HttpServletResponse response) 和 invokeMethod(UrlMethodMapping urlMethodMapping, HttpServletRequest request) 这两个方法。
doInvoke:处理每次请求的主要方法,负责根据请求的信息获取对应的Method并执行这个Method,在没有找到对应Method的时候显示对应的错误信息。最后根据配置将其处理成相应的视图(现在是Json)。
invokeMethod:通过对象工厂获取实例化对象,并通过反射执行Method,获取方法的返回值。
现在入口就写好了,新建一个Web项目测试一下吧
测试一下
首先我们新建一个web项目,之后在web.xml中添加:
mvc com.hebaibai.amvc.MvcServlet mvc /*
然后写一个IndexController.java作为controller:
package com.hebaibai.demo.web;import java.util.HashMap;import java.util.Map;/** * @author hjx */public class IndexController { /** * @param name * @return */ public Mapindex(String name) { Map map = new HashMap<>(); map.put("value", name); map.put("msg", "success"); return map; }}
因为servlet-name的值为mvc,所以我们需要在resources目录下新建文件mvc.json作为配置文件,so~ 新建文件:
{ "annotationSupport": false, "mapping": [ { "url": "/index", "requestType": [ "get" ], "method": "index", "objectClass": "com.hebaibai.demo.web.IndexController", "paramTypes": [ "java.lang.String" ] } ]}
现在所有的配制就写好,可以测试了~~~
but~~,现在有一个BUG,惊不惊喜 !!!
有一个BUG
这个bug是在 自己写一个mvc框架吧(二) 这一章的通过asm获取方法入参名称的时候出现的,之前的代码是这样的:
ClassReader classReader = null;try { classReader = new ClassReader(className);} catch (IOException e) { e.printStackTrace();}
因为我们最终写好的mvc框架是作为一个jar包出现的,所以在jar中,是无法通过这种形式解析到依赖这个jar的项目中的class,这里会出现一个异常,我觉得应该是类加载器在获取文件路径时候的问题。怎么解决呢?
解决bug
我们看一下classReader = new ClassReader(className) 这个方法的实现代码:
/** * Constructs a new {@link ClassReader} object. * * @param className the fully qualified name of the class to be read. The ClassFile structure is * retrieved with the current class loader's {@link ClassLoader#getSystemResourceAsStream}. * @throws IOException if an exception occurs during reading. */public ClassReader(final String className) throws IOException { this( readStream( ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true));}
他是通过class的包名称转换成为文件路径之后,通过相对路径(应该是以项目路径作为根路径)的形式读取的,这样就好解决了。我们使用绝对路径的形式(以系统中的根路)获取到这个文件流就好了,这样写:
ClassReader getClassReader(Class aClass) { Assert.notNull(aClass); String className = aClass.getName(); String path = getClass().getClassLoader().getResource("/").getPath(); File classFile = new File(path + className.replace('.', '/') + ".class"); try (InputStream inputStream = new FileInputStream(classFile)) { ClassReader classReader = new ClassReader(inputStream); return classReader; } catch (IOException e) { } throw new RuntimeException(className + "无法加载!");}
先获取到项目中的根目录在系统中的那个位置,然后将包名转换成文文件路径,最后拼接一下就好了~ 搞定。
现在就可以测试了,只需要将刚才的web项目启动后,访问一下配置的地址,就好了。我就不写了~~
最后
还剩视图控制器没有写,现在我们只是简单的用Json来返回出来,这个不太好,最起码要能返回个页面啥的。
下一章开始写视图控制器
拜拜~