SpringMVC系列八: 手动实现SpringMVC底层机制-下

手动实现SpringMVC底层机制-下

在本篇文章中,我们将继续深入探讨如何手动实现SpringMVC的底层机制。通过这两部分的学习,你将全面理解SpringMVC的工作原理。


⬅️ 上一讲 : SpringMVC系列七: 手动实现SpringMVC底层机制-上



🔧 需要用到的项目 : zzw-springmvc项目


实现任务阶段五

🍍完成Spring容器对象的自动装配-@Autowired

说明: 完成Spring容器中对象的注入/自动装配

示意图[分析说明]

分析:

加入@Autowired注解, 进行对象属性的装配. -如图

测试:

浏览器输入 http://localhost:8080/monster/list, 返回列表信息

代码实现:

1.在com.zzw.zzwspringmvc.annotation下新建@Autowired

java 复制代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    String value() default "";
}

1.1MonsterController添加属性monsterService, 标注@Autowired

java 复制代码
@Controller
public class MonsterController {

    //@Autowired表示要完成属性的装配.
    @Autowired
    private MonsterService monsterService;

	.....
}

2.ZzwWebApplicationContext增加方法executeAutowired()

java 复制代码
//编写方法, 完成属性的自动装配
public void executeAutowired() {
    //判断ioc有没有要装配的对象
    if (ioc.isEmpty()) {
        return;//你也可以抛出异常, throw new RuntimeException("ioc 容器没有bean对象")
    }

    //遍历ioc容器中的所有注入的bean对象, 然后获取到bean的所有字段/属性, 判断是否需要装配
    /**
     * entry => <String, Object> -> String 就是你注入对象时的名称, Object就是bean对象
     */
    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        
        //String key = entry.getKey();
        Object bean = entry.getValue();
        
        //获取bean的所有字段/属性
        Field[] declaredFields = bean.getClass().getDeclaredFields();
        for (Field declaredField : declaredFields) {
            //判断当前这个字段, 是否有@Autowired
            if (declaredField.isAnnotationPresent(Autowired.class)) {//有@Autowired
                //得到当前这个字段的@Autowired
                Autowired autowiredAnnotation = declaredField.getDeclaredAnnotation(Autowired.class);
                String beanName = autowiredAnnotation.value();
                if ("".equals(beanName)) {//如果没有设置value, 按照默认规则
                    //即得到字段类型名称的首字母小写, 作为名字来进行装配
                    Class<?> type = declaredField.getType();
                    beanName = type.getSimpleName().substring(0, 1).toLowerCase()
                            + type.getSimpleName().substring(1);
                }

                //如果设置了value, 直接按照beanName来装配
                //从ioc容器中获取bean
                if (ioc.get(beanName) == null) {//说明你指定的名字对应的bean不在ioc容器
                    throw new RuntimeException("ioc容器中, 不存在你要装配的bean");
                }
                //防止属性是private, 我们需要暴力破解
                declaredField.setAccessible(true);
                try {
                    declaredField.set(bean, ioc.get(beanName));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

3.ZzwWebApplicationContext.javainit()方法的最后添加三行代码

java 复制代码
//编写方法, 完成自己的spring容器的初始化
public void init() {
    //这里我们写的是固定的spring容器配置文件 => 做活
    //String basePackage = XMLParser.getBasePackage("zzwspringmvc.xml");
    String basePackage =
            XMLParser.getBasePackage(configLocation.split(":")[1]);
    //这时basePackages => com.zzw.controller, com.zzw.service
    String[] basePackages = basePackage.split(",");
    if (basePackages.length > 0) {
        for (String pack : basePackages) {
            scanPackage(pack.trim());
        }
    }
    System.out.println("basePackage=" + basePackage);
    System.out.println("classFullPathList=" + classFullPathList);
    //将扫描到的类, 反射到ioc容器
    executeInstance();
    System.out.println("扫描后 ioc容器=" + ioc);

    //完成注入的bean对象,的属性的装配
    executeAutowired();
    System.out.println("装配后 ioc容器=" + ioc);

3.打断点, debug, 重启tomcat


4.修改MonsterControllerlistMonster方法

java 复制代码
@RequestMapping(value = "/monster/list")
public void listMonster(HttpServletRequest request, HttpServletResponse response) {
    //设置返回编码和返回类型
    response.setContentType("text/html;charset=utf-8");

    StringBuilder content = new StringBuilder("<h1>妖怪列表信息</h1>");//单线程使用StringBuilder
    content.append("<table border='1px' width='500px' style='border-collapse:collapse'>");
    //调用monsterService
    List<Monster> monsters = monsterService.listMonster();
    for (Monster monster : monsters) {
        content.append("<tr><td>" + monster.getId() + "</td><td>" + monster.getName() +
                "</td><td>" + monster.getAge() + "</td><td>" + monster.getSkill() + "</td></tr>");
    }
    content.append("</table>");
    //获取writer返回信息
    try {
        response.getWriter().write(content.toString());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

5.重启tomcat, 浏览器输入 http://localhost:8080/monster/list

实现任务阶段六

🍍完成控制器方法获取参数-@RequestParam

功能说明: 自定义@RequestParam 和 方法参数名获取参数

完成: 将 方法的 HttpServletRequest 和 HttpServletResponse 参数封装到数组, 进行反射调用

完成: 在方法参数 指定 @RequestParam 的参数封装到参数数组, 进行反射调用

完成: 在方法参数 没有指定 @RequestParam, 按照默认参数名获取值, 进行反射调用

示意图[分析说明]

1.🥦将 方法的 HttpServletRequest 和 HttpServletResponse 参数封装到数组, 进行反射调用

修改ZzwDispatcherServletexecuteDispatcher()方法

java 复制代码
//编写方法, 完成分发请求任务
private void executeDispatcher(HttpServletRequest request,
                               HttpServletResponse response) {
    try {
        ZzwHandler zzwHandler = getZzwHandler(request);
        if (zzwHandler == null) {//说明用户请求的路径/资源不存在
            response.getWriter().print("<h1>404 NOT FOUND!</h1>");
        } else {//匹配成功, 反射调用控制器的方法

            //目标将: HttpServletRequest 和 HttpServletResponse 封装到参数数组
            //1. 得到目标方法的所有形参参数信息[返回对应的数组]
            Class<?>[] parameterTypes =
                    zzwHandler.getMethod().getParameterTypes();

            //2. 创建一个参数数组[对应实参数组], 在后面反射调用目标方法时, 会使用到
            Object[] params = new Object[parameterTypes.length];

            //3.遍历parameterTypes形参数组, 根据形参数组信息, 将实参填充到实参数组中
            for (int i = 0; i < parameterTypes.length; i++) {
                //取出每一个形参"类型"
                Class<?> parameterType = parameterTypes[i];
                //如果这个形参类型是HttpServletRequest, 将request填充到params
                //在原生的SpringMVC中, 是按照类型来进行匹配的, 老师这里简化, 使用名字来匹配
                if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                    params[i] = request;
                } else if("HttpServletResponse".equals(parameterType.getSimpleName())){
                    params[i] = response;
                }
            }

            /**
             * 1.下面这样写, 其实是针对目标方法 m(HttpServletRequest request, HttpServletResponse response)
             * zzwHandler.getMethod()
             *              .invoke(zzwHandler.getController(), request, response)
             * 2.这里我们准备将需要传递给目标方法的 实参 => 封装到参数数组
             *   => 然后以反射调用的方式传递给目标方法
             * 源码: public Object invoke(Object var1, Object... var2)...
             */
            zzwHandler.getMethod().
                    invoke(zzwHandler.getController(), params);
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

2.🥦在方法形参处, 指定 @RequestParam, 将对应的实参封装到参数数组, 进行反射调用

1.MonsterService新添方法findMonsterByName

java 复制代码
public interface MonsterService {
    //增加方法, 通过传入的name, 返回对应的monster列表
    public List<Monster> findMonsterByName(String name);
}

MonsterServiceImpl将其实现

java 复制代码
@Service
public class MonsterServiceImpl implements MonsterService {

    public List<Monster> findMonsterByName(String name) {
        //这里我们模拟数据->DB
        List<Monster> monsters = new ArrayList<Monster>();
        monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
        monsters.add(new Monster(200, "汤姆猫", "抓老鼠", 200));
        monsters.add(new Monster(300, "红孩儿", "三昧真火", 100));
        monsters.add(new Monster(400, "黄袍怪", "吐烟雾", 300));
        monsters.add(new Monster(500, "白骨精", "美人计", 800));

        //创建集合并且返回查询到的monster集合
        List<Monster> findMonsters = new ArrayList<Monster>();
        //遍历monsters集合, 返回满足条件的对象
        for (Monster monster : monsters) {
            if (monster.getName().contains(name)) {
                findMonsters.add(monster);
            }
        }
        return findMonsters;
    }
}

2.com.zzw.zzwspringmvc.annotation下新建@RequestParam

java 复制代码
/**
 * @author 赵志伟
 * @version 1.0
 * RequestParam 注解 标注在目标方法的参数上, 表示对应http请求的参数
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)//runtime表示在反射时可以拿到这个注解
@Documented
public @interface RequestParam {
    String value() default "";
}

3.ZzwDispatcherServlet增添代码

注意点
1)method.getParameters(): 得到所有的形参
2)method.getParameterTypes(): 得到形参的类型

java 复制代码
//编写方法, 完成分发请求任务
private void executeDispatcher(HttpServletRequest request,
                               HttpServletResponse response) {
    try {
        ZzwHandler zzwHandler = getZzwHandler(request);
        if (zzwHandler == null) {//说明用户请求的路径/资源不存在
            response.getWriter().print("<h1>404 NOT FOUND!</h1>");
        } else {//匹配成功, 反射调用控制器的方法

            //目标将: HttpServletRequest 和 HttpServletResponse 封装到参数数组
            //1. 得到目标方法的所有形参参数信息[对应的数组]
            Class<?>[] parameterTypes =
                    zzwHandler.getMethod().getParameterTypes();

            //2. 创建一个参数数组[对应实参数组], 在后面反射调用目标方法时, 会使用到
            Object[] params = new Object[parameterTypes.length];

            //3.遍历parameterTypes形参数组, 根据形参数组信息, 将实参填充到实参数组中
            for (int i = 0; i < parameterTypes.length; i++) {
                //取出每一个形参类型
                Class<?> parameterType = parameterTypes[i];
                //如果这个形参是HttpServletRequest, 将request填充到params
                //在原生的SpringMVC中, 是按照类型来进行匹配的, 老师这里简化, 使用名字来匹配
                if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                    params[i] = request;
                } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                    params[i] = response;
                }
            }

            //将http请求参数封装到params数组中, 老韩提示, 要注意填充实参的时候, 顺序问题 👈

            //1.获取http请求的参数集合
            //老韩解读
            //2.返回的Map<String, String[]> String: 表示http请求的参数名
            //  String[]: 表示http请求的参数值, 想一下为什么是数组?
            //http://localhost:8080/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒&hobby=吃肉(防止有类似checkbox)
            Map<String, String[]> parameterMap =
                    request.getParameterMap();

            //3.遍历parameterMap, 将请求参数, 按照顺序填充到实参数组
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {

                //取出key. 这个name就是对应请求的参数名
                String name = entry.getKey();
                //说明: 只考虑提交的数据是单值的情况, 即不考虑类似checkbox提交的数据
                //     老师这里做了简化, 如果考虑多值情况, 也不难
                String value = entry.getValue()[0];

                //我们得到请求的参数对应我们目标方法的第几个形参, 然后将其填充
                //这里专门编写一个方法, 得到请求参数对应的是第几个形参
                //1. API 2.java内力真正增加 3.老韩忠告..
                int indexRequestParameterIndx =
                        getIndexRequestParameterIndex(zzwHandler.getMethod(), name);
                if (indexRequestParameterIndx != -1) {//找到对应的位置
                    params[indexRequestParameterIndx] = value;
                } else {//说明并没有找到@RequestParam注解对应的参数, 就会使用默认的机制进行匹配[待...]
                    //一会再写
                }
            }                                                                 👈
            /**
             * 1.下面这样写, 其实是针对目标方法 m(HttpServletRequest request, HttpServletResponse response)
             * zzwHandler.getMethod()
             *              .invoke(zzwHandler.getController(), request, response)
             * 2.这里我们准备将需要传递给目标方法的 实参=> 封装到参数数组=> 然后以反射调用的方式传递给目标方法
             * public Object invoke(Object var1, Object... var2)...
             */
            zzwHandler.getMethod().
                    invoke(zzwHandler.getController(), params);
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

//编写方法, 返回请求参数是目标方法的第几个形参

/**
 * @param method 目标方法
 * @param name   请求的参数名
 * @return 是目标方法的第几个形参
 */
public int getIndexRequestParameterIndex(Method method, String name) {
    //1.得到method的所有形参参数
    Parameter[] parameters = method.getParameters();
    for (int i = 0; i < parameters.length; i++) {
        //取出当前的参数
        Parameter parameter = parameters[i];
        //判断parameter是不是有@RequestParam注解
        boolean annotationPresent = parameter.isAnnotationPresent(RequestParam.class);
        if (annotationPresent) {//说明有@RequestParam
            //取出当前这个参数的@RequestParam(value = "xxx")
            RequestParam requestParamAnnotation =
                    parameter.getAnnotation(RequestParam.class);
            String value = requestParamAnnotation.value();
            //这里就是匹配的比较
            if (name.equals(value)) {
                return i;//找到请求的参数, 对应的目标方法的形参的位置
            }
        }
    }
    //如果没有匹配成功, 就返回-1
    return -1;
}

4.MonsterController增加如下方法

java 复制代码
//增加方法, 通过name返回对应的monster对象
@RequestMapping(value = "/monster/find")
public void findMonsterByName(HttpServletRequest request,
                              HttpServletResponse response,
                              @RequestParam(value = "name") String name) {
    //设置返回编码和返回类型
    response.setContentType("text/html;charset=utf8");
    System.out.println("---接收到的name---" + name);👈
    StringBuilder content = new StringBuilder("<h1>妖怪列表信息</h1>");//单线程使用StringBuilder
    content.append("<table border='1px' width='500px' style='border-collapse:collapse'>");
    //调用monsterService
    List<Monster> monsters = monsterService.findMonsterByName(name);👈
    for (Monster monster : monsters) {
        content.append("<tr><td>" + monster.getId() + "</td><td>" + monster.getName() + "</td><td>"
                + monster.getAge() + "</td><td>" + monster.getSkill() + "</td></tr>");
    }
    content.append("</table>");

    //获取writer返回信息
    try {
        response.getWriter().write(content.toString());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

5.测试

3.🥦在方法形参 没有指定 @RequestParam, 按照默认参数名获取值, 进行反射调用

1.去掉MonsterControllerfindMonsterByName()方法的name字段的@RequestParam注解

java 复制代码
//增加方法, 通过name返回对应的monster对象
@RequestMapping(value = "/monster/find")
public void findMonsterByName(HttpServletRequest request,
                              HttpServletResponse response,
                              String name) {
        .....
}

2.ZzwDispatcherServletexecuteDispatcher()else分支内增加如下代码, 并增加方法getParameterNames

java 复制代码
//编写方法, 完成分发请求任务
private void executeDispatcher(HttpServletRequest request,
                               HttpServletResponse response) {
    try {
        ZzwHandler zzwHandler = getZzwHandler(request);
        if (zzwHandler == null) {//说明用户请求的路径/资源不存在
            response.getWriter().print("<h1>404 NOT FOUND!</h1>");
        } else {//匹配成功, 反射调用控制器的方法

            //目标将: HttpServletRequest 和 HttpServletResponse 封装到参数数组
            //1. 得到目标方法的所有形参参数信息[对应的数组]
            Class<?>[] parameterTypes =
                    zzwHandler.getMethod().getParameterTypes();

            //2. 创建一个参数数组[对应实参数组], 在后面反射调用目标方法时, 会使用到
            Object[] params = new Object[parameterTypes.length];

            //3.遍历parameterTypes形参数组, 根据形参数组信息, 将实参填充到实参数组中
            for (int i = 0; i < parameterTypes.length; i++) {
                //取出每一个形参类型
                Class<?> parameterType = parameterTypes[i];
                //如果这个形参是HttpServletRequest, 将request填充到params
                //在原生的SpringMVC中, 是按照类型来进行匹配的, 老师这里简化, 使用名字来匹配
                if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                    params[i] = request;
                } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                    params[i] = response;
                }
            }

            //将http请求参数封装到params数组中, 老韩提示, 要注意填充实参的时候, 顺序问题

            //1.获取http请求的参数集合
            //老韩解读
            //http://localhost:8080/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒&hobby=吃肉(防止有类似checkbox)
            //2.返回的Map<String, String[]> String: 表示http请求的参数名
            //  String[]: 表示http请求的参数值, 为什么是数组
            //
            Map<String, String[]> parameterMap = request.getParameterMap();

            //2.遍历parameterMap, 将请求参数, 按照顺序填充到实参数组
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {

                //取出key. 这个name就是对应请求的参数名
                String name = entry.getKey();
                //说明: 只考虑提交的数据是单值的情况, 即不考虑类似checkbox提交的数据
                //     老师这里做了简化, 如果考虑多值情况, 也不难
                String value = entry.getValue()[0];

                //我们得到请求的参数对应我们目标方法的第几个形参, 然后将其填充
                //这里专门编写一个方法, 得到请求参数对应的是第几个形参
                //1. API 2.java内力真正增加 3.老韩忠告..
                int indexRequestParameterIndx =
                        getIndexRequestParameterIndex(zzwHandler.getMethod(), name);
                if (indexRequestParameterIndx != -1) {//找到对应的位置
                    params[indexRequestParameterIndx] = value;
👉👉👉👉👉👉👉} else {//说明并没有找到@RequestParam注解对应的参数, 就会使用默认的机制进行配置[待...]
                    //思路 														
                    //1.得到目标方法的所有形参的名称, 而不是形参类型的名称-专门编写一个方法获取形参名
                    //2.对得到的目标方法的所有形参名进行遍历, 如果匹配就把当前请求的参数值, 填充到params
                    List<String> parameterNames = getParameterNames(zzwHandler.getMethod());
                    for (int i = 0; i < parameterNames.size(); i++) {
                        //如果请求参数名和目标方法的形参名一样, 说明匹配成功
                        if (name.equals(parameterNames.get(i))) {
                            params[i] = value;//填充到实参数组
                            break;
                        }
                    }                  									
                }
            }
            /**
             * 1.下面这样写, 其实是针对目标方法 m(HttpServletRequest request, HttpServletResponse response)
             * zzwHandler.getMethod()
             *              .invoke(zzwHandler.getController(), request, response)
             * 2.这里我们准备将需要传递给目标方法的 实参=> 封装到参数数组=> 然后以反射调用的方式传递给目标方法
             * public Object invoke(Object var1, Object... var2)...
             */
            zzwHandler.getMethod().
                    invoke(zzwHandler.getController(), params);
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

//编写方法, 得到目标方法的所有形参的名称, 并放入到集合中返回

/**
 * @param method 目标方法
 * @return 所有形参的名称, 并放入到集合中返回
 */
public List<String> getParameterNames(Method method) {
    List<String> parametersList = new ArrayList<String>();
    //获取到所有的参数名称, 而不是参数类型的名称
    //这里有一个小细节-->在默认情况下 parameter.getName()
    //得到的名字不是形参真正的名字, 而是 [arg0, arg1, arg2...]
    //, 这里我们要引入一个插件, 使用java8的特性, 这样才能解决
    Parameter[] parameters = method.getParameters();
    //遍历parameters, 取出名字, 放入parametersList
    for (Parameter parameter : parameters) {
        parametersList.add(parameter.getName());
    }
    System.out.println("目标方法的形参参数列表=" + parametersList);
    return parametersList;
}

3.parameter.getName()得到的名字不是形参真正的名字, 而是 [arg0, arg1, arg2...].

Parameter[] parameters = method.getParameters();

parameters=[javax.servlet.http.HttpServletRequest arg0,
javax.servlet.http.HttpServletResponse arg1,
java.lang.String arg2]

parameter.getName() ⇒ [arg0, arg1, arg2]

我们需要引入一个插件.

pom.xmlbuild节点内插入以下代码 -parameters, 点击右上角刷新

xml 复制代码
<build>
  <finalName>zzw-springmvc2(你的文件名)</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.7.0</version>
      <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <compilerArgs>
          <arg>-parameters</arg>
        </compilerArgs>
        <encoding>utf-8</encoding>
      </configuration>
    </plugin>
  </plugins>
</build>

3.1点击maven管理, Lifecycle目录下, clean项目, 重启(不是重新部署)tomcat.

5.测试

实现任务阶段七

🍍完成简单视图解析

功能说明: 通过方法返回的String, 转发或者重定向到指定页面

●完成任务说明

-用户输入白骨精, 可以登陆成功, 否则失败

-根据登陆的结果, 可以重定向或者请求转发到 login_ok.jsp / login_error.jsp, 并显示妖怪名

-思路分析示意图

1.在webapp目录下新建

1)login.jsp

html 复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登陆页面</title>
</head>
<body>
<h1>登陆页面</h1>
<form action="?" method="post">
    妖怪名: <input type="text" name="mName"/><br/>
    <input type="submit" value="登录">
</form>
</body>
</html>

2)login_ok.jsp

html 复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
<h1>登陆成功</h1>
欢迎你: ${?}
</body>
</html>

3)login_error.jsp

html 复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录失败</title>
</head>
<body>
<h1>登陆失败</h1>
sorry, 登陆失败 ${?}
</body>
</html>

2.MonsterService增加一个方法login

java 复制代码
public interface MonsterService {
	....
    //增加方法, 处理登录
    public boolean login(String name);
}

MonsterServiceImpl实现它

java 复制代码
@Service
public class MonsterServiceImpl implements MonsterService {
	....
    @Override
    public boolean login(String name) {
        //实际上会到DB验证->这里我们模拟一下
        if ("白骨精".equals(name)) {
            return true;
        } else {
            return false;
        }
    }
}

3.MonsterController添加一个方法login

java 复制代码
//处理妖怪登陆的方法, 返回要请求转发/重定向的字符串
@RequestMapping("/monster/login")
public String login(HttpServletRequest request, HttpServletResponse response, String mName) {
    System.out.println("---接收到mName---" + mName);
    boolean b = monsterService.login(mName);
    if (b) {//登陆成功
        return "forward:/login_ok.jsp";
    } else {//登陆失败
        return "forward:/login_error.jsp";
    }
}

接着 login.jsp填充action="/monster/login"

html 复制代码
<h1>登陆页面</h1>
<%--第一个/会被解析 http://localhost:8080
/monster/login => http://localhost:8080/monster/login--%>
<form action="/monster/login" method="post">
    妖怪名: <input type="text" name="mName"/><br/>
    <input type="submit" value="登录">
</form>

4.测试

如果输入中文, 发现提交的数据有中文乱码问题, 因为是post请求.

解决方案

我们在底层解决乱码问题.

ZzwDispatcherServlet前端控制器完成分发请求任务executeDispatcher()方法内, 添加如下代码 request.setCharacterEncoding("utf-8");即可

java 复制代码
//2.返回的Map<String, String[]> String: 表示http请求的参数名
//  String[]: 表示http请求的参数值, 为什么是数组
//处理提交的数据中文乱码问题
request.setCharacterEncoding("utf-8");
Map<String, String[]> parameterMap = request.getParameterMap();

测试

5.在ZzwDispatcherServletexecuteDispatcher方法, 添加如下代码

java 复制代码
//上面代码省略...

/**
 * 1.下面这样写, 其实是针对目标方法 m(HttpServletRequest request, HttpServletResponse response)
 * zzwHandler.getMethod()
 *              .invoke(zzwHandler.getController(), request, response)
 * 2.这里我们准备将需要传递给目标方法的 实参=> 封装到参数数组=> 然后以反射调用的方式传递给目标方法
 * public Object invoke(Object var1, Object... var2)...
 */

//反射调用目标方法
Object result = zzwHandler.getMethod()
        .invoke(zzwHandler.getController(), params);

//这里就是对返回的结果进行解析=>原生springmvc 可以通过视图解析器来完成
//这里老师让我们直接解析, 只要把视图解析器的核心机制表达清楚就OK
//instanceof 判断 运行类型
if (result instanceof String) {
    String viewName = (String) result;
    if (viewName.contains(":")) {//说明你返回的String 结果forward:/login_ok.jsp 或者 redirect:/xxx/xx/xx.xx
        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)) {//说明你希望重定向
            //如果是redirect, 那么这里需要拼接Application  Context. 只不过这个项目的Application Context 正好是 /
            response.sendRedirect(viewPage);
        }
    } else {//默认是请求转发
        request.getRequestDispatcher(viewName)
                .forward(request, response);
    }
}//这里还可以扩展

6.测试



解决{?}:将信息保存到request域, 在页面显示.

6.MonsterController修改login方法, 将mName保存到request域

java 复制代码
//处理妖怪登陆的方法, 返回要请求转发/重定向的字符串
@RequestMapping("/monster/login")
public String login(HttpServletRequest request, HttpServletResponse response, String mName) {
    System.out.println("---接收到mName---" + mName);
    //将nName设置到request域
    request.setAttribute("mName", mName);
    boolean b = monsterService.login(mName);
    if (b) {//登陆成功
        return "forward:/login_ok.jsp";
    } else {//登陆失败
        return "forward:/login_error.jsp";
    }
}

6.1修改login_ok.jsp. 设置isELIgnored="false", 否则el表达式不会生效, 默认是true

html 复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
<h1>登陆成功</h1>
欢迎你: ${requestScope.mName}
</body>
</html>

6.2修改login_error.jsp.设置isELIgnored="false", 否则el表达式不会生效 默认是true

html 复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登录失败</title>
</head>
<body>
<h1>登陆失败</h1>
sorry, 登陆失败: ${requestScope.mName}
</body>
</html>

6.3测试

7.演示redirect.
MonsterController修改login方法

java 复制代码
//处理妖怪登陆的方法, 返回要请求转发/重定向的字符串
@RequestMapping("/monster/login")
public String login(HttpServletRequest request, HttpServletResponse response, String mName) {
    System.out.println("---接收到mName---" + mName);
    //将nName设置到request域
    request.setAttribute("mName", mName);
    boolean b = monsterService.login(mName);
    if (b) {//登陆成功
        //return "forward:/login_ok.jsp";
        //测试从定向
        return "redirect:/login_ok.jsp";👈
    } else {//登陆失败
        return "forward:/login_error.jsp";
    }
}

7.1测试

之所有不显示, 是因为请求方式是重定向.

7.2演示默认方式(forward)
MonsterController修改login方法

java 复制代码
//处理妖怪登陆的方法, 返回要请求转发/重定向的字符串
@RequestMapping("/monster/login")
public String login(HttpServletRequest request, HttpServletResponse response, String mName) {
    System.out.println("---接收到mName---" + mName);
    //将nName设置到request域
    request.setAttribute("mName", mName);
    boolean b = monsterService.login(mName);
    if (b) {//登陆成功
        //return "forward:/login_ok.jsp";
        //测试从定向
        //return "redirect:/login_ok.jsp";
        //测试默认方式(forward)
        return "/login_ok.jsp";👈
    } else {//登陆失败
        return "forward:/login_error.jsp";
    }
}

8 将登录页面提交的参数名改成monsterName, 还会不会登陆成功?

html 复制代码
<h1>登陆页面</h1>
<%--第一个/会被解析 http://localhost:8080
/monster/login => http://localhost:8080/monster/login--%>
<form action="/monster/login" method="post">
    妖怪名: <input type="text" name="monsterName"/><br/>
    <input type="submit" value="登录">
</form>

8.1测试

8.2 解决方案: 在MonsterControllerlogin方法, 形参mName加上@RequestParam(value = "monsterName")

java 复制代码
//处理妖怪登陆的方法, 返回要请求转发/重定向的字符串
@RequestMapping("/monster/login")
public String login(HttpServletRequest request,
                    HttpServletResponse response,
                    @RequestParam(value = "monsterName") String mName) {
    System.out.println("---接收到mName---" + mName);
    //将nName设置到request域
    request.setAttribute("mName", mName);
    boolean b = monsterService.login(mName);
    if (b) {//登陆成功
        //return "forward:/login_ok.jsp";
        //测试从定向
        //return "redirect:/login_ok.jsp";
        //测试默认方式-forward
        return "/login_ok.jsp";
    } else {//登陆失败
        return "forward:/login_error.jsp";
    }
}

8.3测试

实现任务阶段八

🍍完成返回JSON格式数据-@ResponseBody

功能说明: 通过自定义@ResponseBody, 返回JSON格式数据

●完成任务说明

-在实际开发中, 比如前后端分离的项目, 通常是直接返回json数据给客户端/浏览器

-客户端/浏览器接收到数据后, 自己决定如何处理和显示

-测试页面 浏览器输入: http://localhost:8080/monster/list/json

🥦分析+代码实现

1.在com.zzw.zzwspringmvc.annotation下新建@ResponseBody

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
    String value() default "";
}

2.MonsterController添加方法listMonsterByJson

java 复制代码
@Controller
public class MonsterController {

    //@Autowired表示要完成属性的装配.
    @Autowired
    private MonsterService monsterService;

    /**
     * 编写方法, 返回json格式的数据
     * 老师梳理
     * 1.目标方法返回的结果是给springmvc底层反射调用的位置使用
     * 2.我们在springmvc底层反射调用的位置, 接收到结果并解析即可
     * 3.方法上标注了@ResponseBody 表示希望以json格式返回给客户端/浏览器
     * 4.目标方法的实参, 在springmvc底层通过封装好的参数数组, 传入...
     * @param request
     * @param respons
     * @return
     */
    @RequestMapping("/monster/list/json")
    @ResponseBody
    public List<Monster> listMonsterByJson(HttpServletRequest request,
                                                HttpServletResponse respons) {
        List<Monster> monsters = monsterService.listMonster();
        return monsters;
    }
}

3.pom.xml引入jackson, 刷新. 注意: 这里我们不使用gson
注意: jackson-databind

java 复制代码
<!--引入jackson, 使用它的工具类可以进行json操作-->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.4</version>
</dependency>

3.1com.zzw包下新建一个测试类ZzwTest

java 复制代码
public class ZzwTest {
    public static void main(String[] args) {
        List<Monster> monsters = new ArrayList<Monster>();
        monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
        monsters.add(new Monster(200, "汤姆猫", "抓老鼠", 200));

        //把monsters 转成json
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String monsterJson = objectMapper.writeValueAsString(monsters);
            System.out.println("monsterJson=" + monsterJson);

        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

4.ZzwDispatcherServletexecuteDispatcher, 在反射调用目标方法的位置扩展代码

java 复制代码
if (result instanceof String) {
    String viewName = (String) result;
    ......
}//这里还可以扩展
else if (result instanceof ArrayList) {//如果是ArrayList
    //判断目标方法是否有@ResponseBody注解
    Method method = zzwHandler.getMethod();
    if (method.isAnnotationPresent(ResponseBody.class)) {
        //把result [ArrayList] 转成json格式数据->返回
        //这里我们需要使用到java中如何将 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();
    }
}

🥦完成测试

1.浏览器测试

2.也可以在线转成规整的json格式
JSON工具在线解析

3.也可以使用Postman进行测试


🔜 下一篇预告 🔜

敬请期待:SpringMVC系列九: 数据格式化与验证及国际化


📚 目录导航 📚

  1. SpringMVC系列一: 初识SpringMVC
  2. SpringMVC系列二: 请求方式介绍
  3. SpringMVC系列三: Postman(接口测试工具)
  4. SpringMVC系列四: Rest-优雅的url请求风格
  5. SpringMVC系列五: SpringMVC映射请求数据
  6. SpringMVC系列六: 视图和视图解析器
  7. SpringMVC系列七: 手动实现SpringMVC底层机制-上
  8. SpringMVC系列八: 手动实现SpringMVC底层机制-下
    ...

💬 读者互动 💬

在学习SpringMVC底层机制的过程中,你有哪些疑问或需要帮助的地方?欢迎在评论区留言,我们一起讨论。


相关推荐
这个男人是小帅21 分钟前
【GAT】 代码详解 (1) 运行方法【pytorch】可运行版本
人工智能·pytorch·python·深度学习·分类
Qter_Sean23 分钟前
自己动手写Qt Creator插件
开发语言·qt
何曾参静谧27 分钟前
「QT」文件类 之 QIODevice 输入输出设备类
开发语言·qt
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
冰芒猓4 小时前
SpringMVC数据校验、数据格式化处理、国际化设置
开发语言·maven
Shy9604184 小时前
Doc2Vec句子向量
python·语言模型
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
杜杜的man5 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang