手动实现 SpringMVC 底层机制

  |   0 评论   |   0 浏览

手动实现 SpringMVC 底层机制

https://www.bilibili.com/video/BV1eX4y137BQ?t=61.1

1.搭建 SpringMVC 底层机制开发环境

image-20220611131917146

image-20220611132506393

2.自己实现SpringMVC底层机制【核心分发控制器+Controller和Service注入容器】

1.实现任务阶段1-开发LLPDispatcherServlet

1.说明:编写LLPDispatcherServlet充当原生的DispatcherServlet

2.分析+代码实现

● 分析示意图

● 代码实现

1.创建自己的dispatcherServlet,\src\main\java\com\llp\llpspringmvc\servlet\LLPDispatcherServlet.java

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

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

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

2.创建\resources\llpspringmvc.xml 用来充当原生的applicationContext-mvc.xml文件(即spring容器的配置文件),比如指定要扫描哪些包下的类,先创建一个空的文件

3.修改\src\main\webapp\WEB-INF\web.xml,完成LLPDispatcherServlet的配置

<!--配置llpDispatcherServlet, 作为我们自己的前端控制器-->
<servlet>
  <servlet-name>llpDispatcherServlet</servlet-name>
  <servlet-class>com.llp.llpspringmvc.servlet.LLPDispatcherServlet</servlet-class>
  <!--给llpDispatcherServlet配置参数,指定要操作的spring容器配置文件-->
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:llpspringmvc.xml</param-value>
  </init-param>
  <!--llpDispatcherServlet在tomcat启动时,就自动加载-->
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>llpDispatcherServlet</servlet-name>
  <!--因为llpDispatcherServlet作为前端控制器,所以需要拦截所有请求,url-pattern配置 /-->
  <url-pattern>/</url-pattern>
</servlet-mapping>

3.配置Tomcat完成测试

http://localhost:8080/llp_myspringmv/01

image-20220611151202074

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

1.创建自己的Controller和自定义注解

● 代码实现

  1. 创 建 src\main\java\com\llp\controller\MonsterController.java
@Controller
public class MonsterController {
    //编写方法可以列出妖怪列表
    //SpringMVC支持原生的Servlet API,为了看到底层机制
    //这里我们设计两个参数
    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) {
            e.printStackTrace();
        }
    }
}

​ 2.创建自定义注解

自定义@Controller注解

/**
 * 注解用于标识一个控制器组件
 * 这里设计到注解知识,在java基础讲过.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
    String value() default "";
}

自定义@RequestMapping注解

/**
 * RequestMapping 注解用于指定控制器-方法的映射路径
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {

    String value() default "";
}

2.配置llpspringmvc.xml

1.在该文件指定,我们的 springmvc 要扫描的包

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

</beans>

3.编写 XMLPaser工具类,可以解析llpspringmvc.xml

完成功能说明: -编写 XMLParser 工具类, 可以解析 llpringmvc.xml, 得到要扫描的包

public class XMLParser {

    public static String getBasePackage(String XmlFile) {
        InputStream inputStream = XMLParser.class.getClassLoader().getResourceAsStream(XmlFile);
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(inputStream);
            Element rootElement = document.getRootElement();
            Element componentScanElement = rootElement.element("component-scan");
            String basePackage = componentScanElement.attributeValue("base-package");
            return basePackage;
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return "";
    }


}

测试

public class LLPSpringMVCTest {

    @Test
    public void readXml(){
        String basePackage = XMLParser.getBasePackage("llpspringmvc.xml");
        //com.llp.controller
        System.out.println(basePackage);
    }
}

4.开发 LLPWebApplicationContext充当Spring容器-得到扫描类的全路径列表

  1. 完成的功能说明,考虑需要扫描多个包的情况

image-20220611215345277

我们这里多个包的情况,在配置文件中用逗号分隔,在自定义容器获取扫描的包时采用逗号进行分割

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

</beans>

​ 2.把指定的目录包括子目录下的 java 类的全路径扫描到集合中,比如 ArrayList

自定义spring容器-LLPWebApplicationContext

/**
 * 表示我们自己的spring容器
 */
public class LLPWebApplicationContext {
    //定义属性classFullPathList,保存扫描包/子包的类的全路径
    private List<String> classFullPathList = new ArrayList<>();

    public List<String> getClassFullPathList() {
        return classFullPathList;
    }

    /**
     * 编写方法,完成自己的spring容器的初始化
     * 容器的初始化,交由前端控制器
     * 而前端控制器由tomcat来触发
     */
    public void init() {
        //解析我们自己的spring配置文件,获取基本包
        String basePackage = XMLParser.getBasePackage("llpspringmvc.xml");
        //考虑有多个包的情况,我们在llpspringmvc.xml中配置多个包时采用了逗号分割的方式
        String[] basePackages = basePackage.split(",");
        if (basePackages.length > 0) {
            for (String pack : basePackages) {
                scanPackage(pack);
            }
        }
    }

    /**
     * 表示要扫描的包,比如"com.llp.controller"
     *
     * @param pack
     */
    public void scanPackage(String pack) {
        //得到包所在的工作路径[绝对路径]
        //下面这句话的含义是 通过类的加载器,得到你指定的包对应的 工作路径[绝对路径]
        //比如 "com.llp.controller" => url
        //细节说明: 1. 不要直接使用Junit测试, 否则 url null 2. 启动tomcat来测试
        //file:/C:/ide/IdeaProjects/llp-myspringmv/target/llp-myspringmv/WEB-INF/classes/com/llp/controller/
        URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/"));
        System.out.println("url= " + url);
        String path = url.getFile();
        File dir = new File(path);
        for (File file : dir.listFiles()) {
            //如果是一个目录需要递归扫描
            if (file.isDirectory()) {
                //file.getName() = 子目录名
                scanPackage(pack + "." + file.getName());
            } else {
                //是一个文件,但不一定是class文件
                //就算是.class文件,是否需要注入到容器中也不一定
                //目前先把这些文件的全路径都保存到集合中,后面在注入对象到容器时在处理
                String classFullPath = pack + "." + file.getName().replaceAll(".class", "");
                classFullPathList.add(classFullPath);
            }
        }


    }

}

自定义DispatcherServlet

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

    /**
     * 在前端控制器init方法中初始化我们自己的spring容器
     */
    @Override
    public void init() {
        LLPWebApplicationContext llpWebApplicationContext = new LLPWebApplicationContext();
        llpWebApplicationContext.init();
        System.out.println("ClassFullPathList="+llpWebApplicationContext.getClassFullPathList());

    }

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

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

5.完善LLPWebApplicationContext充当Spring容器-实例化对象到容器中

完成功能说明

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

修改LLPApplicationContext,新executeInstance方法,并添加到初始化方法中

//编写方法,完成自己的spring容器的初始化
public void init() {

    String basePackage = XMLParser.getBasePackage("llpspringmvc.xml");
    //这时basePackage => com.llp.controller,com.service
    String[] basePackages = basePackage.split(",");
    //遍历basePackages, 进行扫描
    if (basePackages.length > 0) {
        for (String pack : basePackages) {
            scanPackage(pack);
        }
    }
    System.out.println("扫描后的= classFullPathList=" + classFullPathList);
    //将扫描到的类, 反射到ico容器
    executeInstance();
    System.out.println("扫描后的 ioc容器= " + ioc);
}
//编写方法,将扫描到的类, 在满足条件的情况下,反射到ioc容器
public void executeInstance() {
    //判断是否扫描到类
    if (classFullPathList.size() == 0) {//说明没有扫描到类
        return;
    }
    try {
        //遍历classFullPathList,进行反射
        for (String classFullPath : classFullPathList) {
            Class<?> clazz = Class.forName(classFullPath);
            //说明当前这个类有@Controller
            if (clazz.isAnnotationPresent(Controller.class)) {
                //得到类名首字母小写
                String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() +
                        clazz.getSimpleName().substring(1);
                ioc.put(beanName, clazz.newInstance());
            } //如果有其它的注解,可以扩展
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

修改LLPDispatcherServlet初始化方法

SpirngMVC底层在前端控制器对Spring容器进行初始化

/**
 * 在前端控制器init方法中初始化我们自己的spring容器
 */
@Override
public void init() {
    LLPWebApplicationContext llpWebApplicationContext = new LLPWebApplicationContext();
    llpWebApplicationContext.init();
    System.out.println("ClassFullPathList="+llpWebApplicationContext.getClassFullPathList());
    System.out.println(llpWebApplicationContext.ioc);
}

测试结果

image-20220611224513820

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

创建LLPHandler

//对象记录请求的 url 和 控制器方法映射关系
public class LLPHandler {
    private String url;
    private Method method;
    private Object controller;


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

    public String getUrl() {
        return url;
    }

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

    public Method getMethod() {
        return method;
    }

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

    public Object getController() {
        return controller;
    }

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

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

修改LLPDispatcherServlet

1.添加initHandlerMapping

/**
     * 完成url 和 控制器方法的映射
     */
    private void initHandlerMapping() {
        //1.遍历ioc容器
        ConcurrentHashMap<String, Object> ioc = llpWebApplicationContext.ioc;
        for (String key : ioc.keySet()) {
            Object o = ioc.get(key);
            //获取bean的class对象
            Class<?> clazz = o.getClass();
            //判断bean是不是controller
            if (clazz.isAnnotationPresent(Controller.class)) {
                //如果是则获取该类所有方法
                Method[] declaredMethods = clazz.getDeclaredMethods();
                for (Method method : declaredMethods) {
                    //遍历方法,判断方法是否被@RequestMapping修饰
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        //获取RequestMapping注解
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        //获取映射路径
                        String url = requestMapping.value();
                        LLPHandler llpHandler = new LLPHandler(url, method, o);
                        handlerList.add(llpHandler);
                    }
                }
            }
        }
        System.out.println("handlerList = "+handlerList);
    }

2.初始化dispatcherServlet时调用映射处理方法

/**
 * 在前端控制器init方法中初始化我们自己的spring容器
 */
@Override
public void init() {
    llpWebApplicationContext = new LLPWebApplicationContext();
    llpWebApplicationContext.init();
    System.out.println("ClassFullPathList=" + llpWebApplicationContext.getClassFullPathList());
    System.out.println("ioc=" + llpWebApplicationContext.ioc);
    initHandlerMapping();
}

3.MonsterCotroller 方法添加自定义的@RequestMapping注解

//编写方法可以列出妖怪列表
//SpringMVC支持原生的Servlet API,为了看到底层机制
//这里我们设计两个参数
@RequestMapping(value = "/monster/list")
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) {
        e.printStackTrace();
    }
}

4.测试结果

image-20220611234007985

7.完成LLPDispatcherServlet分发请求到对应控制器方法

修改LLPDispatcherServlet

1.新增根据uri获取handler的方法

/**
 * 编写方法通过request对象,返回LLPHandler对象
 * 如果没有,返回null
 */
public LLPHandler getLLPHandler(HttpServletRequest request) {
    //1.通过request获取uri
    String requestURI = request.getRequestURI();
    for (LLPHandler llpHandler : handlerList) {
        String url = llpHandler.getUrl();
        if (requestURI.equals(url)) {
            return llpHandler;
        }
    }
    return null;
}

2.实现请求分发方法

/**
 * 编写方法,根据request对象获取uri,查询HandlerList集合匹配对应的handler对象
 * 通过反射调用方法,进行请求分发
 */
public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
    //获取LLPHandler[url和控制器方法的映射]
    LLPHandler llpHandler = getLLPHandler(request);
    try {
        //考虑根据请求的uri没有匹配到对应的handler的情况,我们返回404NOTFOUND
        if (llpHandler == null) {
            PrintWriter writer = response.getWriter();
            writer.println("<h1>404 NOT FOUND</h1>");
            writer.close();
        } else {
            //如果匹配到了对应的handler,则调用方法实现请求分发
            llpHandler.getMethod().invoke(llpHandler.getController(), request, response);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.LLPDispatcherServlet本身时一个Servlet,我们为这个servlet配置了拦截所有请求;因此在执行doGet、doPost方法时进行请求分发

@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);
}

4.测试效果

image-20220612095024448

image-20220612095630069

3.实现任务阶段 3- 从 web.xml 动态获取 llpspringmvc.xml

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

2.分析+代码实现+测试

● 完成功能说明

  • 分图析示意

LLPDispatcherServlet 在创建并初始化 LLPWebApplicationContext,动态的从 web.xml 中获取到配置文件.

● 代码实现

dispatcherServlet继承了父类HttpServlet而HttpServlet又继承了GenericServlet,可以看到里面又两个init方法

image-20220612100929146

修改LLPDispatcherServlet

private String contextPath;
/**
 * 在前端控制器init方法中初始化我们自己的spring容器
 */
@Override
public void init(ServletConfig config) {
    ServletContext servletContext = config.getServletContext();
    contextPath = servletContext.getContextPath();
    //classpath:llpspringmvc.xml
    String configLocation = config.getInitParameter("contextConfigLocation");
    //classpath-0  llpspringmvc.xml-1
    llpWebApplicationContext = new LLPWebApplicationContext(configLocation.split(":")[1]);
    //自定义spring容器初始化
    llpWebApplicationContext.init();
    System.out.println("ClassFullPathList=" + llpWebApplicationContext.getClassFullPathList());
    System.out.println("ioc=" + llpWebApplicationContext.ioc);
    //完成url 和 控制器方法的映射
    initHandlerMapping();
}

修改自定义Spring容器-LLPWebApplicationContext

//定义属性,表示spring容器配置文件
private String configLocation;

public LLPWebApplicationContext() {
}

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

/**
 * 编写方法,完成自己的spring容器的初始化
 * 容器的初始化,交由前端控制器
 * 而前端控制器由tomcat来触发
 */
public void init() {
    //解析我们自己的spring配置文件,获取基本包
    String basePackage = XMLParser.getBasePackage(configLocation);
    //考虑有多个包的情况,我们在llpspringmvc.xml中配置多个包时采用了逗号分割的方式
    String[] basePackages = basePackage.split(",");
    //遍历包
    if (basePackages.length > 0) {
        for (String pack : basePackages) {
            //扫描包路径下的所有.class类,并将类全路径放入classFullPathList集合中
            scanPackage(pack);
        }
    }
    //将扫描到的类, 在满足条件的情况下,通过反射注入到ioc容器
    executeInstance();
}

4.实现任务阶段 4- 完成自定义@Service 注解功能

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

2.分析+代码实现+测试

  • 给 Service 类标注@Service, 可以将对象注入到 Spring 容器中 - 并可以通过接口名支持多级, 类名来获取到 Service Bean

● 代码实现

public interface MonsterService {

    public List<Monster> listMonsters();

}
@Service
public class MonsterServiceImpl implements MonsterService {
    @Override
    public List<Monster> listMonsters() {
        List<Monster> monsterList = new ArrayList<>();
        monsterList.add(new Monster(100,"孙悟空",1000,"打妖怪"));
        monsterList.add(new Monster(200,"白骨精",1000,"吸人血"));
        monsterList.add(new Monster(300,"蜘蛛精",1000,"吐丝"));
        return monsterList;
    }
}
/**
 * 编写方法,将扫描到的类, 在满足条件的情况下,反射到ioc容器
 */
public void executeInstance() {
    if (classFullPathList.size() == 0) {
        return;
    }
    try {
        //遍历类全路径集合
        for (String classFullPath : classFullPathList) {
            //通过反射获取Class对象
            Class<?> clazz = Class.forName(classFullPath);
            //如果Class对象包含Controller注解,则将其实例化并保存到 ioc 中
            if (clazz.isAnnotationPresent(Controller.class)) {
                Controller controllerAnnotation = clazz.getAnnotation(Controller.class);
                Object o = clazz.newInstance();
                if ("".equals(controllerAnnotation.value())) {
                    //获取类名
                    String simpleName = clazz.getSimpleName();
                    //将类名首字母小写
                    String className = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
                    ioc.put(className, o);
                } else {
                    ioc.put(controllerAnnotation.value(), o);
                }
            } else if (clazz.isAnnotationPresent(Service.class)) {
                Service serviceAnnotation = clazz.getAnnotation(Service.class);
                String value = serviceAnnotation.value();
                //如果@Service注解的value值为空
                Object o = clazz.newInstance();
                if ("".equals(value)) {
                    //获取类的所有接口遍历,将接口名称注入到ioc容器
                    Class<?>[] interfaces = clazz.getInterfaces();
                    for (Class<?> anInterface : interfaces) {
                        String name = anInterface.getSimpleName();
                        name = name.substring(0, 1).toLowerCase() + name.substring(1);
                        ioc.put(name, o);
                    }
                    //获取实现类名
                    String simpleName = clazz.getSimpleName();
                    simpleName = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
                    ioc.put(simpleName, o);
                } else {
                    ioc.put(value, o);
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

测试

image-20220612114338794

5.实现任务阶段 5- 完成 Spring 容器对象的自动装配 -@Autowried

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

● 完成任务说明

  • 分析示意图 - 加入@AutoWired 注解, 进行对象属性的装配-如图

image-20220612140902267

2.分析+代码实现

1.新增@Autowired注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

    String value() default "";

}

2.在LLPWebApplicationContext-自定义spring容器中新增自动装配方法

/**
 * 完成属性的自动装配
 * 思路:实现自动装配,首先我们需要获取容器中的所有实例集合,遍历实例的字段,判断字段上面是否含有Autowired注解,如果存在则进行装配
 */
public void executeAutoWired() {
    //如果ioc为空则返回
    if (ioc.isEmpty()) {
        return;
    }
    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        //bean实例
        Object bean = entry.getValue();
        //获取实例的Class对象
        Class<?> clazz = bean.getClass();
        //获取所有字段
        Field[] declaredFields = clazz.getDeclaredFields();
        //遍历
        for (Field declaredField : declaredFields) {
            //判断字段是否被@Autowired注解修饰
            if (declaredField.isAnnotationPresent(Autowired.class)) {
                Autowired autowiredAnnotation = declaredField.getAnnotation(Autowired.class);
                String beanName = autowiredAnnotation.value();
                if ("".equals(beanName)) {
                    //如果没有指定value值,则获取类型名首字母小写作为beanName
                    Class<?> fieldType = declaredField.getType();
                    String simpleName = fieldType.getSimpleName();
                    beanName = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
                }
                //根据beanName从容器中获取bean实例
                Object o = ioc.get(beanName);
                if (o == null) {
                    throw new RuntimeException("容器中没有找到beanName=" + beanName + "对应的bean实例");
                }
                //防止属性是private, 我们需要暴力破解
                declaredField.setAccessible(true);
                try {
                    /**
                     * @param1: bean实例 MonsterController 实例
                     * @param2: 字段的值  MonsterService 实例
                     */
                    declaredField.set(bean, o);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

3.容器初始化时进行自动装配

/**
 * 编写方法,完成自己的spring容器的初始化
 * 容器的初始化,交由前端控制器
 * 而前端控制器由tomcat来触发
 */
public void init() {
    //解析我们自己的spring配置文件,获取基本包
    String basePackage = XMLParser.getBasePackage(configLocation);
    //考虑有多个包的情况,我们在llpspringmvc.xml中配置多个包时采用了逗号分割的方式
    String[] basePackages = basePackage.split(",");
    //遍历包
    if (basePackages.length > 0) {
        for (String pack : basePackages) {
            //扫描包路径下的所有.class类,并将类全路径放入classFullPathList集合中
            scanPackage(pack);
        }
    }
    //将扫描到的类, 在满足条件的情况下,通过反射注入到ioc容器
    executeInstance();
    //完成属性的自动装配
    executeAutoWired();

}

4.修改MonsterController

@RequestMapping(value = "/monster/list")
public void ListMonster(HttpServletRequest request, HttpServletResponse response) {
    //设置编码和返回的类型
    response.setContentType("text/html; charset=UTF-8");
    List<Monster> monsterList = monsterService.listMonsters();
    StringBuilder builder = new StringBuilder("<h1>妖怪列表信息</h1>");
    builder.append("<table border='1px' width='500px' style='border-collapse:collapse'>");
    for (Monster monster : monsterList) {
        builder.append("<tr><td>" + monster.getId() +
                "</td><td>" + monster.getName() +
                "</td><td>" + monster.getSkill() +
                "</td><td>" + monster.getAge() + "</td></tr>");
    }
    builder.append("</table>");
    String content = builder.toString();
    try {
        //获取writer返回信息
        PrintWriter printWriter = response.getWriter();
        printWriter.write(content);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

3.启动 Tomcat, 完成测试

6.实现任务阶段 6- 完成控制器方法获取参数-@RequestParam

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

● 完成任务说明

  • 前端页面

image-20220612211231606

  • 后端 Handler 的目标方法

    @RequestMapping(value = "/monster/find") 
    public void findMonstersByName(HttpServletRequest request,HttpServletResponse response, @RequestParam(value = "name") String name) {
        //代码.... 
    }
    

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

之前请求分发的问题

下面这段代码,通过反射调用的方法只针对method(HttpServletRequest request, HttpServletResponse response)这样的方法有效,因此我们需要将传递的参数封装到参数数组中,然后以反射调用的方式传递给目标方法

llpHandler.getMethod().invoke(llpHandler.getController(), request, response);

/**
 * 编写方法,根据request对象获取uri,查询HandlerList集合匹配对应的handler对象
 * 通过反射调用方法,进行请求分发
 */
public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
    //获取LLPHandler[url和控制器方法的映射]
    LLPHandler llpHandler = getLLPHandler(request);
    try {
        //考虑根据请求的uri没有匹配到对应的handler的情况,我们返回404NOTFOUND
        if (llpHandler == null) {
            PrintWriter writer = response.getWriter();
            writer.println("<h1>404 NOT FOUND</h1>");
            writer.close();
        } else {
            //如果匹配到了对应的handler,则调用方法实现请求分发
            llpHandler.getMethod().invoke(llpHandler.getController(), request, response);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

修改LLPDispatcherServlet 请求分发方法—executeDispatch

/**
     * 编写方法,根据request对象获取uri,查询HandlerList集合匹配对应的handler对象
     * 通过反射调用方法,进行请求分发
     */
    public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
        //获取LLPHandler[url和控制器方法的映射]
        LLPHandler llpHandler = getLLPHandler(request);
        try {
            //考虑根据请求的uri没有匹配到对应的handler的情况,我们返回404NOTFOUND
            if (llpHandler == null) {
                PrintWriter writer = response.getWriter();
                writer.println("<h1>404 NOT FOUND</h1>");
                writer.close();
            } else {
                //目标:HttpServletRequest request, HttpServletResponse response 封装到参数数组
                Method method = llpHandler.getMethod();
                //得到目标方法的参数信息
                Class<?>[] parameterTypes = method.getParameterTypes();
                //创建一个和目标方法相同大小的数组
                Object[] params = new Object[parameterTypes.length];
                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;
                    }
                }

                //如果匹配到了对应的handler,则调用方法实现请求分发
                //Object invoke(Object obj, Object... args)
                llpHandler.getMethod().invoke(llpHandler.getController(), params);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

测试效果

image-20220612174410640

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

● 完成任务说明

image-20220612211231606

  • 后端 Handler 的目标方法

    @RequestMapping(value = "/monster/find") 
    public void findMonstersByName(HttpServletRequest request,HttpServletResponse response, @RequestParam(value = "name") String name) {
        //代码.... 
    }
    

自定义@RequestParam注解

/**
 * RequestParam 注解标注在目标方法的参数上,表示对应http请求的参数
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface RequestParam {
    String value() default "";

}

MonsterController新增findMonsterByName方法

@RequestMapping(value = "/monster/find")
public void findMonsterByName(HttpServletRequest request, HttpServletResponse response, @RequestParam("name")String name) {
    //设置编码和返回的类型
    response.setContentType("text/html; charset=UTF-8");
    List<Monster> monsterList = monsterService.findMonsterByName(name);
    StringBuilder builder = new StringBuilder("<h1>妖怪列表信息</h1>");
    builder.append("<table border='1px' width='500px' style='border-collapse:collapse'>");
    for (Monster monster : monsterList) {
        builder.append("<tr><td>" + monster.getId() +
                "</td><td>" + monster.getName() +
                "</td><td>" + monster.getSkill() +
                "</td><td>" + monster.getAge() + "</td></tr>");
    }
    builder.append("</table>");
    String content = builder.toString();
    try {
        //获取writer返回信息
        PrintWriter printWriter = response.getWriter();
        printWriter.write(content);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

修改MonsterService

List<Monster> findMonsterByName(String name);

修改MonsterServiceImpl

@Override
public List<Monster> findMonsterByName(String name) {
    List<Monster> monsterList = new ArrayList<>();
    monsterList.add(new Monster(100,"孙悟空",1000,"打妖怪"));
    monsterList.add(new Monster(200,"白骨精",1000,"吸人血"));
    monsterList.add(new Monster(300,"蜘蛛精",1000,"吐丝"));
    List<Monster> monsters = new ArrayList<>();
    for (Monster monster : monsterList) {
        if(monster.getName().contains(name)){
            monsters.add(monster);
        }
    }
    return monsters;
}

修改LLPDispatcherServlet,获取浏览器请求参数并匹配目标方法下标封装到参数数组中对目标方法参数进行赋值

/**
     * 编写方法,根据request对象获取uri,查询HandlerList集合匹配对应的handler对象
     * 通过反射调用方法,进行请求分发
     */
    public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
        //获取LLPHandler[url和控制器方法的映射]
        LLPHandler llpHandler = getLLPHandler(request);
        try {
            //考虑根据请求的uri没有匹配到对应的handler的情况,我们返回404NOTFOUND
            if (llpHandler == null) {
                PrintWriter writer = response.getWriter();
                writer.println("<h1>404 NOT FOUND</h1>");
                writer.close();
            } else {
                //目标:HttpServletRequest request, HttpServletResponse response 封装到参数数组
                Method method = llpHandler.getMethod();
                //得到目标方法的参数信息
                Class<?>[] parameterTypes = method.getParameterTypes();
                //创建一个和目标方法相同大小的数组
                Object[] params = new Object[parameterTypes.length];
                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. 获取http请求的参数集合
                //http://localhost:8080/llp_myspringmvc/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
                //2. 返回的Map<String,String[]>
                // String:表示http请求的参数名
                // String[]:表示http请求的参数值,为什么是数组
                Map<String, String[]> parameterMap = request.getParameterMap();
                //3.遍历parameterMap获取浏览器端传入的参数在目标方法中的下标
                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                    //参数名
                    String name = entry.getKey();
                    //这里只考虑提交的参数是单值的情况,即不考虑类似checkbox提示的数据
                    String value = entry.getValue()[0];
                    //获取参数在目标方法中的下标
                    int indexRequestParameterIndex = getIndexRequestParameterIndex(method, name);
                    if(indexRequestParameterIndex !=-1){
                        //4.将从浏览器端传入参数的值赋值给params[目标参数所在下标]
                        params[indexRequestParameterIndex] = value;
                    }else {
                        //TODO 
                    }
                }

                //如果匹配到了对应的handler,则调用方法实现请求分发
                //Object invoke(Object obj, Object... args)
                llpHandler.getMethod().invoke(llpHandler.getController(), params);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

新增getIndexRequestParameterIndex方法,用于获取请求参数在目标方法中的下标

/**
     * 获取参数在目标方法中的下标
     *
     * @param method 目标方法
     * @param name   请求参数名
     * @return
     */
    public 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 parameterAnnotation = parameter.getAnnotation(RequestParam.class);
                String value = parameterAnnotation.value();
                //这里就是匹配的比较
                if (value.equals(name)) {
                    //找到请求的参数,对应的目标方法的形参的位置
                    return i;
                }
            }
        }
        //如果没有匹配成功,就返回-1
        return -1;
    }

测试效果

image-20220612225710459

image-20220612225727102

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

修改LLPDispatcherServlet,支持默认参数名匹配

/**
 * 编写方法,根据request对象获取uri,查询HandlerList集合匹配对应的handler对象
 * 通过反射调用方法,进行请求分发
 */
public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
    //获取LLPHandler[url和控制器方法的映射]
    LLPHandler llpHandler = getLLPHandler(request);
    try {
        //考虑根据请求的uri没有匹配到对应的handler的情况,我们返回404NOTFOUND
        if (llpHandler == null) {
            PrintWriter writer = response.getWriter();
            writer.println("<h1>404 NOT FOUND</h1>");
            writer.close();
        } else {
            //目标:HttpServletRequest request, HttpServletResponse response 封装到参数数组
            Method method = llpHandler.getMethod();
            //得到目标方法的参数信息
            Class<?>[] parameterTypes = method.getParameterTypes();
            //创建一个和目标方法相同大小的数组
            Object[] params = new Object[parameterTypes.length];
            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. 获取http请求的参数集合
            //http://localhost:8080/llp_myspringmvc/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
            //2. 返回的Map<String,String[]>
            // String:表示http请求的参数名
            // String[]:表示http请求的参数值,为什么是数组
            Map<String, String[]> parameterMap = request.getParameterMap();
            //3.遍历parameterMap获取浏览器端传入的参数在目标方法中的下标
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                //参数名
                String name = entry.getKey();
                //这里只考虑提交的参数是单值的情况,即不考虑类似checkbox提示的数据
                String value = entry.getValue()[0];
                //获取参数在目标方法中的下标
                int indexRequestParameterIndex = getIndexRequestParameterIndex(method, name);
                if (indexRequestParameterIndex != -1) {
                    //4.将从浏览器端传入参数的值赋值给params[目标参数所在下标]
                    params[indexRequestParameterIndex] = value;
                } else {
                    List<String> parametersList = getParameterNames(llpHandler.getMethod());
                    for (int i = 0; i < parametersList.size(); i++) {
                        if (name.equals(parametersList.get(i))) {
                            params[i] = value;
                            break;
                        }
                    }
                }
            }
            //如果匹配到了对应的handler,则调用方法实现请求分发
            //Object invoke(Object obj, Object... args)
            llpHandler.getMethod().invoke(llpHandler.getController(), params);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

新增getParameterNames方法将所有形参名称放入到集合中

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

注意这里获取到参数列表时这样的,parametersList=[args0,args1,arg2]

**1. 在默认情况下,返回的并不是 request, response ,name 而是 arg0, arg1, arg2 **

2. 需要使用到 jdk8 的新特性,并需要在 pom.xml 配置 maven 编译插件(可以 百度搜索到),才能得到 request, response, name

解决办法:

1.在pom.xml中添加插件

<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>

2.点击 maven 管理,clean 项目,在重启一下 tomcat ,完成测试

image-20220612234405930

可以看到这时获取到parametersListc参数列表就是正常的了

image-20220612234551271

7.实现任务阶段 7- 完成简单视图解析

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

● 完成任务说明 - 用户输入白骨精,可以登录成功, 否则失败 - 根据登录的结果, 可以重定向或者请求转发到 login_ok.jsp / login_error.jsp, 并显示名称

image-20220615000423311

image-20220615000542207

2.分析+代码

● 代码实现

1.前端页面准备

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录页面</title>
</head>
<body>
<h1>登录页面</h1>
<%--因为我这里工程路径是llp_myspringmv,因此monster前面是不能携带斜杠的,否则浏览器会将第一个斜杠解析成ip:port--%>
<form action="monster/login" method="post">
    妖怪名: <input type="text" name="monsterName"><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>
sorry, 登录失败 ${requestScope.monsterName}
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
<h1>登录成功</h1>
欢迎你: ${requestScope.monsterName}
</body>
</html>

2.Controller,Service准备

MonsterController

@RequestMapping(value = "/monster/login")
public String login(String monsterName,HttpServletRequest request){
    System.out.println("妖怪名称:"+monsterName);
    request.setAttribute("monsterName",monsterName);
    boolean b = monsterService.login(monsterName);
    if(b){
        //登录成功跳转到login_ok.jsp
        return "/login_ok.jsp";
    }
    //登录失败跳转到login_error.jsp
    return "redirect:/login_error.jsp";
}

MonsterService

boolean login(String monsterName);

MonsterServiceImpl

@Override
public boolean login(String monsterName) {
    if("llp".equals(monsterName)){
        return true;
    }
    return false;
}

3.修改LLPDispatcherServlet,executeDispatch-执行分发请求方法

这里做了简化,之前按我们通过反射调用目标方法实现请求分发,我们拿到反射调用方法的返回值,进行处理判断是重定向还是请求转发

/**
 * 编写方法,根据request对象获取uri,查询HandlerList集合匹配对应的handler对象
 * 通过反射调用方法,进行请求分发
 */
public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
    //获取LLPHandler[url和控制器方法的映射]
    LLPHandler llpHandler = getLLPHandler(request);
    try {
        //考虑根据请求的uri没有匹配到对应的handler的情况,我们返回404NOTFOUND
        if (llpHandler == null) {
            PrintWriter writer = response.getWriter();
            writer.println("<h1>404 NOT FOUND</h1>");
            writer.close();
        } else {
            //目标:HttpServletRequest request, HttpServletResponse response 封装到参数数组
            Method method = llpHandler.getMethod();
            //得到目标方法的参数信息
            Class<?>[] parameterTypes = method.getParameterTypes();
            //创建一个和目标方法相同大小的数组
            Object[] params = new Object[parameterTypes.length];
            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;
                }
            }

            //解决请求参数中文乱码问题
            request.setCharacterEncoding("UTF-8");

            //1. 获取http请求的参数集合
            //http://localhost:8080/llp_myspringmvc/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
            //2. 返回的Map<String,String[]>
            // String:表示http请求的参数名
            // String[]:表示http请求的参数值,为什么是数组
            Map<String, String[]> parameterMap = request.getParameterMap();
            //3.遍历parameterMap获取浏览器端传入的参数在目标方法中的下标
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                //参数名
                String name = entry.getKey();
                //这里只考虑提交的参数是单值的情况,即不考虑类似checkbox提示的数据
                String value = entry.getValue()[0];
                //获取参数在目标方法中的下标
                int indexRequestParameterIndex = getIndexRequestParameterIndex(method, name);
                if (indexRequestParameterIndex != -1) {
                    //4.将从浏览器端传入参数的值赋值给params[目标参数所在下标]
                    params[indexRequestParameterIndex] = value;
                } else {
                    List<String> parametersList = getParameterNames(llpHandler.getMethod());
                    for (int i = 0; i < parametersList.size(); i++) {
                        if (name.equals(parametersList.get(i))) {
                            params[i] = value;
                            break;
                        }
                    }
                }
            }
            //如果匹配到了对应的handler,则调用方法实现请求分发
            //Object invoke(Object obj, Object... args)
            //获取反射执行方法的返回结果
            //这里就是对返回的结果进行解析=>原生springmvc 可以通过视图解析器来完成
            Object result = llpHandler.getMethod().invoke(llpHandler.getController(), params);
            if (result instanceof String) {
                String viewName = (String) result;
                if (viewName.contains(":")) {
                    String[] split = viewName.split(":");
                    String viewType = split[0];
                    if ("forward".equals(viewType)) {
                        //执行请求转发
                        //forward:/login_error.jsp
                        //viewType=forward viewPage=/login_ok.jsp
                        String viewPage = split[1];
                        //请求转发是在服务器端执行,第一个斜杠 “/” 应该被解析成 ip:port/工程路径/
                        request.getRequestDispatcher(contextPath+"/"+viewPage).forward(request, response);
                    } else if ("redirect".equals(viewType)) {
                        String viewPage = split[1];
                        //请求重定向,浏览器会再次发起http请求, 在servlet中第一个斜杠 "/" 被解析成 ip:port
                        //在springmvc中底层会拼接上contextPath,给viewPage拼接上工程路径页面才能正常的跳转
                        String contextPath = request.getContextPath();
                        response.sendRedirect(contextPath + viewPage);
                    }
                }else {
                    //默认按照请求转发处理
                    request.getRequestDispatcher(viewName).forward(request, response);
                }

            }

        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

8.实现任务阶段 8- 完成返回 JSON 格式数据-@ResponseBody

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

2.分析+代码实现

● 分析示意图

​ 在前面我们在LLPDispatcherServlet中在执行请求分发时,获取到了目标方法的返回值。通过返回值进行了请求的转发和重定向,

以此类推,我们可以在请求分发中扩展@ResponseBody注解返回json的功能。

● 代码实现

1.首先我们需要自己去定义一个我们自己的@ResponseBody注解

/**
 * 修饰方法返回json字符串
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ResponseBody {
}

2.在MonsterController中增加测试方法

@ResponseBody
    @RequestMapping(value = "/monster/list/json")
    public List<Monster> listMonstersByJson(HttpServletRequest request, HttpServletResponse response) {
        List<Monster> monsters = monsterService.listMonsters();
        return monsters;
    }

3.引入json处理工具包,比如gson,hutool,fastjson,jackson,这里我们就引入jackson进行处理

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

4.在LLPDispatcher-executeDispatch请求分发方法中完成@ResponseBody返回json字符串给浏览器的功能

/**
 * 编写方法,根据request对象获取uri,查询HandlerList集合匹配对应的handler对象
 * 通过反射调用方法,进行请求分发
 */
public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
    //获取LLPHandler[url和控制器方法的映射]
    LLPHandler llpHandler = getLLPHandler(request);
    try {
        //考虑根据请求的uri没有匹配到对应的handler的情况,我们返回404NOTFOUND
        if (llpHandler == null) {
            PrintWriter writer = response.getWriter();
            writer.println("<h1>404 NOT FOUND</h1>");
            writer.close();
        } else {
            //目标:HttpServletRequest request, HttpServletResponse response 封装到参数数组
            Method method = llpHandler.getMethod();
            //得到目标方法的参数信息
            Class<?>[] parameterTypes = method.getParameterTypes();
            //创建一个和目标方法相同大小的数组
            Object[] params = new Object[parameterTypes.length];
            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;
                }
            }

            //解决请求参数中文乱码问题
            request.setCharacterEncoding("UTF-8");

            //1. 获取http请求的参数集合
            //http://localhost:8080/llp_myspringmvc/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
            //2. 返回的Map<String,String[]>
            // String:表示http请求的参数名
            // String[]:表示http请求的参数值,为什么是数组
            Map<String, String[]> parameterMap = request.getParameterMap();
            //3.遍历parameterMap获取浏览器端传入的参数在目标方法中的下标
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                //参数名
                String name = entry.getKey();
                //这里只考虑提交的参数是单值的情况,即不考虑类似checkbox提示的数据
                String value = entry.getValue()[0];
                //获取参数在目标方法中的下标
                int indexRequestParameterIndex = getIndexRequestParameterIndex(method, name);
                if (indexRequestParameterIndex != -1) {
                    //4.将从浏览器端传入参数的值赋值给params[目标参数所在下标]
                    params[indexRequestParameterIndex] = value;
                } else {
                    List<String> parametersList = getParameterNames(llpHandler.getMethod());
                    for (int i = 0; i < parametersList.size(); i++) {
                        if (name.equals(parametersList.get(i))) {
                            params[i] = value;
                            break;
                        }
                    }
                }
            }
            //如果匹配到了对应的handler,则调用方法实现请求分发
            //Object invoke(Object obj, Object... args)
            //获取反射执行方法的返回结果
            //这里就是对返回的结果进行解析=>原生springmvc 可以通过视图解析器来完成
            Object result = llpHandler.getMethod().invoke(llpHandler.getController(), params);
            if (result instanceof String) {
                if (llpHandler.getMethod().isAnnotationPresent(ResponseBody.class)) {
                    //方法被@ResponseBody注解修饰,将返回结果转换成json字符串返回给浏览器
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    response.setContentType("text/html;charset=utf-8");
                    PrintWriter writer = response.getWriter();
                    writer.write(json);
                    writer.flush();
                    writer.close();
                } else {
                    String viewName = (String) result;
                    if (viewName.contains(":")) {
                        String[] split = viewName.split(":");
                        String viewType = split[0];
                        if ("forward".equals(viewType)) {
                            //执行请求转发
                            //forward:/login_error.jsp
                            //viewType=forward viewPage=/login_ok.jsp
                            String viewPage = split[1];
                            //请求转发是在服务器端执行,第一个斜杠 “/” 应该被解析成 ip:port/工程路径/
                            request.getRequestDispatcher(contextPath + "/" + viewPage).forward(request, response);
                        } else if ("redirect".equals(viewType)) {
                            String viewPage = split[1];
                            //请求重定向,浏览器会再次发起http请求, 在servlet中第一个斜杠 "/" 被解析成 ip:port
                            //在springmvc中底层会拼接上contextPath,给viewPage拼接上工程路径页面才能正常的跳转
                            String contextPath = request.getContextPath();
                            response.sendRedirect(contextPath + viewPage);
                        }
                    } else {
                        //默认按照请求转发处理
                        request.getRequestDispatcher(viewName).forward(request, response);
                    }
                }
            } else if (result instanceof ArrayList) {
                Method m = llpHandler.getMethod();
                if (m.isAnnotationPresent(ResponseBody.class)) {
                    //方法被@ResponseBody注解修饰,将返回结果转换成json字符串返回给浏览器
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    response.setContentType("text/html;charset=utf-8");
                    PrintWriter writer = response.getWriter();
                    writer.write(json);
                    writer.flush();
                    writer.close();
                }
            }

        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.完成测试

image-20220615233150618


标题:手动实现 SpringMVC 底层机制
作者:llp
地址:https://llinp.cn/articles/2022/06/12/1655048853502.html