自己写一个java的mvc框架吧(四)

时间:2022-12-10 22:39:44

自己写一个mvc框架吧(四)

写一个请求的入口,以及初始化框架

上一章写了获取方法的入参,并根据入参的参数类型进行数据转换。这时候,我们已经具备了通过反射调用方法的一切必要条件。现在我们缺少一个http请求的入口,就是一个servlet。现在我们开始写吧~

在这一章我们要做的事情有

  1. 定义一个配置文件,用来描述什么样的请求映射到哪一个class的哪一个方法上面。

  2. 在servlet初始化后,根据上面定义的配置文件加载mvc框架。

  3. 在一个http请求进入后,根据其请求路径,找到相应的方法,获取参数,使用反射执行该方法。

  4. 得到方法的执行结果后,先以json的形式在浏览器显示出来。

    这一步是视图层的功能,先这样写,之后在写各种视图控制器

现在开始写吧

定义配置文件

这里的配置不一定就必须是一个xmljsonyaml... 之类的文件,也可以是注解的形式。区别就只是在加载框架的时候根据不同的形式进行解析就好了。这里为了写起来方便,就先定义一个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:

<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>com.hebaibai.amvc.MvcServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

这时,配置文件的名称就是mvc.json。那么怎么做呢? 我们这么写:

//先定义一个servlet
public 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
*/
@Log
public 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 Map<String, UrlMethodMapping> applicationUrlMapping = 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:applicationUrlMappingurl描述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
*/
@Log
public 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中添加:

<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>com.hebaibai.amvc.MvcServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

然后写一个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 Map<String, String> index(String name) {
Map<String, String> 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来返回出来,这个不太好,最起码要能返回个页面啥的。

下一章开始写视图控制器

拜拜~

自己写一个java的mvc框架吧(四)的更多相关文章

  1. 自己写一个java的mvc框架吧(五)

    自己写一个mvc框架吧(五) 给框架添加注解的支持 一段废话 上一章本来是说这一章要写视图处理的部分,但是由于我在测试代码的时候需要频繁的修改配置文件,太麻烦了.所以这一章先把支持注解的功能加上,这样 ...

  2. 自己写一个java的mvc框架吧(三)

    自己写一个mvc框架吧(三) 根据Method获取参数并转换参数类型 上一篇我们将url与Method的映射创建完毕,并成功的将映射关系创建起来了.这一篇我们将根据Method的入参参数名称.参数类型 ...

  3. 自己写一个java的mvc框架吧(二)

    自己写一个mvc框架吧(二) 自己写代码的习惯 写一个框架吧,如果这个框架会用到一些配置上的东西,我自己习惯是先不用考虑这个配置文件应该是怎样的,什么形式的,先用一个java对象(比如叫 Config ...

  4. 自己写一个java的mvc框架吧(一)

    自己写一个mvc框架吧(一) 目录 自己写一个mvc框架吧(一) 自己写一个mvc框架吧(二) 自己写一个mvc框架吧(三) 自己写一个mvc框架吧(四) 写之前的一些废话 废话 1 (总是要先随便说 ...

  5. 自己动手写一个简单的MVC框架(第一版)

    一.MVC概念回顾 路由(Route).控制器(Controller).行为(Action).模型(Model).视图(View) 用一句简单地话来描述以上关键点: 路由(Route)就相当于一个公司 ...

  6. 自己动手写一个简单的MVC框架(第二版)

    一.ASP.NET MVC核心机制回顾 在ASP.NET MVC中,最核心的当属“路由系统”,而路由系统的核心则源于一个强大的System.Web.Routing.dll组件. 在这个System.W ...

  7. AsMVC&colon;一个简单的MVC框架的Java实现

    当初看了<从零开始写一个Java Web框架>,也跟着写了一遍,但当时学艺不精,真正进脑子里的并不是很多,作者将依赖注入框架和MVC框架写在一起也给我造成了不小的困扰.最近刚好看了一遍sp ...

  8. Summer——从头开始写一个简易的Spring框架

    Summer--从头开始写一个简易的Spring框架                ​ 参考Spring框架实现一个简易类似的Java框架.计划陆续实现IOC.AOP.以及数据访问模块和事务控制模块. ...

  9. 用Python写一个简单的Web框架

    一.概述 二.从demo_app开始 三.WSGI中的application 四.区分URL 五.重构 1.正则匹配URL 2.DRY 3.抽象出框架 六.参考 一.概述 在Python中,WSGI( ...

随机推荐

  1. Windows批处理:自动开关程序

    公司有台14年组装的PC,时常无故重启,所以编写了个然并卵的批处理来测试稳定性. 打开程序.bat @echo off title Start Software color 2F : "C: ...

  2. centos7网络设置

    1.设置虚拟机网络连接方式 2.启动改为从驱动启动 3.启动虚拟机,使用命令 ip addr 查看ip,发现网卡配置为 eno16777736 如果找不到网卡配置文件,请返回安装过程,查看是否正确选择 ...

  3. 三、jQuery--jQuery基础--jQuery基础课程--第2章 jQuery 基础选择器

    1.#id选择器 jquery能使用CSS选择器来操作网页中的标签元素.如果你想要通过一个id号去查找一个元素,就可以使用如下格式的选择器:$("#my_id") 其中#my_id ...

  4. Eval有什么功能?

    它的功能是把对应的字符串解析成JS代码并运行.应该尽量避免使用eval,因为不安全,非常耗性能.解析成JS代码要耗能,执行时也要耗能.

  5. Mac环境下用Java&lpar;Sikuli&plus;Robot&rpar;实现页游自动化

    转载请注明出自天外归云的博客园:http://www.cnblogs.com/LanTianYou/ Sikulix(以前叫Sikuli)在Mac电脑的环境配置步骤如下: 1.从官网上下载Sikuli ...

  6. Ubuntu 环境变量及 ADB 配置

    Ubuntu Linux 环境变量 同Windows一样,Ubuntu Linux系统包含两类环境变量:系统环境变量和用户环境变量.系统环境变量对所有系统用户都有效,用户环境变量仅仅对当前的用户有效. ...

  7. IP访问SQL数据库设置

    http://wenku.baidu.com/link?url=mnjuPMo9qJvzluCHEvqVDawpuloKeGla05a2L3UtqzD_bF1VJMb7jHY4SBhuYH3-K_xF ...

  8. QT更改程序图标

    方法只要几个步骤就好了,如下: 1.准备好一个ico格式的图标文件,例如demo.ico 2.创建一个rc文件, 例如demo.rc. 并copy下面代码: // Generated by ResEd ...

  9. hdu 4400 Mines(离散化&plus;bfs&plus;枚举)

    Problem Description Terrorists put some mines in a crowded square recently. The police evacuate all ...

  10. c语言对齐问题

    引言 考虑下面的结构体定义: typedef struct{ char c1; short s; char c2; int i; }T_FOO; 假设这个结构体的成员在内存中是紧凑排列的,且c1的起始 ...