声明
本篇文章除部分引用外,均为原创内容,如有雷同纯属巧合,引用转载请附上原文链接与声明。
阅读条件
读本篇文章需掌握java基础知识,了解Spring MVC请求转发原理,掌握注解的使用方式,HTTP基本知识,Servlet基础,灵活运用反射,阅读自研Spring IOC(一)
注意
本文若包含部分下载内容,本着一站式阅读的想法,本站提供其对应软件的直接下载方式,但是由于带宽原因下载缓慢是必然的,建立读者去相关官网进行下载,若某些软件禁止三方传播,请在主页上通过联系作者的方式将相关项目进行取消。
相关文章
文章大纲
- 注解&枚举定义
- RequestHandleMapping
- DispatchServlet
- 参数解析工具
- EmbedTomcat嵌入
- 全链路整合
- 使用举例
简述
项目github地址传送门点此 ,该篇文章对应的代码在mvc-1.0分支上
该篇文章简述如何实现一个简易的MVC框架,做到MVC框架里面最重要的请求转发功能,为了实现该功能,需要设计实现转发规则,提供全局唯一DispatchServlet,解析请求参数到指定控制器的指定方法,并内嵌EmbedTomcat以提供。从客户端发出请求,到请求转发获取结果整个过程如下
- 获取请求路径与请求方法
- 根据请求路径与请求方法找到对应的RequestHandleMapping
- 将请求托管给RequestHandleMapping
- 解析请求提交的参数
- 将请求转发到RequestHandleMapping所绑定的Controller上的指定方法
- 获取执行结果
- 输出结果到客户端
注解&枚举定义
RequestMethod枚举定义
该枚举定义HTTP请求的方法种类,用于在请求分发时查找对应的处理方法,该类定义了常规的HTTP请求方法,如下
public enum RequestMethod {
GET("GET"),
HEAD("HEAD"),
POST("POST"),
PUT("PUT"),
DELETE("DELETE"),
OPTIONS("OPTIONS"),
TRACE("TRACE");
@Getter
private String value;
RequestMethod(String value) {
this.value = value;
}
}
定义请求转发注解@RequestMapping
该注解用于定义请求路径和处理方法的映射方式,与Spring MVC中的@RequestMapping定位一致,该路径通过value获取,映射的请求方法为上文中的RequestMethod,源码如下
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
/**
* 映射路径
*
* @return
*/
String value() default "";
/**
* 请求方法
*
* @return
*/
RequestMethod method();
}
RequestHandleMapping
该类用于在请求定义请求路径,请求方法,处理方法之间的映射关系,而处理方法实际上是Controller中的某个方法,并且提供了是否匹配请求路径,请求方法是否匹配方法以及处理请求的方法,当请求到来时,只需要找到对应的RequestHandleMaping,则将请求派发到该类中进行执行,该类的定义如下
@Getter
@Setter
public class RequestHandleMapping {
/**
* 响应的路径
*/
private String mapping;
/**
* 对应的Controller
*/
private Object controller;
/**
* 对应的处理方法
*/
private Method method;
/**
* 请求方法
*/
private RequestMethod requestMethod;
/**
* 是否匹配该请求
* @param mapping
* @param requestMethod
* @return
*/
public boolean isMatch(String mapping, String requestMethod) {
return this.mapping.equals(mapping) && this.requestMethod.getValue().equalsIgnoreCase(requestMethod);
}
/**
* 处理请求
* @param req
* @param res
*/
public void handle(ServletRequest req, ServletResponse res) {
try {
Object returnValue = method.invoke(controller, ParamUtil.extractParamFromRequest(req, method));
res.getWriter().write(returnValue.toString());
} catch (IllegalAccessException | InvocationTargetException | IOException e) {
log.error("call method [{}] of controller [{}] fail by reflect", method.getName(), controller.getClass().getName());
}
}
}
DispatchServlet
该类是请求的分发器,也是全局唯一一个Servlet,需要注意的是,设计该类时一定要保持无状态设计,因为该类并不能参与到业务中,该类的作用就是迅速转发请求到指定的请求处理方法中,该模式是前端控制器模式的体现。请求转发主要经过以下几个流程
- 获取请求方法
- 根据请求方法进一步派发请求到指定的方法中
- 获取请求路径,根据请求路径和请求方法找到指定的RequestHandleMapping
- 根据获取的RequestHandleMapping,将请求派发到Controller的指定方法中进行处理
- 获取处理结果,并将结果输出到请求客户端
首先在pom.xml中新增Servlet支持
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
设计如下
public class DispatchServlet extends HttpServlet {
private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
private Set<RequestHandleMapping> requestHandleMappings;
public DispatchServlet(Set<RequestHandleMapping> requestHandleMappings) {
this.requestHandleMappings = requestHandleMappings;
}
@Override
public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
if (req instanceof HttpServletRequest && resp instanceof HttpServletResponse) {
resp.setContentType(CONTENT_TYPE);
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
doDispatch(request, response);
} else {
throw new ServletException("non-HTTP request or response");
}
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if (method.equals("GET")) {
doGet(req, resp);
} else if (method.equals("HEAD")) {
doHead(req, resp);
} else if (method.equals("POST")) {
doPost(req, resp);
} else if (method.equals("PUT")) {
doPut(req, resp);
} else if (method.equals("DELETE")) {
doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
doOptions(req, resp);
} else if (method.equals("TRACE")) {
doTrace(req, resp);
} else {
doUnsupportedType(req, resp);
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
}
@Override
protected void doHead(HttpServletRequest req, HttpServletResponse res) {
getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) {
getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res) {
getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse res) {
getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse res) {
getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
}
@Override
protected void doTrace(HttpServletRequest req, HttpServletResponse res) {
getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
}
private void doUnsupportedType(HttpServletRequest req, HttpServletResponse resp) {
try {
resp.getWriter().write("unsupported method :" + req.getMethod());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private RequestHandleMapping getRequestMapping(String requestUrl, String method) {
for (RequestHandleMapping requestHandleMapping : requestHandleMappings) {
if (requestHandleMapping.isMatch(requestUrl, method)) {
return requestHandleMapping;
}
}
throw new RuntimeException("can't find the right requestHandleMapping for requestUrl:" + requestUrl + ",method :" + method);
}
}
参数解析工具
在请求派发到指定Controller的指定方法时,需要将客户端提交的参数解析为指定方法所需要的参数类型,实际上这里一般是做的反序列化等操作,但是笔者这里简单实现,统一返回一个指定的String类型字符串即可,设计如下
public class ParamUtil {
/**
* 从请求中抽取相关的参数,并组装成调用方法的参数数据进行方法。
* 这个方法模拟的是spring mvc关于调用前对于参数的前置处理器所做的业务操作
* 这里为了简化操作,默认返回一个 string参数
*
* @param req
* @param method
* @return
*/
public static Object[] extractParamFromRequest(ServletRequest req, Method method) {
return new String[]{"the default test param"};
}
}
EmbedTomcat嵌入
为了支持Web service,项目需要引入嵌入式Tomcat,这样在初始化完毕时启动Tomcat,并将Tomcat启动,这样请求才能映射到指定的controller,嵌入EmbedTomcat只需要在pom.xml加入以下依赖
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.0.48</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.0.48</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-logging-juli</artifactId>
<version>8.0.48</version>
</dependency>
全链路整合
编写SpringMVCContext
该类提供mvc模块的全部能力,外部只需要调用doMvc方法时,即可完成映射的创建,DispatchServlet初始化,EmbedTomcat启动,即开始对外提供服务,所以该类的doMvc方法有以下三个流程
- 从ioc容器中获取Controller
- 根据获取的Controller构建RequestHandleMapping集合
- 根据获取的RequestHandleMapping构建DispatchServlet
- 配置EmbedTomcat的初始化参数,比如监听端口,工作空间,监听根路径...等等
- 将DispatchServlet通过配置到EmbedTomcat中
- 启动Tomcat
所以设计的SpringMVCContext如下
public class SpringMVCContext {
private static final int PORT = 80;
private static final String CONTEXT_PATH = "/";
private static final String BASE_DIR = "temp";
private static final String DISPATCH_SERVLET_NAME = "dispatchServlet";
private static final String URL_PATTERN = "/";
/**
* 根据ioc容器创建相关的handlerMapping 完成MVC的操作
*/
public static void doMvc() {
Set<RequestHandleMapping> requestHandleMappings = buildRequestHandleMappings();
DispatchServlet dispatchServlet = buildDispatchServlet(requestHandleMappings);
startTomcatService(dispatchServlet);
}
/**
* 根据容器中被管理的controller bean组装 requestHandleMapping
*/
public static Set<RequestHandleMapping> buildRequestHandleMappings() {
Set<RequestHandleMapping> requestHandleMappings = new HashSet<>();
Set<?> controllerBeans = BeanContainer.getBeansByAnnotation(Controller.class);
for (Object controllerBean : controllerBeans) {
String basePath = controllerBean.getClass().getAnnotation(Controller.class).value();
Method[] methods = controllerBean.getClass().getDeclaredMethods();
for (Method method : methods) {
if (isHandleMappingMethod(method)) {
RequestHandleMapping requestHandleMapping = new RequestHandleMapping();
requestHandleMapping.setController(controllerBean);
requestHandleMapping.setMethod(method);
requestHandleMapping.setMapping(getMethodMapping(basePath, method));
requestHandleMapping.setRequestMethod(method.getDeclaredAnnotation(RequestMapping.class).method());
log.info("generate mapping info: [{}]", requestHandleMapping.getMapping());
requestHandleMappings.add(requestHandleMapping);
}
}
}
return requestHandleMappings;
}
public static DispatchServlet buildDispatchServlet(Set<RequestHandleMapping> requestHandleMappings) {
return new DispatchServlet(requestHandleMappings);
}
/**
* 开启Tomcat服务
*/
public static void startTomcatService(DispatchServlet dispatchServlet) {
try {
Tomcat tomcat = new Tomcat();
tomcat.setBaseDir(BASE_DIR);
tomcat.setPort(PORT);
Context context = tomcat.addContext(CONTEXT_PATH, new File(".").getAbsolutePath());
tomcat.addServlet(CONTEXT_PATH, DISPATCH_SERVLET_NAME, dispatchServlet);
context.addServletMappingDecoded(URL_PATTERN, DISPATCH_SERVLET_NAME);
tomcat.start();
tomcat.getServer().await();
} catch (LifecycleException e) {
log.error("tomcat start fail...");
throw new RuntimeException(e);
}
}
/**
* 判断指定方法是否需要对其坐映射
*
* @param method
* @return
*/
public static boolean isHandleMappingMethod(Method method) {
return method.isAnnotationPresent(RequestMapping.class);
}
public static String getMethodMapping(String basePath, Method method) {
if (method.isAnnotationPresent(RequestMapping.class)) {
return formatUrlPath(basePath) + formatUrlPath(method.getDeclaredAnnotation(RequestMapping.class).value());
}
throw new RuntimeException("unsupported method for generate handelMapping");
}
/**
* 格式化路径 使其最后创建的最终路径为 /xxx/xxx
* @param path
* @return
*/
public static String formatUrlPath(String path) {
StringBuilder sb = new StringBuilder();
if (!Strings.isNullOrEmpty(path)) {
if (!path.startsWith("/")) {
sb.append("/");
}
if (path.endsWith("/")) {
sb.append(path, 0, path.length() - 1);
} else {
sb.append(path);
}
}
return sb.toString();
}
}
使用举例
这里使用的例子与自研Spring AOP2.0(三)中的相同,这里仅仅修改Controller的映射声明,如下
@Controller("/a")
@Slf4j
public class ExampleController implements IExampleController {
@Autowired
private IExampleService service;
@Autowired
private IExampleRepository repository;
@Override
@RequestMapping(value = "/b", method = RequestMethod.GET)
public String show(String param) {
log.info("the receive param is [{}]", param);
log.info("ExampleController.show()");
service.show();
return "controller show method";
}
@Override
@RequestMapping(value = "/c", method = RequestMethod.POST)
public String show2(String param) {
log.info("the receive param is [{}]", param);
log.info("ExampleController.show()");
service.show();
return "controller show method";
}
}
接下来启动NobitaApplication,然后打开浏览器,请求localhost/a/b,首先可以在控制台看到如下结果,表明之前的ioc,aop功能仍然正常,且Controller.show()方法收到了正确的参数。
在返回浏览器,可以看到浏览器也收到了来自于Controller.show()方法的返回结果,如下