1.写在前面
笔者打算今天手动模拟一个SpringMVC,因为在之前学习SpringMVC的时候,都是没有模拟过SpringMVC,我们只停在应用的级别,都是真的了解过源码,这样就不容易记住,今天笔者先是介绍下SpringMVC的使用,然后笔者手动模拟一个SpringMVC。
2.SpringMVC的使用
学习SpringMVC的最好的方式当然是Spring的官网了。我们先根据Spring的官网,来搭建一个基于xml版的SpringMVC的项目,首先让我们来看下Spring的官网,为了节省大家的时间,直接上代码吧!先是pom.xml
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>80</port>
<path>/</path>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
</plugins>
</build>
主要导入了一个springMVC的依赖,然后就是导入了一个Tomcat的插件。接下来看的就是web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:SpringMVC.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
主要配置了一个DispatcherServlet的类,然后指定了SpringMVC的配置的文件,然后配置了拦截的路径。接下来看的就是SpringMVC.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--配置扫描的路径-->
<context:component-scan base-package="com.ys.controller"/>
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"/>
<property name="suffix" value=".html"/>
</bean>
</beans>
最后我们再写一个Controller类的,具体的代码如下:
package com.ys.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/test")
public class Test {
@RequestMapping("/test.do")
public String test() {
System.out.println("调用了这个方法");
return "index";
}
}
最后再来看下我们默认的index.html,具体的如下:
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<h1>测试SpringMVC</h1>
</body>
</html>
最后的运行的结果如下:
3.手动模拟springMVC
现象:上面就是写了一个方法,然后加上对应的注解,然后在浏览器中输入对应的地址就是可以调用到对应的方法,同时也能跳转对应的页面。
思路:我们要配置好我们自己的servlet,然后通过这个servlet的生命周期的方法,解析web.xml中配置的SpringMVC.xml中配置的内容,然后扫描对应的包下面的加了@Controller注解的类,然后遍历这个类中所有加了@RequestMapping注解的方法,然后将这些方法存到对应的Map中去,其中键是请求的路径,然后值是对应的方法,这样我们就能做到在浏览器中输入对应的地址,然后调用对应的方法了。废话不多说,直接上代码。
我们先来看下我们的pom.xml
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>80</port>
<path>/</path>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
上面笔者导入了一个dom4j的依赖用来解析xml的文件,然后就是servlet的依赖。接下来我们就要模拟springMVC的核心的servlet的类,在模拟这个类之前,笔者先写好web.xml中的东西,具体的内容如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>com.ys.servlet.MyMVCServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>MyMVC.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
接下来我们书写我们的配置类,类比SpringMVC.xml
<beans>
<componentScan package="com"></componentScan>
<view prefix = "/page/" suffix=".html"></view>
</beans>
然后我们定义了几个注解类,分别是Controller、RequestMapping、ResponseBody,具体的代码如下:
package com.ys.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
package com.ys.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
//URL 拦截的地址
String value() default "";
}
package com.ys.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}
然后我们又定义了一个实体类UserEntity,用来测试接收参数,具体的代码如下:
package com.ys.entity;
public class UserEntity {
private String name;
private String age;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "UserEntity{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
", sex='" + sex + '\'' +
'}';
}
}
然后我们又定义了一个HTML的页面,用来测试跳转页面的。具体的代码如下:
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>这里是index.html</h1>
</body>
</html>
接下来我们就书写我们的MyMVCServlet类,模拟springMVC的核心类。具体的代码如下:
package com.ys.servlet;
import com.ys.annotation.Controller;
import com.ys.annotation.RequestMapping;
import com.ys.annotation.ResponseBody;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class MyMVCServlet extends HttpServlet {
private static String COMPONENT_SCAN_ELEMENT_PACKAGE_NAME = "package";
private static String COMPONENT_SCAN_ELEMENT_NAME = "componentScan";
private static String VIEW_ELEMENT_NAME = "view";
private static String VIEW_ELEMENT_PREFIX_NAME = "prefix";
private static String VIEW_ELEMENT_SUFFIX_NAME = "suffix";
private static String XML_PATH_LOCAL = "contextConfigLocation";
private static String prefixValue = "";
private static String suffixValue = "";
private static String packageValue = "";
private static String projectPath = MyMVCServlet.class.getResource("/").getPath();
//用来存解析出来的方法
private static Map<String, Method> methodMap = new HashMap<>();
/**
* init主要做得事情:
* 加载配置文件 web.xml 加载spring mvc.xml
* 扫描整个项目 根据配置文件给定的目录来扫描
* 扫描所有加了@Controller注解的类
* 当扫描到加了@Controller注解的类之后遍历里面所有的方法
* 拿到方法对象之后 解析方法上面是否加了@RequestMapping注解
* 定义一个Map集合 吧@RequstMapping的Value 与方法对象绑定起来
* Map<String,Method>
*
* @param config web.xml
* @throws ServletException 异常
*/
@Override
public void init(ServletConfig config) throws ServletException {
//获取web.xml中contextConfigLocation配置的值
String initParameter = config.getInitParameter(XML_PATH_LOCAL);
//获取类似springMVC.xml的文件
File file = new File(projectPath + "//" + initParameter);
Document parse = parse(file);
//根节点
Element rootElement = parse.getRootElement();
//componentScan节点
Element componentScanEle = rootElement.element(COMPONENT_SCAN_ELEMENT_NAME);
//componentScan节点中的package的属性值
packageValue = componentScanEle.attributeValue(COMPONENT_SCAN_ELEMENT_PACKAGE_NAME);
//view 节点
Element viewEle = rootElement.element(VIEW_ELEMENT_NAME);
//prefix
prefixValue = viewEle.attributeValue(VIEW_ELEMENT_PREFIX_NAME);
//suffix
suffixValue = viewEle.attributeValue(VIEW_ELEMENT_SUFFIX_NAME);
//扫描项目
scanProjectByPath(projectPath + "\\" + packageValue);
}
private void scanProjectByPath(String projectPath) {
File file = new File(projectPath);
//递归解析项目的所有文件
scanFile(file);
}
private void scanFile(File file) {
//是文件夹,递归
if (file.isDirectory()) {
for (File listFile : Objects.requireNonNull(file.listFiles())) {
scanFile(listFile);
}
} else { // 不是文件夹直接解析
String filePath = file.getPath();
String suffix = filePath.substring(filePath.lastIndexOf("."));
if (".class".equals(suffix)) {
String classPath = filePath.replace(new File(projectPath).getPath() + "\\", "");
classPath = classPath.replaceAll("\\\\", ".");
//获取到类名
String className = classPath.substring(0, classPath.lastIndexOf("."));
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(Controller.class)) {
RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
String classRequestMappingUrl = "";
if (classRequestMapping != null) {
//获取类上面的RequestMapping的value的值
classRequestMappingUrl = classRequestMapping.value();
}
//遍历所有的方法
for (Method method : clazz.getDeclaredMethods()) {
if (!method.isSynthetic()) {
//获取方法上的RequestMapping的注解
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
if (annotation != null) {
String methodRequestMappingUrl = "";
methodRequestMappingUrl = annotation.value();
System.out.println("类:" + clazz.getName() + "的" + method.getName() + "方法映射到了" + classRequestMappingUrl + methodRequestMappingUrl + "上面");
methodMap.put(classRequestMappingUrl + methodRequestMappingUrl, method);
}
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
/**
* @param file 要解析的文件
* @return 返回解析好的Document
*/
public Document parse(File file) {
SAXReader saxReader = new SAXReader();
try {
return saxReader.read(file);
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
/**
* 执行的时候做的事情:
* 拿到请求URI去map里面get
* 给参数赋值并调用方法
* 拿到方法返回值做视图跳转和消息返回
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//拿到请求的URI
String requestURI = request.getRequestURI();
Method method = methodMap.get(requestURI);
if (method != null) {
Parameter[] parameters = method.getParameters();
Object[] objects = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String name = parameter.getName();
Class<?> type = parameter.getType();
if (type.equals(String.class)) {
objects[i] = request.getParameter(name);
} else if (type.equals(HttpServletRequest.class)) {
objects[i] = request;
} else if (type.equals(HttpServletResponse.class)) {
objects[i] = response;
} else {
try {
Object o = type.newInstance();
for (Field field : type.getDeclaredFields()) {
field.setAccessible(true);
String fieldName = field.getName();
field.set(o, request.getParameter(fieldName));
}
objects[i] = o;
} catch (Exception e) {
e.printStackTrace();
}
}
}
Object o = null;
try {
o = method.getDeclaringClass().newInstance();
Object invoke = method.invoke(o, objects);
//判断返回值
if (!method.getReturnType().equals(Void.class)) {
ResponseBody annotation = method.getAnnotation(ResponseBody.class);
if (annotation != null) {
response.getWriter().write(String.valueOf(invoke));
} else {
request.getRequestDispatcher(prefixValue + String.valueOf(invoke) + suffixValue).forward(request, response);
}
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
response.setStatus(404);
}
}
}
最后再来看下我们书写的Controller类,具体的代码如下:
package com.ys.controller;
import com.ys.annotation.Controller;
import com.ys.annotation.RequestMapping;
import com.ys.annotation.ResponseBody;
import com.ys.entity.UserEntity;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
@RequestMapping("/test")
public class TestController {
@RequestMapping("/test.do")
@ResponseBody
public Object test(String name, HttpServletRequest request, HttpServletResponse response, UserEntity userEntity){
request.getParameter("name");
System.out.println(name);
System.out.println(request);
System.out.println(response);
System.out.println(userEntity);
return "test";
}
@RequestMapping("/model.do")
public Object model(){
return "index";
}
}
运行的结果如下:
可以看到我们的结果已经映射上去了,已经存到对应的map中去了。然后我们再来看下浏览器中的运行的结果,走来先看页面的跳转,具体的如下:
可以看到我们的页面的跳转也是没有问题的。最后再看下我们参数的封装,具体的运行的结果如下:
可以看到我们的参数也是封装进来了。至此一个简单的springMVC就结束了。
4.写在最后
通过我们简单的手动模拟springMVC,这样就能对springMVC理解的更加深刻一点。源码我会上传的。可以去我的资源中下载。