原文链接一起手撕MVC框架!
今天来一次手撕MVC框架,主要难点在于使用反射机制调用Controller方法。
以下知识点均来源于廖雪峰官方网站
https://www.liaoxuefeng.com/wiki/1252599548343744/1337408645759009
通过结合Servlet
和JSP
的MVC
模式,我们可以发挥二者各自的优点:
Servlet
实现业务逻辑;JSP
实现展示逻辑。但是,直接把MVC
搭在Servlet
和JSP
之上还是不太好,原因如下:
Servlet
提供的接口仍然偏底层,需要实现Servlet
调用相关接口;JSP
对页面开发不友好,更好的替代品是模板引擎;Servlet
。能不能通过普通的Java类实现MVC
的Controller
?类似下面的代码:
public class UserController {
@GetMapping("/signin")
public ModelAndView signin() {
...
}
@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
...
}
@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
框架?在上文中,我们已经定义了上层代码编写Controller
的一切接口信息,并且并不要求实现特定接口,只需返回ModelAndView
对象,该对象包含一个View
和一个Model
。实际上View
就是模板的路径,而Model
可以用一个Map
表示,因此,ModelAndView
定义非常简单:
public class ModelAndView {
Map<String, Object> model;
String view;
}
比较复杂的是我们需要在MVC
框架中创建一个接收所有请求的Servlet
,通常我们把它命名为DispatcherServlet
,它总是映射到/
,然后,根据不同的Controller
的方法定义的@Get
或@Post
的Path
决定调用哪个方法,最后,获得方法返回的ModelAndView
后,渲染模板,写入HttpServletResponse
,即完成了整个MVC
的处理。
这个MVC的架构如下:
其中,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);
}
}
最后我们来看看整个工程的结构:
其中,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
可以看到如下页面:
为了把方法参数的名称编译到class
文件中,以便处理@GetMapping
时使用,我们需要打开编译器的一个参数:
Eclipse
中勾选Preferences-Java-Compiler-Store information about method parameters (usable via reflection)
;Preferences-Build, Execution, Deployment-Compiler-Java Compiler-Additional command line parameters
,填入-parameters
;在Maven
的pom.xml
添加一段配置如下:
<project ...>
<modelVersion>4.0.0modelVersion>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<compilerArgs>
<arg>-parametersarg>
compilerArgs>
configuration>
plugin>
plugins>
build>
project>
有些用过Spring MVC
的童鞋会发现,本节实现的这个MVC
框架,上层代码使用的公共类如GetMapping
、PostMapping
和ModelAndView
都和Spring MVC
非常类似。实际上,我们这个MVC
框架主要参考就是Spring MVC
,通过实现一个“简化版”MVC
,可以掌握Java Web MVC
开发的核心思想与原理,对将来直接使用Spring MVC
是非常有帮助的。
多图预警
当servlet 容器创建当前Servlet 实例后,会自动调用init(ServletConfig)方法
初始化init()函数
init()执行过程
init()
viewEngine 初始化
init()初始化完成
接收请求
计算属性
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;
}
}
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("[email protected]", "bob123", "Bob", "This is bob."),
new User("[email protected]", "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);
}
}
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框架”