了解一下类似Spring MVC框架的底层实现 相信看完这个短文,MVC你就懂了,还不懂请艾特我······

带着问题去学习

  • MVC到底是什么?
  • 如何实现MVC?

    开始!

MVC是什么?

Web开发

在讲MVC之前,还是简要介绍一下Web开发吧,相信搞程序的或多或少都知道Web编程、Socket编程等等。
其实开发Web应用最原始的方式面向Http连接的底层进行开发,自己编写Web服务,需要了解和考虑的内容包括但不限于:Socket编程、端口号、报文处理、IO多路复用、线程池等等。

举个例子,用Java来编写一个Http服务器:

  1. public class Server {
  2. public static void main(String[] args) throws IOException {
  3. ServerSocket ss = new ServerSocket(8080); // 监听指定端口
  4. System.out.println("server is running...");
  5. for (;;) {
  6. Socket sock = ss.accept();
  7. System.out.println("connected from " + sock.getRemoteSocketAddress());
  8. Thread t = new Handler(sock);
  9. t.start();
  10. }
  11. }
  12. }
  13. class Handler extends Thread {
  14. Socket sock;
  15. public Handler(Socket sock) {
  16. this.sock = sock;
  17. }
  18. public void run() {
  19. try (InputStream input = this.sock.getInputStream()) {
  20. try (OutputStream output = this.sock.getOutputStream()) {
  21. handle(input, output);
  22. }
  23. } catch (Exception e) {
  24. try {
  25. this.sock.close();
  26. } catch (IOException ioe) {
  27. }
  28. System.out.println("client disconnected.");
  29. }
  30. }
  31. private void handle(InputStream input, OutputStream output) throws IOException {
  32. System.out.println("Process new http request...");
  33. var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
  34. var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
  35. // 读取HTTP请求:
  36. boolean requestOk = false;
  37. String first = reader.readLine();
  38. if (first.startsWith("GET / HTTP/1.")) {
  39. requestOk = true;
  40. }
  41. for (;;) {
  42. String header = reader.readLine();
  43. if (header.isEmpty()) { // 读取到空行时, HTTP Header读取完毕
  44. break;
  45. }
  46. System.out.println(header);
  47. }
  48. System.out.println(requestOk ? "Response OK" : "Response Error");
  49. if (!requestOk) {
  50. // 发送错误响应:
  51. writer.write("HTTP/1.0 404 Not Found\r\n");
  52. writer.write("Content-Length: 0\r\n");
  53. writer.write("\r\n");
  54. writer.flush();
  55. } else {
  56. // 发送成功响应:
  57. String data = "<html><body><h1>Hello, world!</h1></body></html>";
  58. int length = data.getBytes(StandardCharsets.UTF_8).length;
  59. writer.write("HTTP/1.0 200 OK\r\n");
  60. writer.write("Connection: close\r\n");
  61. writer.write("Content-Type: text/html\r\n");
  62. writer.write("Content-Length: " + length + "\r\n");
  63. writer.write("\r\n"); // 空行标识Header和Body的分隔
  64. writer.write(data);
  65. writer.flush();
  66. }
  67. }
  68. }

一个Http服务器,本质上来说就是TCP服务器,处理流程就是服务器一直监听端口,客户端向指定端口发送请求,服务器接收请求进行相应的逻辑处理,而后将处理结果又返回给客户端,最后在客户端浏览器上进行展示。更详细的玩意儿还是参见《Unix网络编程》那本书的例子,可以模仿C语言写个Java的客户端
但是在实际开发过程中,这些基础、底层的工作,就是重复造轮子,需要耗费大量的时间!
为了方便开发,将这些底层工作封装到一个库中——Servlet中,于是后来的程序员只需要基于Servlet的API来开发Web应用即可,不用再去管这些端口啊、IO多路复用什么的。

基于Servlet开发

基于Servlet开发可以大大减小开发Web服务的复杂程度,例如只需要几行代码,就可以让服务端相应客户端对主页的请求:

  1. //原址https://www.liaoxuefeng.com/wiki/1252599548343744/1304265949708322
  2. //WebServlet注解表示这是一个Servlet,并将url地址为"/"的请求映射到这个类中进行处理
  3. @WebServlet(urlPatterns = "/")
  4. public class HelloServlet extends HttpServlet {
  5. protected void doGet(HttpServletRequest req,
  6. HttpServletResponse resp)
  7. throws ServletException, IOException {
  8. // 设置响应类型:
  9. resp.setContentType("text/html");
  10. // 获取输出流:
  11. PrintWriter pw = resp.getWriter();
  12. // 写入响应:
  13. pw.write("<h1>Hello, world!</h1>");
  14. // flush强制输出:
  15. pw.flush();
  16. }
  17. }

一个Web应用其实就是由一个或多个Servlet组成的,每个Servlet通过注解说明自己负责的路径Url,并通过实现doGet和doPost方法来进行相应的逻辑处理。比如处理GET方法,就覆写doGet()方法:

  1. @WebServlet(urlPatterns = "/hello")
  2. public class HelloServlet extends HttpServlet {
  3. @Override
  4. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  5. ...
  6. }
  7. }

其他的如POST等请求方法也是类似的。
可以看到,当有了Servlet之后,开发Web应用就变得简洁了起来,只需要编写处理对应url的逻辑处理即可。
但是,你们有没有发现一个问题——在Java代码里出现了跟前端有关的代码。在Servlet程序中,输出HTML的方式如下:

  1. PrintWriter pw = resp.getWriter();
  2. pw.write("<html>");
  3. pw.write("<body>");
  4. pw.write("<h1>Welcome, " + name + "!</h1>");
  5. pw.write("</body>");
  6. pw.write("</html>");
  7. pw.flush();

上述这种输出HTML的步骤较为繁琐,而且容易出错,并且也不像直接编写HTML网页那么直观(比如JS、CSS语言),虽然全栈工程师前后端都可以care,不过在项目开发中还是前端和后端的东西各自分开最好,才能使两者的开发效率最高。
为了解决这个问题,后面出现了JSP(Java Server Pages)语言。
JSP代码类似于html语言,并且其中还可以嵌入Java代码,进行变量插入、变量输出等操作:

  1. //原址:https://www.liaoxuefeng.com/wiki/1252599548343744/1266262958498784
  2. <html>
  3. <head>
  4. <title>Hello World - JSP</title>
  5. </head>
  6. <body>
  7. <%-- JSP Comment --%>
  8. <h1>Hello World!</h1>
  9. <p>
  10. <%
  11. out.println("Your IP address is ");
  12. %>
  13. <span style="color:red">
  14. <%= request.getRemoteAddr() %>
  15. </span>
  16. </p>
  17. </body>
  18. </html>

不过这种方式(JSP)虽然可以很方便的输出HTML,并在其中实现一定的动态内容,运行简单的代码,但是写起Java来也忒不适应了吧,总感觉怪怪的。
另外,Servlet简化了Web应用的开发,可以基于Java语言实现复杂的业务逻辑,但是不太适合输出HTML。
那么,有没有一种方式可以结合两者,从而是两者相辅相成呢?
有的!那就是MVC!

MVC设计模式

先给出MVC设计模式的简单定义——Model-View-Controller:

  • Model(模型)是前后端定义的数据传输单元,通常前端只是需要后端提供一些数据来展示而已,比如用户名、处理结果来,需要HTML输出的东西;
  • View(视图)的工作就是接收后端发过来的Model,取出数据,然后写入Html中;
  • Controller(控制器)其实就是业务逻辑,类似于之前的Servlet开发,区别在于现在将处理结果都放在Model里,然后发给View;

来个例子吧,实现用户登录,获取信息
首先,我们编写了两个JavaBean(JavaBean简单来说就是一个可以放入数据和取出数据的类),也就是之前说的Model

  1. //源码:https://www.liaoxuefeng.com/wiki/1252599548343744/1266264917931808
  2. public class User {
  3. public long id;
  4. public String name;
  5. public School school;
  6. }
  7. public class School {
  8. public String name;
  9. public String address;
  10. }

接着,来实现Controller,在UserServlet中实现从数据库读取User、School等信息,并放入Request中,再发送给user.jsp,也就是View进行渲染处理:

  1. @WebServlet(urlPatterns = "/user")
  2. public class UserServlet extends HttpServlet {
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  4. // 假装从数据库读取:
  5. School school = new School("No.1 Middle School", "101 South Street");
  6. User user = new User(123, "Bob", school);
  7. // 放入Request中:
  8. req.setAttribute("user", user);
  9. // forward给user.jsp:
  10. req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp);
  11. }
  12. }

View——user.jsp负责将接收到的user信息进行渲染展示,即写入Html中:

  1. <%@ page import="com.itranswarp.learnjava.bean.*"%>
  2. <%
  3. User user = (User) request.getAttribute("user");
  4. %>
  5. <html>
  6. <head>
  7. <title>Hello World - JSP</title>
  8. </head>
  9. <body>
  10. <h1>Hello <%= user.name %>!</h1>
  11. <p>School Name:
  12. <span style="color:red">
  13. <%= user.school.name %>
  14. </span>
  15. </p>
  16. <p>School Address:
  17. <span style="color:red">
  18. <%= user.school.address %>
  19. </span>
  20. </p>
  21. </body>
  22. </html>

以上,就完成了一个简单的web应用,在浏览器访问http://localhost:8080/user,结果如下:
Java Web ——MVC基础框架讲解及代码演示 - 图1
捋一捋,用一个结构图来描述上述代码:
Java Web ——MVC基础框架讲解及代码演示 - 图2
简单来说,就是将整个Web开发分成了三个部分:逻辑处理、前端展示、数据传输,分别代表Controller、View、Model。
这种方式有什么好处?分工明确、开发简单。
Controller就好好关注逻辑处理,比如访问数据库啦、进行复杂的逻辑交互等等;View就关注如何让界面好看就完了,比如用模板引擎写出好看的界面,留几个空白给Model进行展示就ok了!

有关模板引擎的具体内容,可以了解:https://blog.csdn.net/qq_42266891/article/details/108265478

实现一个简单MVC框架

上文虽然结合JSP和Servlet可以发挥两者的优点,但是仍存在一些问题:

  • Servlet提供的接口仍然偏向底层,我想用纯粹的Java语言咋办;
  • JSP实现的页面比较简单,难以满足前端的各种骚操作,我想前端好看一点!

    其实,廖大实现了一个简单的MVC框架(可以更好地理解Spring MVC的底层知识),可以直接阅读: https://www.liaoxuefeng.com/wiki/1252599548343744/1337408645759009。 下面做一个简单介绍

首先,项目框架是这样的:
image.png
其中,bean其实就是后续Model将要用到的东西,根据具体的业务来定,User的具体实现如下:

  1. public class User {
  2. public String email;
  3. public String password;
  4. public String name;
  5. public String description;
  6. public User() {
  7. }
  8. public User(String email, String password, String name, String description) {
  9. this.email = email;
  10. this.password = password;
  11. this.name = name;
  12. this.description = description;
  13. }
  14. }

controller其实就是日常开发中的业务处理逻辑,UserController负责用户登录、注销、用户展示:

  1. public class UserController {
  2. private Map<String, User> userDatabase = new HashMap<String, User>() {
  3. {
  4. List<User> users = Arrays.asList( //
  5. new User("bob@example.com", "" +
  6. "", "Bob", "This is bob."),
  7. new User("tom@example.com", "tomcat", "Tom", "This is tom."));
  8. users.forEach(user -> {
  9. put(user.email, user);
  10. });
  11. }
  12. };
  13. @GetMapping("/signin")
  14. public ModelAndView signin() {
  15. return new ModelAndView("/signin.html");
  16. }
  17. @PostMapping("/signin")
  18. public ModelAndView doSignin(SignInBean bean, HttpServletResponse response, HttpSession session)
  19. throws IOException {
  20. User user = userDatabase.get(bean.email);
  21. if (user == null || !user.password.equals(bean.password)) {
  22. response.setContentType("application/json");
  23. PrintWriter pw = response.getWriter();
  24. pw.write("{\"error\":\"Bad email or password\"}");
  25. pw.flush();
  26. } else {
  27. session.setAttribute("user", user);
  28. response.setContentType("application/json");
  29. PrintWriter pw = response.getWriter();
  30. pw.write("{\"result\":true}");
  31. pw.flush();
  32. }
  33. return null;
  34. }
  35. @GetMapping("/signout")
  36. public ModelAndView signout(HttpSession session) {
  37. session.removeAttribute("user");
  38. return new ModelAndView("redirect:/");
  39. }
  40. @GetMapping("/user/profile")
  41. public ModelAndView profile(HttpSession session) {
  42. User user = (User) session.getAttribute("user");
  43. if (user == null) {
  44. return new ModelAndView("redirect:/signin");
  45. }
  46. return new ModelAndView("/profile.html", "user", user);
  47. }
  48. }

可以看到,编写业务处理逻辑只需要通过Get、Post注解来指明映射url地址即可,极为方便!返回的ModelAndView是给前端进行展示的东西,此处按下不表。

最后的framework就是MVC框架的核心内容,完全可以封装成一个包用作自己代码的后续开发。
首先,对于一个MVC框架来说,它需要一个分发器来接收所有的url,而后再根据请求内容将请求内容转发给对应的controller进行逻辑处理,分发器的实现

  1. //源码地址:https://www.liaoxuefeng.com/wiki/1252599548343744/1337408645759009
  2. @WebServlet(urlPatterns = "/")
  3. public class DispatcherServlet extends HttpServlet {
  4. private final Map<String, GetDispatcher> getMappings = new HashMap<>();
  5. private final Map<String, PostDispatcher> postMappings = new HashMap<>();
  6. // 指定package并自动扫描:
  7. private final List<Class<?>> controllers = Arrays.asList(IndexController.class, UserController.class);
  8. private ViewEngine viewEngine;
  9. //扫描指定package中所有的Get和Post注解,获取对应的controller处理方法
  10. @Override
  11. public void init() throws ServletException {
  12. // 利用反射,来获取所有Get和Post注解对应的method
  13. ......
  14. // 创建ViewEngine:
  15. this.viewEngine = new ViewEngine(getServletContext());
  16. }
  17. // 处理get请求
  18. @Override
  19. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  20. try {
  21. process(req, resp, this.getMappings);
  22. } catch (PebbleException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. // 处理post请求
  27. @Override
  28. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  29. try {
  30. process(req, resp, this.postMappings);
  31. } catch (PebbleException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. // 主要逻辑是根据getMappings和postMappings获取指定的method,并通过反射调用
  36. private void process(HttpServletRequest req, HttpServletResponse resp,
  37. Map<String, ? extends AbstractDispatcher> dispatcherMap) throws ServletException, IOException, PebbleException {
  38. ......
  39. }
  40. }

上面所提到的注解,实现起来很简单:

  1. @Target({ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface GetMapping {
  4. String value();
  5. }
  6. @Target({ElementType.METHOD})
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface PostMapping {
  9. String value();
  10. }

之前提到的ModelAndView,其实主要有两个作用,一是指明模板html的地址,二是获取要展示的内容:

  1. public class ModelAndView {
  2. Map<String, Object> model;
  3. String view;
  4. public ModelAndView(String view) {
  5. this.view = view;
  6. this.model = new HashMap<>();
  7. }
  8. public ModelAndView(String view, String name, Object value) {
  9. this.view = view;
  10. this.model = new HashMap<>();
  11. this.model.put(name, value);
  12. }
  13. public ModelAndView(String view, Map<String, Object> model) {
  14. this.view = view;
  15. this.model = new HashMap<>(model);
  16. }
  17. }

上述代码中,string view简单来说就是某一个html文件的路径,一般是通过模板引擎生成的,有关模板引擎可以参考:模板引擎.
另一个变量model,就是之前定义的bean,以获取用户信息为例:

  1. @GetMapping("/user/profile")
  2. public ModelAndView profile(HttpSession session) {
  3. User user = (User) session.getAttribute("user");
  4. if (user == null) {
  5. return new ModelAndView("redirect:/signin");
  6. }
  7. return new ModelAndView("/profile.html", "user", user);
  8. }

最后还需要一个View对内容进行前端渲染,简单来说就是将需要展示的内容写入html中:

  1. public class ViewEngine {
  2. private final PebbleEngine engine;
  3. public ViewEngine(ServletContext servletContext) {
  4. // 定义一个ServletLoader用于加载模板:
  5. ServletLoader loader = new ServletLoader(servletContext);
  6. // 模板编码:
  7. loader.setCharset("UTF-8");
  8. // 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录:
  9. loader.setPrefix("/WEB-INF/templates");
  10. // 模板后缀:
  11. loader.setSuffix("");
  12. // 创建Pebble实例:
  13. this.engine = new PebbleEngine.Builder()
  14. .autoEscaping(true) // 默认打开HTML字符转义,防止XSS攻击
  15. .cacheActive(false) // 禁用缓存使得每次修改模板可以立刻看到效果
  16. .loader(loader).build();
  17. }
  18. public void render(ModelAndView mv, Writer writer) throws IOException, PebbleException {
  19. // 查找模板:
  20. PebbleTemplate template = this.engine.getTemplate(mv.view);
  21. // 渲染:
  22. template.evaluate(writer, mv.model);
  23. }
  24. }

对以上内容做一个总结:
Java Web ——MVC基础框架讲解及代码演示 - 图4

补充资料

重定向与转发
Redirect(重定向):服务器给浏览器回复一个302码同时附带新的url,浏览器接收到302响应后就会向新的url发送请求报文(浏览器发起了两次请求),如下:
Java Web ——MVC基础框架讲解及代码演示 - 图5
Foward(转发):在Servlet内部进行转发,而不会发送到浏览器在对新的url进行请求,如下:
Java Web ——MVC基础框架讲解及代码演示 - 图6
Session与Cookie
在Web应用程序中,经常需要保持用户的登录状态(不用频繁的输入账号密码),即我们需要有一个方式可以让Web应用能够识别用户的身份。

Servlet提供了一个基于Cookie的识别机制——Session。
Cookie是服务器给浏览器分配的唯一ID,并以Cookie的形式发送给浏览器,在后续的访问中,浏览器在请求报文中附带此Cookie,从而服务器可以识别其用户身份。

Session有什么缺点?
由于Session需要将用户数据保存在内存中,一方面,需要耗费一定的服务器资源;另一方面,当采用服务器集群时,必须保证同一个用户映射到固定的服务器上,这需要反向代理服务器的支持,比如采取Hash等方式将其转发至固定的服务器。
总之,由于Session的存在会使得服务器集群扩展较为艰难。
PS:Cookie只是请求报文中附加的信息而已,除了Cookie,还可以通过隐藏表单、URL末尾附加ID来追踪Session(只要有保证唯一性的标识即可)。
PS:所以平时的登录操作,可以看作是服务器给浏览器分配了一个Session ID;登出或者较长时间不操作,则从浏览器中移除这个ID,下次登录的时候就需要再登录了。

参考资料

廖老师的网站