day08-SpringMVC底層機制簡單實現-04

一刀一個小西瓜發表於2023-02-13

SpringMVC底層機制簡單實現-04

https://github.com/liyuelian/springmvc-demo.git

8.任務7-完成簡單檢視解析

功能說明:透過目標方法返回的 String,轉發或重定向到指定頁面

8.1分析

原生的 SpringMVC 使用檢視解析器來對 Handler 方法返回的 String(該String會轉為檢視類)進行解析,然後轉發或重定向到指定頁面。

這裡為了簡化,直接在自定義的前端控制器編寫方法完成檢視解析器的功能。

image-20230212213432844

8.2程式碼實現

(1)修改 MyDispatcherServlet 的 executeDispatch 方法

部分程式碼:

//編寫方法,完成分發請求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
    MyHandler myHandler = getMyHandler(request);
    try {
        //如果 myHandler為 null,說明請求 url沒有匹配的方法,即使用者請求的資源不存在
        if (myHandler == null) {
            response.getWriter().print("<h1>404 NOT FOUND</h1>");
        } else {//匹配成功,就反射呼叫控制器的方法
            //1.先獲取目標方法的所有形參的引數資訊
            Class<?>[] parameterTypes = myHandler.getMethod().getParameterTypes();
            //2.建立一個引數陣列(對應實引數組),在後面反射調動目標方法時會用到
            Object[] params = new Object[parameterTypes.length];
            //遍歷形引數組 parameterTypes,根據形引數組的資訊,將實參填充到實引數組中

            //步驟一:將方法的 request 和 response 引數封裝到引數陣列,進行反射呼叫
            for (int i = 0; i < parameterTypes.length; i++) {
                //....
                //....略
                //....
            }
            //步驟二:將 http請求的引數封裝到 params陣列中[要注意填充實引數組的順序問題]
            //先處理中文亂碼問題
            request.setCharacterEncoding("utf-8");
            Map<String, String[]> parameterMap = request.getParameterMap();
            // 遍歷 parameterMap,將請求引數按照順序填充到實引數組 params
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                //....
                //....略
                //....
            }
            //反射呼叫目標方法
            Object result = myHandler.getMethod()
                .invoke(myHandler.getController(), params);
            //對返回的結果進行解析(原生的SpringMVC透過檢視解析器來完成)
            if (result instanceof String) {
                String viewName = (String) result;
                System.out.println("viewName=" + viewName);
                if (viewName.contains(":")) {//如果返回的String結果為 forward:/login_ok.jsp
                    // 或 redirect:/login_ok.jsp 的形式
                    String viewType = viewName.split(":")[0]; // forward或redirect
                    String viewPage = viewName.split(":")[1]; // 要跳轉的頁面名
                    //判斷是 forward 還是 redirect
                    if ("forward".equals(viewType)) {//請求轉發
                        request.getRequestDispatcher(viewPage)
                                .forward(request, response);
                    } else if ("redirect".equals(viewType)) {//重定向
                        //注意這裡的路徑問題
                        viewPage = request.getContextPath() + viewPage;
                        response.sendRedirect(viewPage);
                    }
                } else {//如果兩者都沒有,預設為請求轉發
                    request.getRequestDispatcher("/" + viewName)
                            .forward(request, response);
                }
            }//這裡還可以擴充
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

(2)建立測試頁面和測試方法

MonsterService 介面:

package com.li.service;

import com.li.entity.Monster;

import java.util.List;

/**
 * @author 李
 * @version 1.0
 */
public interface MonsterService {
    //增加方法,處理登入
    public boolean login(String name);
}

MonsterServiceImpl 實現類:

package com.li.service.impl;

import com.li.entity.Monster;
import com.li.myspringmvc.annotation.Service;
import com.li.service.MonsterService;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 李
 * @version 1.0
 * MonsterServiceImpl 作為一個Service物件注入容器
 */
@Service
public class MonsterServiceImpl implements MonsterService {
    @Override
    public boolean login(String name) {
        //模擬DB
        if ("白骨精".equals(name)) {
            return true;
        } else {
            return false;
        }
    }
}

MonsterController 控制器:

package com.li.controller;

import com.li.entity.Monster;
import com.li.myspringmvc.annotation.AutoWired;
import com.li.myspringmvc.annotation.Controller;
import com.li.myspringmvc.annotation.RequestMapping;
import com.li.myspringmvc.annotation.RequestParam;
import com.li.service.MonsterService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

/**
 * @author 李
 * @version 1.0
 * 用於測試的 Controller
 */
@Controller
public class MonsterController {
    //屬性
    @AutoWired
    private MonsterService monsterService;

    //處理登入的方法,返回要請求轉發或重定向的字串
    @RequestMapping(value = "/monster/login")
    public String login(HttpServletRequest request,
                        HttpServletResponse response,
                        @RequestParam(value = "monsterName") String mName) {
        System.out.println("----接收到的mName-->" + mName);
        request.setAttribute("mName", mName);
        boolean b = monsterService.login(mName);
        if (b) {//登入成功
            // 請求轉發到login_ok.jsp
            //return "forward:/login_ok.jsp";
            //return "redirect:/login_ok.jsp";
            return "login_ok.jsp";
        } else {//登入失敗
            //return "forward:/login_error.jsp";
            //return "redirect:/login_error.jsp";
            return "login_error.jsp";
        }
    }
}

在webapp目錄下分別建立 login.jsp,login_ok.jsp,login_error.jsp

login.jsp:

<%--
  Created by IntelliJ IDEA.
  User: li
  Date: 2023/2/12
  Time: 22:24
  Version: 1.0
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登入頁面</title>
</head>
<body>
<h1>登入頁面</h1>
<form action="monster/login" method="post">
    妖怪名:<input type="text" name="monsterName"><br/>
    <input type="submit" value="登入">
</form>
</body>
</html>

login_ok.jsp:

<%--
  Created by IntelliJ IDEA.
  User: li
  Date: 2023/2/12
  Time: 22:27
  Version: 1.0
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登入成功</title>
</head>
<body>
<h1>登入成功</h1>
歡迎你:${requestScope.mName}
</body>
</html>

login_error.jsp:

<%--
  Created by IntelliJ IDEA.
  User: li
  Date: 2023/2/12
  Time: 22:28
  Version: 1.0
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登入失敗</title>
</head>
<body>
<h1>登入失敗</h1>
sorry,登入失敗 ${requestScope.mName}
</body>
</html>

(3)啟動 tomcat,訪問 http://localhost:8080/li_springmvc/login.jsp

day08-SpringMVC底層機制簡單實現-04

測試成功。

9.任務8-自定義@ResponseBody

9.1分析

功能說明:透過自定義@ResponseBody 註解,返回 JSON格式資料

在實際開發中,前後端分離的專案,通常是直接json資料給客戶端/瀏覽器。客戶端接收到資料後,再自己決定如何處理和顯示。

9.2程式碼實現

(1)@ResponseBody 註解

package com.li.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * @author 李
 * @version 1.0
 * ResponseBody 註解用於指定目標方法是否要返回指定格式的資料
 * 如果value為預設值,或者value="json",認為目標方法要返回的資料格式為json
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
    String value() default "";
}

(2)修改 MyDispatcherServlet 的 executeDispatch 方法

//編寫方法,完成分發請求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
    MyHandler myHandler = getMyHandler(request);
    try {
        //如果 myHandler為 null,說明請求 url沒有匹配的方法,即使用者請求的資源不存在
        if (myHandler == null) {
            response.getWriter().print("<h1>404 NOT FOUND</h1>");
        } else {//匹配成功,就反射呼叫控制器的方法
            Class<?>[] parameterTypes = myHandler.getMethod().getParameterTypes();
            //2.建立一個引數陣列(對應實引數組),在後面反射調動目標方法時會用到
            Object[] params = new Object[parameterTypes.length];
            //遍歷形引數組 parameterTypes,根據形引數組的資訊,將實參填充到實引數組中
            //...
            //...
            //...
            //...
        
            //反射呼叫目標方法
            Object result = 
                myHandler.getMethod().invoke(myHandler.getController(), params);
            //對返回的結果進行解析(原生的SpringMVC透過檢視解析器來完成)
            if (result instanceof String) {
              //....略
            }//這裡還可以擴充
            else if (result instanceof ArrayList) {//如果是一個集合
                Method method = myHandler.getMethod();
                //判斷目標方法是否有一個@ResponseBody註解
                if (method.isAnnotationPresent(ResponseBody.class)) {
                    String valueType = method.getAnnotation(ResponseBody.class).value();
                    //如果註解的為預設值,或者value="json",就認為目標方法要返回的資料格式為json
                    if ("json".equals(valueType) || "".equals(valueType)) {
                        //對Arraylist轉為json字串
                        //這裡我們使用jackson包下的工具類解決
                        ObjectMapper objectMapper = new ObjectMapper();
                        String resultJson = objectMapper.writeValueAsString(result);
                        //這裡簡單處理,就直接返回
                        response.setContentType("text/html;charset=utf-8");
                        PrintWriter writer = response.getWriter();
                        writer.write(resultJson);
                        writer.flush();
                        writer.close();
                    }
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

(3)pom.xml檔案中引入jackson

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.4</version>
</dependency>

(4)MonsterController 測試類增加方法測試

/**
 * 編寫方法,返回json格式的資料
 * 1.目標方法返回的結果是給SpringMVC底層透過反射呼叫的位置
 * 2.我們在SpringMVC底層反射呼叫的位置接收到結果並進行解析即可
 * 3. @ResponseBody(value = "json") 表示希望以json格式返回資料給瀏覽器
 * @param request
 * @param response
 * @return
 */
@RequestMapping(value = "/monster/list/json")
@ResponseBody(value = "json")
public List<Monster> listMonsterByJson(HttpServletRequest request,
                                       HttpServletResponse response) {
    List<Monster> monsters = monsterService.listMonster();
    return monsters;

}

(5)啟動 tomcat,瀏覽器訪問 http://localhost:8080/li_springmvc/monster/list/json,返回如下結果,測試成功。

image-20230213200853614

10.小結

SpringMVC機制梳理

  1. web.xml 中配置前端控制器(DispatcherServlet)和 spring 容器檔案

  2. 當啟動 tomcat 時,DispatcherServlet 被 tomcat 建立

  3. 前端控制器工作:

    • (1)建立 spring 容器並初始化(從 web.xml 檔案中獲取 spring配置檔名):

      • a. 掃描包,獲取要注入的類的全路徑。

      • b. 將掃描到的類進行反射,放入ioc容器。

      • c. 完成屬性自動裝配

    • (2)記錄控制器的目標方法和 url 的對映關係(在原生 SpringMVC 中,這個工作由 HandlerMapping 完成)

    • (3)完成分發請求:

      • a. 完成使用者 url 和控制器 url 的匹配以及目標方法的呼叫

      • b. 目標方法引數的自動賦值:對瀏覽器請求 url 的引數進行處理,考慮目標方法形參的多樣性,將其封裝到引數陣列,以反射呼叫的形式傳遞給目標方法

        目標方法的實參是在 SpringMVC 底層透過封裝好的引數陣列傳入的

      • c. 反射目標方法,對目標方法返回的結果進行解析(原生SpringMVC中,解析的工作由檢視解析器完成),決定請求轉發/重定向/返回 json 格式的資料等

相關文章