图源:
在上一篇文章中,我们看到了如何结合Servlet和JSP实现简单的MVC思想。但这种做法存在一些问题:
-
用Servlet类来实现Controller(控制器)会让Controller显得复杂,因为Controller必须继承自HttpServlet类,意味着它包含了一些Servlet的功能,这不符合单一任务原则的设计思想。
-
在Servlet中加载JSP和附加信息的部分显得复杂,可以进一步封装。
为了解决上边的问题,我们可以设计一个负责发放所有请求的Servlet,将所有的请求按照Method和Uri进行分发到具体的Controller。
最终的Controller大概长这样:
package cn.icexmoon.java.note.ch30.controller;
// ...
public class HelloController {
"/")
( public ModelAndViewer helloPage( ("name") String name){
Map<String, Object> data = new HashMap<>();
data.put("name", name);
return new ModelAndViewer("/WEB-INF/hello.jsp", data);
}
}
这里引入一个注解GetMapping
用于标记Controller的方法用于处理一个Get请求,其value值就是具体的Get请求的路径(uri)。
GetMapping
注解的声明如下:
package cn.icexmoon.java.note.ch30.framework;
// ...
RUNTIME)
(METHOD)
(public @interface GetMapping {
String value();
}
类似的,还定义了一个PostMapping
注解用于标记POST请求对应的处理方法。
此外,Controller方法返回一个ModelAndViewer
类型的对象,该对象代表需要渲染的视图(viewer),具体包含一个指向JSP的路径和视图需要加载的数据(Model)。
package cn.icexmoon.java.note.ch30.framework;
// ...
public class ModelAndViewer {
private String viewer;
private Map<String, ?> model;
public static ModelAndViewer redirect(String targetUri) {
return new ModelAndViewer(ServletDispatcher.REDIRECT_FLAG + targetUri, null);
}
}
这里还有一个棘手的问题,如何将Http请求中可能包含的参数“绑定”到Controller的处理方法的形参上。一个简单的做法是利用反射获取到方法对用的参数名,但这样做有一个问题,Java作为一个编译语言,在源码转换到class文件的时候,会丢失一些信息,这其中就包含方法的参数名(参数名会变成arg1
这样的无意义命名)。当然可以通过在编译和运行时添加一些参数来改变这一行为,但这样做会让事情变得复杂,且你要确保所有运行环境都有类似的参数。
这个问题可以通过添加一个用于标记参数名称的注解来解决(注解可以通过定义@Retention(RUNTIME)
确保在运行时存在)。这里我定义了一个注解@RequestParam
来做这件事。
下面需要做的就是编写一个类ServletDispatcher
,用于处理所有的Controller调度。这个类需要做两件事:
-
扫描所有的Controller类,为包含
@PostMapping
或@GetMapping
注解的方法创建映射,这样做后我们就能知道发送到服务端的Http请求需要用哪个方法来处理。 -
重写
doGet
和doPost
方法,根据请求的uri和已经生成的方法映射,选取合适的方法,用动态调用的方式来完整方法调用。
为了方便实现上面两点,可以抽象出一个AbsHandler
类,该类包含一个方法对象、方法形参名称、方法形参类型、uri等:
package cn.icexmoon.java.note.ch30.framework;
// ...
public abstract class AbsHandler {
protected Method method;
protected List<Class<?>> paramTypes;
protected List<String> paramNames;
protected String uri;
public static AbsHandler scanMethod(Method method) {
Parameter[] parameters = method.getParameters();
List<Class<?>> paramTypes = new ArrayList<>();
List<String> paramNames = new ArrayList<>();
for (int i = 0; i < parameters.length; i++) {
RequestParam rpa = parameters[i].getAnnotation(RequestParam.class);
if (rpa != null){
paramNames.add(i, rpa.value());
}
else{
paramNames.add(i, parameters[i].getName());
}
paramTypes.add(i, parameters[i].getType());
}
GetMapping getMappingAnnotation = method.getAnnotation(GetMapping.class);
if (getMappingAnnotation != null) {
String uri = getMappingAnnotation.value();
return new GetHandler(method, paramTypes, paramNames, uri);
}
PostMapping postMappingAnnotation = method.getAnnotation(PostMapping.class);
if (postMappingAnnotation != null) {
String uri = postMappingAnnotation.value();
return new PostHandler(method, paramTypes, paramNames, uri);
}
return null;
}
protected abstract Object getParam(HttpServletRequest req, String paramName);
public ModelAndViewer invoke(HttpServletRequest req, HttpServletResponse resp) {
Object clsInstance = null;
try {
clsInstance = method.getDeclaringClass().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
Object[] args = new Object[paramTypes.size()];
for (int i = 0; i < paramNames.size(); i++) {
String pName = paramNames.get(i);
Class<?> pType = paramTypes.get(i);
Object arg;
if (pType == HttpServletRequest.class) {
arg = req;
} else if (pType == HttpSession.class) {
arg = req.getSession();
} else if (pType == HttpServletResponse.class) {
arg = resp;
} else {
arg = this.getParam(req, pName);
}
args[i] = arg;
}
try {
System.out.println(args);
System.out.println(method.getName());
return (ModelAndViewer) method.invoke(clsInstance, args);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
该类的scanMethod
方法用于扫描一个Method对象,并且创建一个正确的AbsHandler
对象。这里从AbsHandler
派生出两个类GetHandler
和PostHandler
,分别代表Get请求和Post请求。
invoke
方法用于有Http请求产生时,从请求获取Handler需要的参数,并调用具体的Handler方法。除了需要从Http请求参数获取的Handler参数外,Handler方法可能还需要某些特殊参数,比如Http请求对象,Http响应对象,session对象。这些我们可以通过参数类型来进行绑定。
处理好需要的传参后,就可以通过反射来进行动态调用:method.invoke(clsInstance, args)
。
最后的工作就是编写一个用于调度的Servlet类ServletDispatcher
:
package cn.icexmoon.java.note.ch30.framework;
// ...
urlPatterns = "/")
(public class ServletDispatcher extends HttpServlet {
private static List<Class<?>> controllerClses = new ArrayList<>();
static {
controllerClses.add(HelloController.class);
controllerClses.add(HomeController.class);
controllerClses.add(SignController.class);
}
private static Map<String, AbsHandler> getMappings = new HashMap<>();
private static Map<String, AbsHandler> postMappings = new HashMap<>();
static {
scan();
}
public static final String REDIRECT_FLAG = "redirect:";
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doHandler(req, resp, getMappings);
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doHandler(req, resp, postMappings);
}
private void doHandler(HttpServletRequest req, HttpServletResponse resp, Map<String, AbsHandler> mappings) throws ServletException, IOException{
String path = req.getRequestURI().substring(req.getContextPath().length());
AbsHandler handler = mappings.get(path);
if (handler == null) {
resp.sendError(404);
return;
}
ModelAndViewer mv = handler.invoke(req, resp);
if (mv == null) {
return;
}
if (mv.getViewer().startsWith(REDIRECT_FLAG)) {
resp.sendRedirect(mv.getViewer().substring(REDIRECT_FLAG.length()));
return;
}
resp.setContentType("text/html; charset=UTF-8");
req.setAttribute("data", mv.getModel());
req.getRequestDispatcher(mv.getViewer()).forward(req, resp);
}
private static void scan() {
for (Class<?> cls : controllerClses) {
for (Method method : cls.getMethods()) {
AbsHandler handler = AbsHandler.scanMethod(method);
if (handler == null) {
continue;
} else if (handler instanceof GetHandler) {
getMappings.put(handler.getUri(), handler);
} else if (handler instanceof PostHandler) {
postMappings.put(handler.getUri(), handler);
} else {
continue;
}
}
}
}
}
这个类中,scan
方法用于扫描Controller类,并生成用于处理Get请求的Handler集合getMappings
以及处理POST请求的Handler集合postMappings
。并且在有HTTP请求产生时,通过doHandler
方法寻找一个正确的Handler进行调用。
doHandler
方法的最后是利用Handler方法调用后产生的ModelAndViewer
redirect:
开头,就进行跳转。
其实到这里,我们就实现了一个简单的MVC框架。整个代码结构类似这样:
在这个简单的MVC框架下,只需要用很简单的代码就可以实现Controller:
package cn.icexmoon.java.note.ch30.controller;
// ...
public class SignController {
private static Map<String, String> users = Collections.synchronizedMap(new HashMap<>());
static {
users.put("icexmoon", "123");
}
"/sign")
( public ModelAndViewer signPage() {
return new ModelAndViewer("/WEB-INF/sign.jsp", null);
}
"/sign")
( public ModelAndViewer sign(HttpSession session,
"name") String name,
( "password") String password) {
( if (!users.containsKey(name)
|| users.get(name) == null
|| !users.get(name).equals(password)){
//登录失败,跳转到错误信息显示页面
Map<String, Object> data = new HashMap<>();
data.put("msg", "用户名或密码错误");
return new ModelAndViewer("/WEB-INF/error.jsp", data);
}
session.setAttribute("user", name);
return ModelAndViewer.redirect("/hello/home");
}
"/exit")
( public ModelAndViewer exit(HttpSession session) {
session.removeAttribute("user");
return ModelAndViewer.redirect("/hello/sign");
}
}
示例中的其他代码和JSP就不一一展示了,完整的示例代码可以通过获取。
要说明的是,这个示例并不完善,存在一些问题:
-
Controller的方法形参除了String外,显然还有其他类型,比如
Integer
等,框架代码并没有处理这些类型的参数输入。 -
简单起见,这里生成页面依然使用的是JSP,没有引入其他的模板语言,使用模板语言会让页面嵌入变量更容易,且避免可能的注入攻击。
-
Controller类的注册是通过代码硬编码实现的,这样显然不适合扩展。实际上应当通过反射扫描包实现自动注册,或者使用注解处理器编写代码来处理。
虽然存在一些问题,但这个简单示例依然可以说明如何用Java构建一个简单的MVC框架,实际上目前成熟的框架(如Spring MVC,Spring Boot等)实现的方式和结果都是与之类似的,可以帮助我们理解和学习这些成熟的商业框架。
就这样了,谢谢阅读。
文章评论