SpringMVC框架学习笔记(五):手动实现 SpringMVC 底层机制。本文章包含所有源码,且均有详细注释(核心分发控制器+ Controller容器控制器+方法获取参数.....),

搭建 SpringMVC 底层机制开发环境

1.1 创建 Maven 项目 my-springmvc

 1.2 对 my-springmvc 进行配置: 创建 test 资源目录(直接复制main的资源目录即可)

 

1.3 在pom文件中添加基本的依赖

<dependencies>
    <!--引入junit测试依赖-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>

    <!--引入原生servlet依赖 的jar-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <!--
        1. scope标签表示引入的jar的作用范围
        2. provided:表示该项目在打包,放到生产环境的时候,不需要带上servlet-api.jar
        3. 因为tomcat本身是有servlet的jar, 到时直接使用tomcat本身的servlet-api.jar,防止版本冲突
        -->
        <scope>provided</scope>
    </dependency>

    <!--引入dom4j,解析xml文件-->
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>

    <!--引入常用工具类的jar 该jar含有很多常用的类-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.5</version>
    </dependency>
</dependencies>

1.4 添加 Web 应用程序

注意:这里还需要对web目录配置资源路径,可参考文章http://t.csdnimg.cn/750uh

1.5 到此: 项目开发环境搭建完成

实现 SpringMVC 底层机制

2.1 阶段 1- 开发 MyDispatcherServlet 核心控制分发器

说明: 编写 MyDispatcherServlet 充当原生的 DispatcherServlet(即核心控制器) 

需要实现的功能分析:

  • 拦截浏览器发来的请求

代码实现

(1)在java目录下创建软件包 com.myspringmvc.servlet,在该包下创建类 MyDispatcherServlet

package com.myspringmvc.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 1. MyDispatcherServlet充当原生DispatcherServlet
 * 2. 本质是一个Servlet, 继承HttpServlet
 */
public class MyDispatcherServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        System.out.println("--MyDispatcherServlet---doGet--");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        System.out.println("--MyDispatcherServlet---doPost--");
    }
}

(2)在main/resource 目录下创建 myspringmvc.xml, 充当原生的 applicationContext-mvc.xml 文件(就是 spring 的容器配置文件), 先创建好一个空的文件 

 (3)修改 web\WEB-INF\web.xml, 完成 MyDispatcherServlet 前端控制器的配置

<!--配置MyDispatcherServlet,作为我们自己的前端控制器-->
<servlet>
    <servlet-name>myDispatcherServlet</servlet-name>
    <servlet-class>com.myspringmvc.servlet.MyDispatcherServlet</servlet-class>
    <!--配置属性contextConfigLocation,指定 DispatcherServlet 去操作的spring配置文件-->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:myspringmvc.xml</param-value>
    </init-param>
    <!--在web项目启动时,就自动加载MyDispatcherServlet-->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>myDispatcherServlet</servlet-name>
    <!--因为MyDispatcherServlet作为前端控制器,所以需要拦截所有请求,url-pattern 配置 /-->
    <url-pattern>/</url-pattern>
</servlet-mapping>

 (4)配置 Tomcat 服务器

 (5)启动服务器,输入网址 http://localhost:8080/springmvc/abc,控制有如下输出,说明我们的前端控制器配置成功,即发出的请求成功被我们自己的控制器拦截

2.2 阶段 2- 完成客户端/浏览器可以请求控制层

2.2.1 创建 自定义注解和 自己的 Controller

(1)在包 myspringmvc 下创建软件包 annotation,在 annotation 下创建注解 @Controller 和 @RequestMapping

package com.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * 该注解用于标识一个控制器组件
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
    String value() default "";
}
package com.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * 该注解用于指定控制器某个方法的映射路径
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
    String value() default "";
}

(2)在包 com 下创建软件包 controller,在controller下创建java类 MonsterController.java

package com.controller;

import com.myspringmvc.annotation.Controller;
import com.myspringmvc.annotation.RequestMapping;

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

@Controller
public class MonsterController {

    // 编写方法,可以列出妖怪列表
    //springmvc 是支持原生的servlet api的,为了看到底层机制
    //这里设计两个参数
    @RequestMapping("/list/monster")
    public void listMonster(HttpServletRequest request, HttpServletResponse response){
        // 设置编码和返回类型
        response.setContentType("text/html;charset=utf-8");
        // 获取 writer 返回信息
        try {
            PrintWriter printWriter = response.getWriter();
            printWriter.write("<h1>妖怪列表信息</h1>");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

2.2.2 配置 myspringmvc.xml

<?xml version="1.0" encoding="utf-8" ?>
<beans>
    <!--指定要扫描的基本包以及子包的类-->
    <component-scan base-package="com.controller"></component-scan>
</beans>

2.2.3 编写 XMLParser 工具类,用于解析 myspringmvc.xml

(1)在包 myspringmvc 创建子包 xml,在 xml 下创建Java类 XMLParse.java

package com.myspringmvc.xml;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;

/**
 * 该类用于解析spring配置文件
 */
public class XMlParser {

    // 解析xml文件,得到要扫描的包
    public static String getBasePackage(String xmlFile){
        SAXReader saxReader = new SAXReader();
        //通过得到类的加载路径,获取到容器配置文件(对应的资源流)
        ClassLoader classLoader = XMlParser.class.getClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream(xmlFile);
        try {
            // 得到xmlFile文档
            Document document = saxReader.read(inputStream);
            Element rootElement = document.getRootElement();
            // 得到 component-scan 元素
            Element componentScanElement = rootElement.element("component-scan");
            // 得到 base-package 属性
            Attribute attribute = componentScanElement.attribute("base-package");
            // 得到 base-package 的属性值
            String basePackage = attribute.getText();
            return basePackage;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

 (2)测试该类,在 test/java 目录下创建软件包 com.test,在该包下创建测试类 MySpringMVCTest.java

package com.test;

import com.myspringmvc.xml.XMlParser;
import org.junit.Test;

public class MySpringMVCTest {

    @Test
    public void readXML(){
        // 这里注意一定要引入自己写的 XMlParser,import com.myspringmvc.xml.XMlParser;
        String basePackage = XMlParser.getBasePackage("myspringmvc.xml");
        System.out.println("basePackage=" + basePackage);
    }
}

 (3)测试结果如下,成功拿到要扫描的包

2.2.4 开发 MyWebApplicationContext,充当 Spring 容器-得到扫描类的全路径列表

功能说明:把指定的目录包括子目录下的 java 类的全路径扫描到集合中,

预计效果如下:

扫描后的 classFullPathList= [com.controller.MonsterController,
com.service.impl.MonsterServiceImpl, com.service.MonsterService]

 (1)在包 myspringmvc 创建子包 context,在 context 下创建Java类 MyWebApplicationContext.java

package com.myspringmvc.context;

import com.myspringmvc.xml.XMLParser;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * 该类作为我们自己的spring容器
 */
public class MyWebApplicationContext {
    //保存扫描包/子包的类的全路径
    private List<String> classFullPath = new ArrayList<>();

    // 该方法完成自己spring容器的初始化
    public void init(){
        // 得到要扫描的包
        String basePackage = XMLParser.getBasePackage("myspringmvc.xml");
        String[] basePackages = basePackage.split(",");
        if (basePackages.length > 0){
            for (String pack : basePackages) {
                //扫描
                scanPackage(pack);
            }
        }
        System.out.println("扫描后的 classFullPath = " + classFullPath);
    }
    /**
     * 该方法完成对包的扫描
     * @param pack 表示要扫描的包,比如"com.controller"
     */
    public void scanPackage(String pack){
        // 通过类加载器,得到包所在的工作路径(绝对路径)
        ClassLoader classLoader = MyWebApplicationContext.class.getClassLoader();
        // 这里需要对pack进行转换,因为 getResource 的参数为路径
        //说明: 1. 这里不能直接使用Junit测试, 否则 url null
        //      2. 需要启动tomcat来测试
        URL url = classLoader.getResource(pack.replace(".", "/"));

        // 根据得到的路径,对其进行扫描,把类的全路径,保存到classFullPath
        String path = url.getFile();
        File dir = new File(path);
        for (File f : dir.listFiles()) {
            if (f.isDirectory()){//如果是一个目录,进行递归扫描
                scanPackage(pack+"."+f.getName());
            } else if (f.isFile()) {
                classFullPath.add(pack + "." + f.getName().substring(0,f.getName().lastIndexOf(".class")));
            }
        }
    }
}

(2)修改类 DispatcherServlet.java , 增加方法 init(该方法会在服务器启动时自动调用)

@Override
public void init() throws ServletException {
    MyWebApplicationContext ioc = new MyWebApplicationContext();
    ioc.init();
}

(3)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功 

创建包service,类MonsterService用于测试

在 myspringmvc.xml 文件要扫描的包中增加该包

<component-scan base-package="com.controller,com.service"></component-scan>

 注意:这里如果报错java.lang.NoClassDefFoundError: org/dom4j/io/SAXReader,可参考文章 http://t.csdnimg.cn/YTwUd

2.2.5 完善 MyWebApplicationContext,充当 Spring 容器-实例化对象到容器中

 功能说明:将扫描到的类, 在满足条件的情况下(即有相应的注解@Controller @Service...), 反射注 入到 ioc 容器

(1)修改java类 MyWebApplicationContext.java

  • 增加属性 ioc
//属性ioc,存放反射生成的Bean对象
public ConcurrentHashMap<String, Object> ioc = new ConcurrentHashMap<>();
  • 增加方法 executeInstance
//该方法将扫描到的类,在满足条件的情况下,反射到ioc容器
public void executeInstance(){
    // 判断是否扫描到类
    if (classFullPath.size() == 0){//说明没有扫描到类
        return;
    }

    // 遍历 classFullPath,进行反射
    for (String fullPath : classFullPath) {
        Class<?> aClass = null;
        try {
            // 得到class 对象
            aClass = Class.forName(fullPath);
            // 判断是否存在 @Controller 注解
            if (aClass.isAnnotationPresent(Controller.class)) {
                // 得到 @Controller 注解
                Controller controller = aClass.getDeclaredAnnotation(Controller.class);
                // 得到value值
                String value = controller.value();
                String beanName = value;
                // 如果没有指定value值,则默认为类名首字母小写
                if ("".equals(value)){
                    // 得到类名
                    String className = aClass.getSimpleName();
                    // 将字符串首字母小写
                    beanName = StringUtils.uncapitalize(className);
                }
                Object instance = aClass.newInstance();
                ioc.put(beanName, instance);
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
  • 修改 init 方法,在该方法中调用executeInstance
// 该方法完成自己spring容器的初始化
public void init(){
    // 得到要扫描的包
    String basePackage = XMLParser.getBasePackage("myspringmvc.xml");
    String[] basePackages = basePackage.split(",");
    if (basePackages.length > 0){
        for (String pack : basePackages) {
            //扫描,得到要扫描包的类全路径
            scanPackage(pack);
        }
    }
    System.out.println("扫描后的 classFullPath = " + classFullPath);
    // 将满足条件的类,创建对象,放入ioc容器中
    executeInstance();
    System.out.println("扫描后的 ioc = "+ioc);
}

 (3)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功 

2.5.6 完成请求 URL 和控制器方法的映射关系

功能说明:将配置的@RequestMapping url 和 对应的 控制器、方法 的映射关系保存到集合中

预计效果如下:

handlerList= [MyHandler{
url="/monster/list",
controller=com.hspedu.controller.MonsterController@714dab1, 
method=public void com.hspedu.controller.MonsterController.
listMonsters(javax.servlet.http.HttpServletRequest,
javax.servlet.http.HttpServletResponse)}]

(1)在myspringmvc 包下,新建一个包 handler,在handler下创建java类 MyHandler

package com.myspringmvc.handler;

import java.lang.reflect.Method;

/**
 * 该类记录请求的 url 和 控制器方法映射关系
 */
public class MyHandler {
    private String url;
    private Object controller;
    private Method method;

    public MyHandler(String url, Object controller, Method method) {
        this.url = url;
        this.controller = controller;
        this.method = method;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Object getController() {
        return controller;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    @Override
    public String toString() {
        return "MyHandler{" +
                "\nurl='" + url + '\'' +
                ", \ncontroller=" + controller +
                ", \nmethod=" + method +
                '}';
    }
}

 (2)修改类 DispatcherServlet.java , 增加属性handlerList,增加方法 initHandlerMapping,作为控制器映射器

private List<MyHandler> handlerList = new ArrayList<>();
/**
 * 该方法作为一个控制器映射器,完成对 url 和 控制器方法 的映射 的初始化
 */
private void initHandlerMapping(ConcurrentHashMap<String, Object> ioc){
    if (ioc.isEmpty()){
        // 判断当前ioc容器是否为null
        return;
    }
    // 遍历容器
    for (String s : ioc.keySet()) {
        // 获取到控制器对象
        Object o = ioc.get(s);
        // 获取到class对象
        Class<?> aClass = o.getClass();
        // 判断该对象是否为一个控制器
        if (aClass.isAnnotationPresent(Controller.class)){// 得到该对象的所有方法
            Method[] methods = aClass.getDeclaredMethods();
            // 遍历方法
            for (Method method : methods) {
                // 判断该方法是否有注解 @RequestMapping
                if (method.isAnnotationPresent(RequestMapping.class)) {
                    // 取出 @RequestMapping
                    RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                    // 创建 MyHandler 对象,存放到集合中
                    handlerList.add(new MyHandler(requestMapping.value(), o, method));
                }
            }
        }
    }
}

 (3)修改 init 方法,在该方法中调用 initHandlerMapping

@Override
public void init() throws ServletException {
    MyWebApplicationContext applicationContext = new MyWebApplicationContext();
    applicationContext.init();
    initHandlerMapping(applicationContext.ioc);
    System.out.println("handlerList初始化的结果 = " + handlerList);
}

(4)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功

2.5.7 完成 MyDispatcherServlet 分发请求到对应控制器方法

功能说明:当用户发出请求,根据用户请求 url 找到对应的控制器-方法, 并反射调用 

(1) 修改类 DispatcherServlet.java,增加方法getMyHandler 和 executeDispatch

// 编写方法,通过request对象,返回MyHandler对象
// 如果没有,就返回null
private MyHandler getMyHandler(HttpServletRequest request){
    // 判断handlerList是否为null
    if (handlerList.isEmpty()) return null;
    // 先获取到用户请求的 uri,比如http://localhost:8080/springmvc/monster/list
    // uri = /springmvc/monster/list
    String requestURI = request.getRequestURI();
    // 获取到工程路径 /springmvc
    String contextPath = request.getContextPath();
    // 从uri中去除掉工程路径,得到 url = /monster/list
    String url = requestURI.substring(contextPath.length());
    // 遍历 handlerList
    for (MyHandler handler : handlerList) {
        if (handler.getUrl().equals(url)){
            return handler;
        }
    }
    return null;
}

// 编写方法,分发请求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response){
    MyHandler myHandler = getMyHandler(request);
    try {
        if (myHandler == null) {
            response.getWriter().print("<h1>404 NOT FOUNT</h1>");
        }else {
            // 通过反射调用该方法
            myHandler.getMethod().invoke(myHandler.getController(),request, response);
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

}

 (2)修改 doGet 和 doPost 方法,使前端控制器在拦截请求后,调用executeDispatch进行请求的分发

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    // 分发请求
    executeDispatch(req, resp);
}

(3)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/list/monster 显示如下页面,测试成功

2.3 阶段 3- 从 web.xml 动态获取 myspringmvc.xml

 功能说明:前面我们加载 myspringmvc.xml 是硬编码, 现在做活, 从 web.xml 动态获取

(1)修改类 DispatcherServlet.java 中的 init 方法,读取容器配置文件名

@Override
public void init(ServletConfig servletConfig) throws ServletException {
    // 获取到web.xml文件中配置的spring容器配置文件名
    String configLocation = servletConfig.getInitParameter("contextConfigLocation");
    // 创建自己的 spring 容器
    MyWebApplicationContext applicationContext = new MyWebApplicationContext(configLocation);
    // 初始化容器
    applicationContext.init();
    // 把url 映射到 控制器的方法
    initHandlerMapping(applicationContext.ioc);
    System.out.println("handlerList初始化的结果 = " + handlerList);
}

 (2)修改类 MyWebApplicationContext.java

  • 增加属性 configLocation,增加有参和无参构造器
/ 该属性存放配置文件路径
private String configLocation;

public MyWebApplicationContext(String configLocation) {
    this.configLocation = configLocation;
}

public MyWebApplicationContext() {
}

修改init方法,替换spring配置文件路径

// 该方法完成自己spring容器的初始化
public void init(){
    // 得到要扫描的包
    String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]);
    String[] basePackages = basePackage.split(",");
    if (basePackages.length > 0){
        for (String pack : basePackages) {
            //扫描,得到要扫描包的类全路径
            scanPackage(pack);
        }
    }
    System.out.println("扫描后的 classFullPath = " + classFullPath);
    // 将满足条件的类,创建对象,放入ioc容器中
    executeInstance();
    System.out.println("扫描后的 ioc = "+ioc);
}

(3)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/list/monster 显示如下页面,测试成功

2.4 阶段 4- 完成自定义@Service 注解功能

功能说明: 如果给某个类加上@Service, 则可以将其注入到我们的 Spring 容器

(1)在包myspringmvc.annotation下 ,创建注解 @Service

package com.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * 该注解用于标识一个Service对象
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
    String value() default "";
}

 (2)在包com下,新建一个包service,在该包下创建接口MonsterService.java

package com.service;

import com.entity.Monster;
import java.util.List;

public interface MonsterService {
    List<Monster> listMonster();
}

(3)在service包下,创建Java类 MonsterServiceImpl.java

package com.service.impl;

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

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

/**
 * 该类作为一个service注入到spring容器中
 */
@Service
public class MonsterServiceImpl implements MonsterService {
    @Override
    public List<Monster> listMonster() {
        // 这里模拟数据
        List<Monster> monsters = new ArrayList<>();
        monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
        monsters.add(new Monster(200, "老猫妖怪", "抓老鼠", 200));
        return monsters;
    }
}

 (4)修改类 MyWebApplicationContext.java 中的 executeInstance 方法的部分代码

// 得到class 对象
                aClass = Class.forName(fullPath);
                // 判断是否存在 @Controller 注解 和 @Service
                if (aClass.isAnnotationPresent(Controller.class) ||
                        aClass.isAnnotationPresent(Service.class)) {
                    String value = "";
                    if (aClass.isAnnotationPresent(Controller.class)){
                        // 得到 @Controller 注解
                        Controller controller = aClass.getDeclaredAnnotation(Controller.class);
                        // 得到value值
                        value = controller.value();
                    } else if (aClass.isAnnotationPresent(Service.class)) {
                        Service service = aClass.getDeclaredAnnotation(Service.class);
                        value = service.value();
                    }
                    String beanName = value;
                    // 如果没有指定value值,则默认为类名首字母小写
                    if ("".equals(value)){
                        // 得到类名
                        String className = aClass.getSimpleName();
                        // 将字符串首字母小写
                        beanName = StringUtils.uncapitalize(className);
                    }
                    Object instance = aClass.newInstance();
                    ioc.put(beanName, instance);
                }

(5)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功

2.5 阶段 5- 完成 Spring 容器对象的自动装配 -@Autowried

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

(1)在包 annotation 下新建注解 @Autowired

package com.myspringmvc.annotation;

import java.lang.annotation.*;

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

(2)修改类 MyWebApplicationContext.java

  • 增加方法 executeAutowired
// 该方法完成对象属性的装配,即依赖注入
private void executeAutowired(){

    if (ioc.isEmpty()){ // 如果容器为空,抛出异常
        throw new RuntimeException("ioc容器中,不存在你要装配的bean");
    }
    // 遍历ioc容器
    for (String key : ioc.keySet()) {
        // 得到对象
        Object o = ioc.get(key);
        // 得到class对象
        Class<?> aClass = o.getClass();
        // 得到所有属性
        Field[] fields = aClass.getDeclaredFields();
        // 遍历所有属性
        for (Field field : fields) {
            // 判断属性中是否存在Autowired注解
            if (field.isAnnotationPresent(Autowired.class)){
                // 得到指定的value值
                String beanName = field.getAnnotation(Autowired.class).value();
                if ("".equals(beanName)){
                    // 如果没有指定value值,则默认为属性名
                    beanName = field.getName();
                }
                // 从ioc容器中获取到对象
                Object bean = ioc.get(beanName);
                // 如果没有找到就抛出异常
                if (bean == null){
                    throw new RuntimeException("ioc容器中,不存在你要装配的bean");
                }
                try {
                    // 因为该属性是私有的,所以需要进行爆破处理
                    field.setAccessible(true);
                    // 依赖注入
                    field.set(o, bean);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}
  • 修改方法init,让其调用executeAutowired
// 该方法完成自己spring容器的初始化
public void init(){
    // 得到要扫描的包
    String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]);
    String[] basePackages = basePackage.split(",");
    if (basePackages.length > 0){
        for (String pack : basePackages) {
            //扫描,得到要扫描包的类全路径
            scanPackage(pack);
        }
    }
    System.out.println("扫描后的 classFullPath = " + classFullPath);
    // 将满足条件的类,创建对象,放入ioc容器中
    executeInstance();
    System.out.println("扫描后的 ioc = "+ioc);
    // 完成对象属性的装配
    executeAutowired();
}

(3)修改类 MonsterController.java

增加属性

@Autowired
private MonsterServiceImpl monsterServiceImpl;

修改方法 listMonster

// 编写方法,可以列出妖怪列表
//springmvc 是支持原生的servlet api的,为了看到底层机制
//这里设计两个参数
@RequestMapping("/list/monster")
public void listMonster(HttpServletRequest request, HttpServletResponse response){

    StringBuilder content = new StringBuilder("<h1>妖怪列表信息</h1>");
    //调用monsterService
    List<Monster> monsters = monsterServiceImpl.listMonster();
    content.append("<table border='1px' width='500px' style='border-collapse:collapse'>");
    for (Monster monster : monsters) {
        content.append("<tr><td>" + monster.getId()
                + "</td><td>" + monster.getName() + "</td><td>"
                + monster.getSkill() + "</td><td>"
                + monster.getAge() + "</td></tr>");
    }
    content.append("</table>");

    // 设置编码和返回类型
    response.setContentType("text/html;charset=utf-8");
    // 获取 writer 返回信息
    try {
        PrintWriter printWriter = response.getWriter();
        printWriter.write(content.toString());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

(4)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/list/monster 显示如下页面,测试成功

2.6 阶段 6- 完成控制器方法获取参数-@RequestParam

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

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

(1)修改类 DispatcherServlet.java 中的 executeDispatch方法

// 编写方法,分发请求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response){
    MyHandler myHandler = getMyHandler(request);
    try {
        if (myHandler == null) {
            response.getWriter().print("<h1>404 NOT FOUNT</h1>");
        }else {
            // 1 得到目标方法的所有参数信息(对应的数组)
            Class<?>[] parameterTypes = myHandler.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;
                }
            }

            // 通过反射调用该方法,通过实参参数数组传入参数
            myHandler.getMethod().invoke(myHandler.getController(),params);
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

2.6.2 将方法参数中指定 @RequestParam 的参数封装到参数数组,进行反射调用

(1)修改service包下的MonsterService接口,增加如下方法

//增加方法,通过传入的name,返回monster列表
public List<Monster> findMonsterByName(String name);

(2)修改service.impl包的MonsterServiceImpl类,增加如下方法

@Override
    public List<Monster> findMonsterByName(String name) {
        //这里模拟数据->DB
        List<Monster> monsters =
                new ArrayList<>();
        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<>();
        //遍历monsters,返回满足条件
        for (Monster monster : monsters) {
            if (monster.getName().contains(name)) {
                findMonsters.add(monster);
            }
        }
        return findMonsters;
}

 (3)修改controller包中的MonsterController类,增加如下方法 

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

    //获取writer返回信息
    try {
        PrintWriter printWriter = response.getWriter();
        printWriter.write(content.toString());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 (4)修改类 DispatcherServlet.java ,增加getIndexRequestParameterIndex方法,修改executeDispatch方法

// 编写方法,分发请求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response){
    MyHandler myHandler = getMyHandler(request);
    try {
        if (myHandler == null) {
            response.getWriter().print("<h1>404 NOT FOUNT</h1>");
        }else {
            // 1 得到目标方法的所有参数信息(对应的数组)
            Class<?>[] parameterTypes = myHandler.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=喝酒
            //2. 返回的Map<String,String[]> String:表示http请求的参数名
            //   String[]:表示http请求的参数值,为什么是数组
            //因为请求可能是这样一种形式:http://localhost:8080/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
            Map<String, String[]> parameterMap = request.getParameterMap();
            //3.遍历parameterMap 将请求参数,按照顺序填充到实参数组params
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                // key对应请求的参数名
                String key = entry.getKey();
                // value对应请求的参数值。这里做了简化,只考虑提交的参数是单值的情况
                String value = entry.getValue()[0];
                // 得到请求的参数对应的是第几个形参
                int indexRequestParameterIndex = getIndexRequestParameterIndex(myHandler.getMethod(), key);
                if (indexRequestParameterIndex != -1) {
                    params[indexRequestParameterIndex] = value;
                }else {
                    // 说明并没有找到@RequestParam注解对应的参数,就会使用默认的机制进行配置
                    //一会再写
                }
            }

            // 通过反射调用该方法,通过实参参数数组传入参数
            myHandler.getMethod().invoke(myHandler.getController(),params);
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

/**
 * @param method 目标方法
 * @param name   请求的参数名
 * @return 是目标方法的第几个形参
 */
private int getIndexRequestParameterIndex(Method method, String name) {
    // 得到该方法的所有形参参数
    Parameter[] parameters = method.getParameters();
    // 遍历所有参数
    for (int i = 0; i < parameters.length; i++) {
        // 取出当前参数
        Parameter parameter = parameters[i];
        // 判断该参数是否有@RequestParam注解
        if (parameter.isAnnotationPresent(RequestParam.class)){
            //得到@RequestParam注解
            RequestParam annotation = parameter.getAnnotation(RequestParam.class);
            //得到@RequestParam注解的value值
            String value = annotation.value();
            //如果注解的value值和请求的参数名key相同,就将参数值填充进对应位置的实参数组
            if (name.equals(value)){
                return i;
            }
        }
    }
    // 如果没有匹配成功,就返回-1
    return -1;
}

(4)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/monster/find?name=怪 ,显示如下页面,测试成功

 

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

 (1)修改类 DispatcherServlet.java ,增加getParameterIndex方法

说明:在默认情况下 parameter.getName() 得到的名字不是形参真正名字,而是 [arg0, arg1, arg2...], 这里要在pom文件中引入下面这个插件,使用java8特性,这样才能解决

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.6.2</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <compilerArgs>
                    <arg>-parameters</arg>
                </compilerArgs>
                <encoding>utf-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

引入插件后,点击 maven 管理,clean 项目,在重启一下 tomcat ,插件才会生效

getParameterIndex方法:

/**
 * @param method 目标方法
 * @param name   请求的参数名
 * @return 是目标方法的第几个形参(这里的形参是没有@RequestParam注解的形参)
 */
public int getParameterIndex(Method method, String name) {
    Parameter[] parameters = method.getParameters();
    for (int i = 0; i < parameters.length; i++) {
        Parameter parameter = parameters[i];
        //在默认情况下 parameter.getName() 得到的名字不是形参真正名字
        //而是 [arg0, arg1, arg2...], 这里我们要引入一个插件,使用java8特性,这样才能解决
        String paramName = parameter.getName();
        if (name.equals(paramName)){
            return i;
        }
    }
    return -1;
}

 (2)修改类 DispatcherServlet.java  中的 executeDispatch 方法中的部分代码

//3.遍历parameterMap 将请求参数,按照顺序填充到实参数组params
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
    // key对应请求的参数名
    String key = entry.getKey();
    // value对应请求的参数值。这里做了简化,只考虑提交的参数是单值的情况
    String value = entry.getValue()[0];
    // 得到请求的参数对应的是第几个形参
    int indexRequestParameterIndex = getIndexRequestParameterIndex(myHandler.getMethod(), key);
    if (indexRequestParameterIndex != -1) {
        params[indexRequestParameterIndex] = value;
    }else {
        // 说明并没有找到@RequestParam注解对应的参数,就会使用默认的机制进行配置
        // 得到请求参数对应的是第几个没有@RequestParam注解修饰的形参
        int parameterIndex = getParameterIndex(myHandler.getMethod(), key);
        if (parameterIndex != -1) {
            // 填充到实参数组
            params[parameterIndex] = value;
        }
    }
}

(3)修改Java类 MonsterController 中的 findMonsterByName 方法,删除RequestParam注解

@RequestMapping(value = "/monster/find")
public void findMonsterByName(HttpServletRequest request,
                              HttpServletResponse response,
                              String name) {

 (4)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/monster/find?name=精 ,显示如下页面,测试成功 

 2.7 阶段 7- 完成简单视图解析

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

完成任务说明:

  • 用户输入白骨精,可以登录成功, 否则失败
  • 根据登录的结果, 可以重定向或者请求转发到 login_ok.jsp / login_error.jsp, 并显示妖怪名 

(1)在web目录下,编写三个jsp页面:login.jsp,login_ok.jsp,login_error.jsp

<%@ 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="mName"><br/>
    <input type="submit" value="登录">
</form>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
<h1>登录成功</h1>
欢迎你: ${requestScope.mName}
</body>
</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>

(2)修改controller包中的MonsterController类,增加如下方法

//处理妖怪登录的方法,返回要请求转发/重定向的字符串
@RequestMapping("/monster/login")
public String login(HttpServletRequest request,
                    HttpServletResponse response,
                    String mName) {

    System.out.println("--接收到mName---" + mName);
    //将mName设置到request域
    request.setAttribute("mName", mName);
    boolean b = monsterServiceImpl.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";
    }
}

 (3)修改service包下的MonsterService接口,增加如下方法

//增加方法,处理登录
public boolean login(String name);

 (4)修改service.impl包的MonsterServiceImpl类,增加如下方法

@Override
public boolean login(String name) {
    //实际上会到DB验证->这里模拟
    if ("白骨精".equals(name)) {
        return true;
    } else {
        return false;
    }
}

 (5)在 类 DispatcherServlet.java  中的 executeDispatch 方法中 以下位置添加如下代码 可以解决web页面提交的数据中文乱码的问题

//处理提交的数据中文乱码
request.setCharacterEncoding("utf-8");

 

 (6)在 类 DispatcherServlet.java  中添加方法 viewResolver 充当视图解析器

/**
 * 该方法作为视图解析器来使用
 * 原生SpringMVC是把这个方法单独写在视图解析类中,由前端控制器来调用
 * 这里做了简化,直接写在前端控制器中
 */
private void viewResolver(Object result, HttpServletRequest request, HttpServletResponse response) {
    String viewName = (String) result;
    try {
        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)) {
                //进行重定向,重定向需要加上项目路径
                response.sendRedirect(request.getContextPath() + viewPage);
            }
        } else {
            //默认请求转发
            request.getRequestDispatcher(viewName).forward(request, response);
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

(7)在 类 DispatcherServlet.java  中的 executeDispatch 方法中 以下位置添加如下代码 ,调用视图解析器

// 通过反射调用该方法,通过实参参数数组传入参数
Object result = myHandler.getMethod().invoke(myHandler.getController(), params);
if (result instanceof String) {// 调用视图解析器
    viewResolver(result, request, response);
}

 (8)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/login.jsp ,显示如下页面,在输入框中输入白骨精

点击登录后,跳转至如下页面,即测试成功

 

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

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

(1)在 annotation 包下创建注解 @ResponseBody

package com.myspringmvc.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}

(2)修改controller包中的MonsterController类,增加如下方法

/**
 * 方法上标注了 @ResponseBody 表示希望以json格式返回给客户端/浏览器
 */
@RequestMapping("/monster/list/json")
@ResponseBody
public List<Monster> listMonsterByJson() {
    List<Monster> monsters = monsterServiceImpl.listMonster();
    return monsters;
}

 (3) 在pom.xml文件中引入 jackson 包

<!--使用jackson工具类可以进行json操作-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.0</version>
</dependency>

(4)在 类 DispatcherServlet.java  中的 executeDispatch 方法中 以下位置添加如下代码,返回json格式数据

else if (result instanceof ArrayList) {
    // 如果是ArrayList,再判断是否有@ResponseBody注解
    if(myHandler.getMethod().isAnnotationPresent(ResponseBody.class)){
        // 有@ResponseBody注解,就返回json数据
        // 把result 转成json格式数据,可以使用jackson包下的工具类
        ObjectMapper objectMapper = new ObjectMapper();
        // 返回的结果resultJson,就是json格式的字符串
        String resultJson = objectMapper.writeValueAsString(result);
        // 这里简单处理,直接返回
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(resultJson);
        writer.flush();
        writer.close();
    }
}

 

(5)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/monster/list/json ,显示如下页面,测试成功

 至此,实现任务结束!附一张原生SpringMVC工作流程图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值