​

今天来一次手撕MVC框架,主要难点在于使用反射机制调用Controller方法。


通过结合​​Servlet​​和​​JSP​​的​​MVC​​模式,我们可以发挥二者各自的优点:

  • ​Servlet​​实现业务逻辑;
  • ​JSP​​实现展示逻辑。

但是,直接把​​MVC​​搭在​​Servlet​​和​​JSP​​之上还是不太好,原因如下:

  • ​Servlet​​提供的接口仍然偏底层,需要实现​​Servlet​​调用相关接口;
  • ​JSP​​对页面开发不友好,更好的替代品是模板引擎;
  • 业务逻辑最好由纯粹的Java类实现,而不是强迫继承自​​Servlet​​。

能不能通过普通的Java类实现​​MVC​​的​​Controller​​?类似下面的代码:

 
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}
}

上面的这个Java类每个方法都对应一个​​GET​​或​​POST​​请求,方法返回值是​​ModelAndView​​,它包含一个​​View​​的路径以及一个​​Model​​,这样,再由​​MVC​​框架处理后返回给浏览器。

如果是​​GET​​请求,我们希望​​MVC​​框架能直接把​​URL​​参数按方法参数对应起来然后传入:

@GetMapping("/hello")
public ModelAndView hello(String name) {
...
}

如果是​​POST​​请求,我们希望​​MVC​​框架能直接把​​Post​​参数变成一个​​JavaBean​​后通过方法参数传入:

@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
...
}

为了增加灵活性,如果​​Controller​​的方法在处理请求时需要访问​​HttpServletRequest​​、​​HttpServletResponse​​、​​HttpSession​​这些实例时,只要方法参数有定义,就可以自动传入:

@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}

设计MVC框架

如何设计一个​​MVC​​框架?在上文中,我们已经定义了上层代码编写​​Controller​​的一切接口信息,并且并不要求实现特定接口,只需返回​​ModelAndView​​对象,该对象包含一个​​View​​和一个​​Model​​。实际上​​View​​就是模板的路径,而​​Model​​可以用一个​​Map<String, Object>​​表示,因此,​​ModelAndView​​定义非常简单:

public class ModelAndView {
Map<String, Object> model;
String view;
}

比较复杂的是我们需要在​​MVC​​框架中创建一个接收所有请求的​​Servlet​​,通常我们把它命名为​​DispatcherServlet​​,它总是映射到​​/​​,然后,根据不同的​​Controller​​的方法定义的​​@Get​​或​​@Post​​的​​Path​​决定调用哪个方法,最后,获得方法返回的​​ModelAndView​​后,渲染模板,写入​​HttpServletResponse​​,即完成了整个​​MVC​​的处理。

这个MVC的架构如下:

MVC框架原理详解_spring

其中,DispatcherServlet以及如何渲染均由MVC框架实现,在MVC框架之上只需要编写每一个Controller。

我们来看看如何编写最复杂的​​DispatcherServlet​​。首先,我们需要存储请求路径到某个具体方法的映射:

@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {
private Map<String, GetDispatcher> getMappings = new HashMap<>();
private Map<String, PostDispatcher> postMappings = new HashMap<>();
}

处理一个​​GET​​请求是通过​​GetDispatcher​​对象完成的,它需要如下信息:

class GetDispatcher {
Object instance; // Controller实例
Method method; // Controller方法
String[] parameterNames; // 方法参数名称
Class<?>[] parameterClasses; // 方法参数类型
}

有了以上信息,就可以定义​​invoke()​​来处理真正的请求:

class GetDispatcher {
...

public GetDispatcher(Object instance, Method method, String[] parameterNames, Class<?>[] parameterClasses) {
super();
this.instance = instance;
this.method = method;
this.parameterNames = parameterNames;
this.parameterClasses = parameterClasses;
}

/**
* 通过构造某个方法需要的所有参数列表,使用反射调用该方法后返回结果。
* 为了增加灵活性,如果Controller的方法在处理请求时需要访问HttpServletRequest、HttpServletResponse、HttpSession这些实例时,
* 只要方法参数有定义,就可以自动传入:
* @param request
* @param response
* @return
* @throws IOException
* @throws ReflectiveOperationException
*/
@Override
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
throws IOException, ReflectiveOperationException {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
String parameterName = parameterNames[i];
Class<?> parameterClass = parameterClasses[i];
if (parameterClass == HttpServletRequest.class) {
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) {
arguments[i] = response;
} else if (parameterClass == HttpSession.class) {
arguments[i] = request.getSession();
} else if (parameterClass == int.class) {
arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == long.class) {
arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == boolean.class) {
arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
} else if (parameterClass == String.class) {
arguments[i] = getOrDefault(request, parameterName, "");
} else {
throw new RuntimeException("Missing handler for type: " + parameterClass);
}
}
/**
* return 中this.method.invoke的invoke()是反射中的方法。
* 对Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,
* 即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
*/
return (ModelAndView) this.method.invoke(this.instance, arguments);
}

/**
* 从request 中获取参数类型
* @param request
* @param name
* @param defaultValue
* @return
*/
private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
String s = request.getParameter(name);
return s == null ? defaultValue : s;
}
}

上述代码比较繁琐,但逻辑非常简单,即通过构造某个方法需要的所有参数列表,使用反射调用该方法后返回结果。

类似的,​​PostDispatcher​​需要如下信息:

class PostDispatcher {
Object instance; // Controller实例
Method method; // Controller方法
Class<?>[] parameterClasses; // 方法参数类型
ObjectMapper objectMapper; // JSON映射
}

和​​GET​​请求不同,​​POST​​请求严格地来说不能有​​URL​​参数,所有数据都应当从​​Post Body​​中读取。这里我们为了简化处理,只支持​​JSON​​格式的​​POST​​请求,这样,把​​Post​​数据转化为​​JavaBean​​就非常容易。

class PostDispatcher extends AbstractDispatcher {

...

public PostDispatcher(Object instance, Method method, Class<?>[] parameterClasses, ObjectMapper objectMapper) {
this.instance = instance;
this.method = method;
this.parameterClasses = parameterClasses;
this.objectMapper = objectMapper;
}

/*
post 方法映射参数在body里面,此时只有方法中定义参数可以查询
*/
@Override
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
throws IOException, ReflectiveOperationException {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
Class<?> parameterClass = parameterClasses[i];
if (parameterClass == HttpServletRequest.class) {
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) {
arguments[i] = response;
} else if (parameterClass == HttpSession.class) {
arguments[i] = request.getSession();
} else {
// 读取JSON并解析为JavaBean:
BufferedReader reader = request.getReader();
arguments[i] = this.objectMapper.readValue(reader, parameterClass);
}
}
return (ModelAndView) this.method.invoke(instance, arguments);
}
}

最后,我们来实现整个​​DispatcherServlet​​的处理流程,以​​doGet()​​为例:

public class DispatcherServlet extends HttpServlet {
...
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
String path = req.getRequestURI().substring(req.getContextPath().length());
// 根据路径查找GetDispatcher:
GetDispatcher dispatcher = this.getMappings.get(path);
if (dispatcher == null) {
// 未找到返回404:
resp.sendError(404);
return;
}
// 调用Controller方法获得返回值:
ModelAndView mv = dispatcher.invoke(req, resp);
// 允许返回null:
if (mv == null) {
return;
}
// 允许返回`redirect:`开头的view表示重定向:
if (mv.view.startsWith("redirect:")) {
resp.sendRedirect(mv.view.substring(9));
return;
}
// 将模板引擎渲染的内容写入响应:
PrintWriter pw = resp.getWriter();
this.viewEngine.render(mv, pw);
pw.flush();
}
}

这里有几个小改进:

  • 允许​​Controller​​方法返回​​null​​,表示内部已自行处理完毕;
  • 允许​​Controller​​方法返回以​​redirect:​​开头的​​view​​名称,表示一个重定向。

最后一步是在​​DispatcherServlet​​的​​init()​​方法中初始化所有​​Get​​和​​Post​​的映射,以及用于渲染的模板引擎:

public class DispatcherServlet extends HttpServlet {
private Map<String, GetDispatcher> getMappings = new HashMap<>();
private Map<String, PostDispatcher> postMappings = new HashMap<>();
private ViewEngine viewEngine;

@Override
public void init() throws ServletException {
this.getMappings = scanGetInControllers();
this.postMappings = scanPostInControllers();
this.viewEngine = new ViewEngine(getServletContext());
}
...
}

​@GetMapping​​和​​@PostMapping​​接口定义:

@Retention(RUNTIME)
@Target(METHOD)
public @interface GetMapping {

String value();
}

@Retention(RUNTIME)
@Target(METHOD)
public @interface PostMapping {

String value();
}




使用反射扫描所有​​Controller​​以获取所有标记有​​@GetMapping​​和​​@PostMapping​​的方法:

public class DispatcherServlet extends HttpServlet {

...

//TODO:可指定package 并自动扫描:
private List<Class<?>> controllers = List.of(IndexController.class, UserController.class);

private static final Set<Class<?>> supportedGetParameterTypes = Set.of(int.class, long.class, boolean.class,
String.class, HttpServletRequest.class, HttpServletResponse.class, HttpSession.class);

private static final Set<Class<?>> supportedPostParameterTypes = Set.of(HttpServletRequest.class,
HttpServletResponse.class, HttpSession.class);

/**
* 当servlet 容器创建当前Servlet 实例后,会自动调用init(ServletConfig)方法
*/
@Override
public void init() throws ServletException {
logger.info("init {}...", getClass().getSimpleName());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 依次处理每个Controller:
for (Class<?> controllerClass : controllers) {
try {
/**
* // 获取构造方法Integer(int):
* Constructor cons1 = Integer.class.getConstructor(int.class);
* // 调用构造方法:
* Integer n1 = (Integer) cons1.newInstance(123);
* System.out.println(n1);
*/
Object controllerInstance = controllerClass.getConstructor().newInstance();
// 依次处理每个Method:
for (Method method : controllerClass.getMethods()) {
if (method.getAnnotation(GetMapping.class) != null) {
// 处理@Get:
if (method.getReturnType() != ModelAndView.class && method.getReturnType() != void.class) {
throw new UnsupportedOperationException(
"Unsupported return type: " + method.getReturnType() + " for method: " + method);
}
//返回mehtod 方法参数类型数组
for (Class<?> parameterClass : method.getParameterTypes()) {
if (!supportedGetParameterTypes.contains(parameterClass)) {
throw new UnsupportedOperationException(
"Unsupported parameter type: " + parameterClass + " for method: " + method);
}
}
//把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:
String[] parameterNames = Arrays.stream(method.getParameters()).map(p -> p.getName())
.toArray(String[]::new);
//path 表示GetMapping 映射的路径,@GetMapping("/hello")
String path = method.getAnnotation(GetMapping.class).value();
logger.info("Found GET: {} => {}", path, method);
this.getMappings.put(path, new GetDispatcher(controllerInstance, method, parameterNames,
method.getParameterTypes()));
} else if (method.getAnnotation(PostMapping.class) != null) {
// 处理@Post:
if (method.getReturnType() != ModelAndView.class && method.getReturnType() != void.class) {
throw new UnsupportedOperationException(
"Unsupported return type: " + method.getReturnType() + " for method: " + method);
}
Class<?> requestBodyClass = null;
for (Class<?> parameterClass : method.getParameterTypes()) {
if (!supportedPostParameterTypes.contains(parameterClass)) {
if (requestBodyClass == null) {
requestBodyClass = parameterClass;
} else {
throw new UnsupportedOperationException("Unsupported duplicate request body type: "
+ parameterClass + " for method: " + method);
}
}
}
String path = method.getAnnotation(PostMapping.class).value();
logger.info("Found POST: {} => {}", path, method);
this.postMappings.put(path, new PostDispatcher(controllerInstance, method,
method.getParameterTypes(), objectMapper));
}
}
} catch (ReflectiveOperationException e) {
throw new ServletException(e);
}
}
// 创建ViewEngine:
this.viewEngine = new ViewEngine(getServletContext());
}

这样,整个​​MVC​​框架就搭建完毕。

实现渲染

有的童鞋对如何使用模板引擎进行渲染有疑问,即如何实现上述的​​ViewEngine​​?其实​​ViewEngine​​非常简单,只需要实现一个简单的​​render()​​方法:

public class ViewEngine {
public void render(ModelAndView mv, Writer writer) throws IOException {
String view = mv.view;
Map<String, Object> model = mv.model;
// 根据view找到模板文件:
Template template = getTemplateByPath(view);
// 渲染并写入Writer:
template.write(writer, model);
}
}

Java有很多开源的模板引擎,常用的有:

他们的用法都大同小异。这里我们推荐一个使用​​Jinja​​语法的模板引擎​​Pebble​​,它的特点是语法简单,支持模板继承,编写出来的模板类似:

<html>
<body>
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
</body>
</html>

即变量用​​{{ xxx }}​​表示,控制语句用​​{% xxx %}​​表示。

使用​​Pebble​​渲染只需要如下几行代码:

public class ViewEngine {
private final PebbleEngine engine;

public ViewEngine(ServletContext servletContext) {
// 定义一个ServletLoader用于加载模板:
ServletLoader loader = new ServletLoader(servletContext);
// 模板编码:
loader.setCharset("UTF-8");
// 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录:
loader.setPrefix("/WEB-INF/templates");
// 模板后缀:
loader.setSuffix("");
// 创建Pebble实例:
this.engine = new PebbleEngine.Builder()
.autoEscaping(true) // 默认打开HTML字符转义,防止XSS攻击
.cacheActive(false) // 禁用缓存使得每次修改模板可以立刻看到效果
.loader(loader).build();
}

public void render(ModelAndView mv, Writer writer) throws IOException {
// 查找模板:
PebbleTemplate template = this.engine.getTemplate(mv.view);
// 渲染:
template.evaluate(writer, mv.model);
}
}

最后我们来看看整个工程的结构:

MVC框架原理详解_java_02

其中,​​framework​​包是​​MVC​​的框架,完全可以单独编译后作为一个​​Maven​​依赖引入,​​controller​​包才是我们需要编写的业务逻辑。

我们还硬性规定模板必须放在​​webapp/WEB-INF/templates​​目录下,静态文件必须放在​​webapp/static​​目录下,因此,为了便于开发,我们还顺带实现一个​​FileServlet​​来处理静态文件:

@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })
public class FileServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 读取当前请求路径:
ServletContext ctx = req.getServletContext();
// RequestURI包含ContextPath,需要去掉:
String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
// 获取真实文件路径:
String filepath = ctx.getRealPath(urlPath);
if (filepath == null) {
// 无法获取到路径:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path path = Paths.get(filepath);
if (!path.toFile().isFile()) {
// 文件不存在:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 根据文件名猜测Content-Type:
String mime = Files.probeContentType(path);
if (mime == null) {
mime = "application/octet-stream";
}
resp.setContentType(mime);
// 读取文件并写入Response:
OutputStream output = resp.getOutputStream();
try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
input.transferTo(output);
}
output.flush();
}
}

结果展示

运行代码,在浏览器中输入​​URL​​:​​http://localhost:8080/signin​​可以看到如下页面:

MVC框架原理详解_spring_03

MVC框架原理详解_java_04

MVC框架原理详解_java_05

添加配置信息

为了把方法参数的名称编译到​​class​​文件中,以便处理​​@GetMapping​​时使用,我们需要打开编译器的一个参数:

  • 在​​Eclipse​​中勾选​​Preferences-Java-Compiler-Store information about method parameters (usable via reflection)​​;
  • 在Idea中选择​​Preferences-Build, Execution, Deployment-Compiler-Java Compiler-Additional command line parameters​​,填入​​-parameters​​;

在​​Maven​​的​​pom.xml​​添加一段配置如下:

<project ...>
<modelVersion>4.0.0</modelVersion>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

有些用过​​Spring MVC​​的童鞋会发现,本节实现的这个​​MVC​​框架,上层代码使用的公共类如​​GetMapping​​、​​PostMapping​​和​​ModelAndView​​都和​​Spring MVC​​非常类似。实际上,我们这个​​MVC​​框架主要参考就是​​Spring MVC​​,通过实现一个“简化版”​​MVC​​,可以掌握​​Java Web MVC​​开发的核心思想与原理,对将来直接使用​​Spring MVC​​是非常有帮助的。

自我总结MVC框架执行过程

多图预警

当servlet 容器创建当前Servlet 实例后,会自动调用init(ServletConfig)方法

初始化init()函数

MVC框架原理详解_servlet_06

init()执行过程

MVC框架原理详解_java_07

init()

MVC框架原理详解_servlet_08

viewEngine 初始化

MVC框架原理详解_servlet_09

init()初始化完成

MVC框架原理详解_servlet_10

接收请求

MVC框架原理详解_java_11

计算属性

MVC框架原理详解_mvc框架_12

MVC框架原理详解_springmvc_13

调用栈列表

MVC框架原理详解_servlet_14

反射调用Controller方法

MVC框架原理详解_servlet_15

返回MV

MVC框架原理详解_springmvc_16

渲染

MVC框架原理详解_servlet_17

源码如下

bean:

public class SignInBean {

public String email;
public String password;
}

public class User {
public String email;
public String password;

public String name;
public String description;

public User() {
}

public User(String email, String password, String name, String description) {
this.email = email;
this.password = password;
this.name = name;
this.description = description;
}
}

Controller:

IndexController

public class IndexController {

@GetMapping("/")
public ModelAndView index(HttpSession session){
User user = (User) session.getAttribute("user");
return new ModelAndView("/index.html","user",user);

}

@GetMapping("/hello")
public ModelAndView hello(String name){
System.out.println("来到 hello");
if(name == null){
name = "World";
}

return new ModelAndView("/hello.html","name",name);
}
}

UserController

public class UserController {

private Map<String, User> userDatabase = new HashMap<>() {
{
List<User> users = List.of( //
new User("bob@example.com", "bob123", "Bob", "This is bob."),
new User("tom@example.com", "tomcat", "Tom", "This is tom."));
users.forEach(user -> {
put(user.email, user);
});
}
};

@GetMapping("/signin")
public ModelAndView signin() {
return new ModelAndView("/signin.html");
}

@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean, HttpServletResponse response, HttpSession session)
throws IOException {
User user = userDatabase.get(bean.email);
if (user == null || !user.password.equals(bean.password)) {
response.setContentType("application/json");
PrintWriter pw = response.getWriter();
pw.write("{\"error\":\"Bad email or password\"}");
pw.flush();
} else {
session.setAttribute("user", user);
response.setContentType("application/json");
PrintWriter pw = response.getWriter();
pw.write("{\"result\":true}");
pw.flush();
}
return null;
}

@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
session.removeAttribute("user");
return new ModelAndView("redirect:/");
}

@GetMapping("/user/profile")
public ModelAndView profile(HttpSession session) {
User user = (User) session.getAttribute("user");
if (user == null) {
return new ModelAndView("redirect:/signin");
}
return new ModelAndView("/profile.html", "user", user);
}
}

framework

DispatcherServlet

package com.sun.framework;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.controller.IndexController;
import com.sun.controller.UserController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.*;

@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {

private final Logger logger = LoggerFactory.getLogger(getClass());
private Map<String, GetDispatcher> getMappings = new HashMap<>();
private Map<String, PostDispatcher> postMappings = new HashMap<>();
private ViewEngine viewEngine;

//TODO:可指定package 并自动扫描:
private List<Class<?>> controllers = List.of(IndexController.class, UserController.class);


private static final Set<Class<?>> supportedGetParameterTypes = Set.of(int.class, long.class, boolean.class,
String.class, HttpServletRequest.class, HttpServletResponse.class, HttpSession.class);

private static final Set<Class<?>> supportedPostParameterTypes = Set.of(HttpServletRequest.class,
HttpServletResponse.class, HttpSession.class);
/**
* 当servlet 容器创建当前Servlet 实例后,会自动调用init(ServletConfig)方法
*/
@Override
public void init() throws ServletException {
logger.info("init {}...", getClass().getSimpleName());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 依次扫描处理每个Controller:
for (Class<?> controllerClass : controllers) {
try {
/**
* // 获取构造方法Integer(int):
* Constructor cons1 = Integer.class.getConstructor(int.class);
* // 调用构造方法:
* Integer n1 = (Integer) cons1.newInstance(123);
* System.out.println(n1);
*/
Object controllerInstance = controllerClass.getConstructor().newInstance();
// 依次处理每个Method:
for (Method method : controllerClass.getMethods()) {
//检查注解
if (method.getAnnotation(GetMapping.class) != null) {
// 处理@Get:
if (method.getReturnType() != ModelAndView.class && method.getReturnType() != void.class) {
throw new UnsupportedOperationException(
"Unsupported return type: " + method.getReturnType() + " for method: " + method);
}
//返回mehtod 方法参数类型数组,校验参数类型,直接可以扫描到
for (Class<?> parameterClass : method.getParameterTypes()) {
if (!supportedGetParameterTypes.contains(parameterClass)) {
throw new UnsupportedOperationException(
"Unsupported parameter type: " + parameterClass + " for method: " + method);
}
}
//把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:
String[] parameterNames = Arrays.stream(method.getParameters()).map(p -> p.getName())
.toArray(String[]::new);
//path 表示GetMapping 映射的路径,@GetMapping("/hello")
String path = method.getAnnotation(GetMapping.class).value();
logger.info("Found GET: {} => {}", path, method);
this.getMappings.put(path, new GetDispatcher(controllerInstance, method, parameterNames,
method.getParameterTypes()));
} else if (method.getAnnotation(PostMapping.class) != null) {
// 处理@Post:
if (method.getReturnType() != ModelAndView.class && method.getReturnType() != void.class) {
throw new UnsupportedOperationException(
"Unsupported return type: " + method.getReturnType() + " for method: " + method);
}

/**
* 处理请求Body数据,当有请求时才得到的数据,例如:
* @PostMapping("/signin")
* public ModelAndView doSignin(SignInBean bean, HttpServletResponse response, HttpSession session)
*/
Class<?> requestBodyClass = null;
for (Class<?> parameterClass : method.getParameterTypes()) {
/**
* SignInBean并不是supportedPostParameterTypes中事先规定的类型,而是请求时自带的数据类型
* 下面的情况针对的一个数据段,如果有多个呢?
*/
if (!supportedPostParameterTypes.contains(parameterClass)) {
if (requestBodyClass == null) {
requestBodyClass = parameterClass;
} else {
throw new UnsupportedOperationException("Unsupported duplicate request body type: "
+ parameterClass + " for method: " + method);
}
}
}
String path = method.getAnnotation(PostMapping.class).value();
logger.info("Found POST: {} => {}", path, method);
this.postMappings.put(path, new PostDispatcher(controllerInstance, method,
method.getParameterTypes(), objectMapper));
}
}
} catch (ReflectiveOperationException e) {
throw new ServletException(e);
}
}
// 创建ViewEngine:
this.viewEngine = new ViewEngine(getServletContext());
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
process(req, resp, this.getMappings);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
process(req, resp, this.postMappings);
}

private void process(HttpServletRequest req, HttpServletResponse resp,
Map<String, ? extends AbstractDispatcher> dispatcherMap) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
// 根据路径查找GetDispatcher:
String path = req.getRequestURI().substring(req.getContextPath().length());
AbstractDispatcher dispatcher = dispatcherMap.get(path);
if (dispatcher == null) {
// 未找到返回404:
resp.sendError(404);
return;
}
// 调用Controller方法获得返回值:
ModelAndView mv = null;
try {
// 调用GetDispatcher对象的invoke()方法
mv = dispatcher.invoke(req, resp);
} catch (ReflectiveOperationException e) {
throw new ServletException(e);
}
// 允许返回null:
if (mv == null) {
return;
}
// 允许返回`redirect:`开头的view表示重定向:
if (mv.view.startsWith("redirect:")) {
resp.sendRedirect(mv.view.substring(9));
return;
}
// 将模板引擎渲染的内容写入响应:
PrintWriter pw = resp.getWriter();
this.viewEngine.render(mv, pw);
pw.flush();
}

}
abstract class AbstractDispatcher {

public abstract ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
throws IOException, ReflectiveOperationException;
}

class GetDispatcher extends AbstractDispatcher {

final Object instance;
final Method method;
final String[] parameterNames;
final Class<?>[] parameterClasses;

public GetDispatcher(Object instance, Method method, String[] parameterNames, Class<?>[] parameterClasses) {
super();
this.instance = instance;
this.method = method;
this.parameterNames = parameterNames;
this.parameterClasses = parameterClasses;
}

/**
* 通过构造某个方法需要的所有参数列表,使用反射调用该方法后返回结果。
* 为了增加灵活性,如果Controller的方法在处理请求时需要访问HttpServletRequest、HttpServletResponse、HttpSession这些实例时,
* 只要方法参数有定义,就可以自动传入:
* @param request
* @param response
* @return
* @throws IOException
* @throws ReflectiveOperationException
*/
@Override
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
throws IOException, ReflectiveOperationException {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
String parameterName = parameterNames[i];
Class<?> parameterClass = parameterClasses[i];
if (parameterClass == HttpServletRequest.class) {
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) {
arguments[i] = response;
} else if (parameterClass == HttpSession.class) {
arguments[i] = request.getSession();
} else if (parameterClass == int.class) {
arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == long.class) {
arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == boolean.class) {
arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
} else if (parameterClass == String.class) {
arguments[i] = getOrDefault(request, parameterName, "");
} else {
throw new RuntimeException("Missing handler for type: " + parameterClass);
}
}
return (ModelAndView) this.method.invoke(this.instance, arguments);
}

/**
* 从request 中获取参数类型
* @param request
* @param name
* @param defaultValue
* @return
*/
private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
String s = request.getParameter(name);
return s == null ? defaultValue : s;
}
}

class PostDispatcher extends AbstractDispatcher {
/**
* post 方式在请求URL中没有参数,参数在body 里面,例如在form里,Controller中方法可以从body 中去的参数,
* 加上方法中自带的参数返回参数列表
*
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String body, HttpServletRequest request)
*System.out.println("进入testRequestBody中");
*System.out.println(body);
*return "success";
*}
*/

final Object instance;
final Method method;
final Class<?>[] parameterClasses;//方法参数类型
final ObjectMapper objectMapper;// JSON 映射

public PostDispatcher(Object instance, Method method, Class<?>[] parameterClasses, ObjectMapper objectMapper) {
this.instance = instance;
this.method = method;
this.parameterClasses = parameterClasses;
this.objectMapper = objectMapper;
}


/*
post 方法映射参数在body里面,此时只有方法中定义参数可以查询
*/
@Override
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
throws IOException, ReflectiveOperationException {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
Class<?> parameterClass = parameterClasses[i];
if (parameterClass == HttpServletRequest.class) {
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) {
arguments[i] = response;
} else if (parameterClass == HttpSession.class) {
arguments[i] = request.getSession();
} else {
// 读取JSON并解析为JavaBean:
/**
* InputStream input = Main.class.getResourceAsStream("/book.json");
* ObjectMapper mapper = new ObjectMapper();
* // 反序列化时忽略不存在的JavaBean属性:
* mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
* Book book = mapper.readValue(input, Book.class);
*/
BufferedReader reader = request.getReader();
arguments[i] = this.objectMapper.readValue(reader, parameterClass);
}
}
return (ModelAndView) this.method.invoke(instance, arguments);
}
}


FileServlet

package com.sun.framework;

import javax.servlet.ServletContext;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@WebServlet(urlPatterns = {"/favicon.ico","/static/*"})
public class FileServlet extends HttpServlet {

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//读取当前请求路径
ServletContext ctx = req.getServletContext();
//RequestURI 包含ContextPath需要去掉
String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
//获取真实文件路径
String filepath = ctx.getRealPath(urlPath);
if(filepath == null){
//无法获取到路径
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path path = Paths.get(filepath);
if(!path.toFile().isFile()){
//文件不存在
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

//根据文件名猜测Content-Type
String mime = Files.probeContentType(path);
if(mime == null){
mime = "application-octet-stream";
}
resp.setContentType(mime);
//读取文件并写入Response;
OutputStream output = resp.getOutputStream();
try(InputStream input = new BufferedInputStream(new FileInputStream(filepath))){
input.transferTo(output);
}
output.flush();
}
}

GetMapping

package com.sun.framework;

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

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;


@Retention(RUNTIME)
@Target(METHOD)
public @interface GetMapping {

String value();
}

ModelAndView

package com.sun.framework;

import java.util.HashMap;
import java.util.Map;


public class ModelAndView {

Map<String,Object> model;
public String view;

public ModelAndView(String view) {
this.view = view;
this.model = Map.of();
}

public ModelAndView(String view,String name,Object value){
this.view = view;
this.model = new HashMap<>();
this.model.put(name,value);

}

public ModelAndView(String view,Map<String,Object> model){
this.view = view;
this.model = new HashMap<>(model);
}
}

PostMapping

package com.sun.framework;

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

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Target(METHOD)
public @interface PostMapping {

String value();
}

ViewEngine

package com.sun.framework;

import com.mitchellbosecke.pebble.PebbleEngine;
import com.mitchellbosecke.pebble.loader.ServletLoader;
import com.mitchellbosecke.pebble.template.PebbleTemplate;

import javax.servlet.ServletContext;
import java.io.IOException;
import java.io.Writer;


public class ViewEngine {


private final PebbleEngine engine;

public void render(ModelAndView mv, Writer writer) throws IOException {

//根据view 找到模板文件
PebbleTemplate template = this.engine.getTemplate(mv.view);
//渲染并写入Writer
template.evaluate(writer,mv.model);
}

public ViewEngine(ServletContext servletContext){
//定义一个servletContext 用于加载模板
ServletLoader loader = new ServletLoader(servletContext);
//模板编码
loader.setCharset("UTF-8");
//模板前缀。这里默认模板必须放在'WEB-INF/template'目录下:
loader.setPrefix("WEB-INF/template");
//模板后缀:
loader.setSuffix("");
//创建Pebble实例:
this.engine = new PebbleEngine.Builder()
.autoEscaping(true)//默认打卡HTML字符转义,防止XSS攻击
.cacheActive(false) //禁用缓存使得每次修改模板可以立即看到效果
.loader(loader).build();
}
}

源码获取,请在公众号[编程牧马人]后台输入“MVC框架”