搭建类似于SpringMVC的简单SimpleMVC框架
SpringMVC核心组件
- DispatcherServlet:前端控制器,用于接收所有请求。
- HandlerMapping:用于配置请求路径与Controller组件的对应关系。
- Controller:控制器,具体处理请求的组件。
- ModelAndView:Controller组件处理完请求后得到的结果,由数据与视图名称组成。
- ViewResolver:视图解析器,可根据视图名称确定需要使用的视图组件。
SimpleMVC实现的核心组件有
DispatcherServlet,HandlerMapping,Controller视图使用的是JSP
下面开始搭建项目
本次项目是依据下面的顺序来进行的
- JSP、Controller、HandlerMapping、DispatcherServlet
下面开始
1.创建一个maven项目,注意这里要选择war包,因为需要用到wabapp/WEB-INF/web.xml这个配置文件。
2.在wabapp/WEB-INF下创建一个hello.jsp页面。
3.有了显示页面以后,就需要有Controller来处理客户端的请求,所以在src/main/java下创建包名为controller的HelloController.java类,类中有处理客户端hello.do请求的hello方法,当前方法上的注解报错,是因为当前注解还不存在,所以下面我们需要创建该注解。
4.在HelloController中需要用到@requestMapping注解,但是当前项目中还没有该注解,所以需要在src/main/java下创建一个包名为bean.annotation的注解RequestMapping。当该注解创建完成后,HelloController只需要导一下包就不会报错了。
package bean.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 自定义映射注解
* 映射客户端请求与对应的方法
*
*
* @Retention 注解中的注解,成为元注解
* 有一个value属性:是RetentionPolicy类型的,它是一个枚举
* RetentionPolicy:有3个值分别是CLASS、RUNTIME、SOURCE
*
* 按声明周期划分:
* RetentionPolicy.SOURCE:注解只保留在源文件,当java文件编译成class文件,注解被遗弃
*
* RetentionPolicy.CLASS:注解保留到class文件,jvm加载class文件时,注解被遗弃,
* 这是默认的生命周期
*
* RetentionPolicy.RUNTIME:注解不仅保存在class文件,jvm加载class文件之后,仍然存在
* 这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
/**
* 当前value对应注解括号中的值
*/
public String value();
}
5.当控制器和映射注解都有了以后,我们接下来就需要在src/main/java下创建一个包名为bean.web的DispatcherServlet的前端控制器,这是一个servlet而不是一个class,内容如下:
package bean.web;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 前端控制器
*/
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* 初始化方法
*/
public void init(ServletConfig config) throws ServletException {
}
/**
* 接收客户端请求方法
*/
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
当前Servlet的配置在web.xml下可以看到,配置如下,注意这里的<url-pattern>*.do</url-pattern>
是跟上面HelloController方法上@RequestMapping(hello.do)
注解括号中的请求结尾对应。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<display-name>simpleMVC</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>DispatcherServlet</display-name>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>bean.web.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
可以将配置精简为
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>bean.web.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
配置DispatcherServlet,因为当前Servlet的init()
需要在tomcat容器启动时,就被加载,所以需要在web.xml配置文件中添加<load-on-startup>1</load-on-startup>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>bean.web.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
为所有的Controller配置一个映射文件simplemvc.xml,当前只有一个HelloController.java需要配置,在src/mian/resources下新建一个xml文件,如:simplemvc.xml,配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<!-- class属性配置的值为HelloController类所在的类全名 -->
<bean class="controller.HelloController" />
<!-- 后续如果还有控制器,依据HelloController的配置,继续配置就行 -->
</beans>
接下来是需要导入dom4j的依赖,在项目的pom.xml下配置,为解析配置有Controller的simplemvc.xml文件做准备。
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
如上的依赖和xml配置文件都准备好了以后,接下来只需在DispatcherServlet中添加List obj
、HandlerMapping handlerMapping
:(这个对象需要创建,当前项目中还没有)属性和init()
方法中进行解析和配置映射关系即可,如下:
init()
:方法的配置
package bean.web;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import bean.common.HandlerMapping;
/**
* 前端控制器
*/
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 存储所有实例化的控制器类
private List obj = new ArrayList<>();
// 创建处理映射关系的类HandlerMapping
private HandlerMapping handlerMapping = new HandlerMapping();
/**
* 初始化方法
*/
public void init(ServletConfig config) throws ServletException {
// 获取解析容器
SAXReader sax = new SAXReader();
// 获取解析文件所在
InputStream in = getClass().getClassLoader().getResourceAsStream("simplemvc.xml");
try {
// 将文件解析出来,需要处理DocumentException异常
Document doc = sax.read(in);
// 获取doc的根节点
Element root = doc.getRootElement();
// 获取根节点下所有的子节点
List<Element> list = root.elements();
// 遍历所有子节点,获取所有class属性所对应的controller
for (Element e : list) {
// 获取到类名
String className = e.attributeValue("class");
// 将该类实例化,需要处理异常
Object o = Class.forName(className).newInstance();
// 将实例化的控制器类,添加到obj集合中
obj.add(o);
}
// 调用HandlerMapping中的process方法
handlerMapping.process(obj);
} catch (Exception e) {
e.printStackTrace();
// 打桩,看配置是否没有问题
System.out.println("初始化失败");
}
}
/**
* 接收客户端请求方法
*/
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
}
HandlerMapping
类的创建src/main/java/bean/common下,在该类需要使用Handler类,所以还需要创建Handler
类:
HandlerMapping
package bean.common;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import bean.annotation.RequestMapping;
/**
* 处理控制器方法与客户端请求的映射关系
*/
public class HandlerMapping {
// 存储请求路径与控制器实例和方法的Map
private Map<String, Handler> handlerMap = new HashMap<>();
// 存放控制器与控制器方法的Handler
private Handler handler = new Handler();
public void process(List obj) {
// 遍历所有实例对象
for (Object o : obj) {
// 查看当前实例的类上是否有注解
RequestMapping orm = o.getClass().getAnnotation(RequestMapping.class);
// 类上注解的映射路径
String path = "";
// 当前类有注解
if(orm != null) {
path = orm.value();
}
// 当前类没有注解
// 获取实例中所有方法
Method[] cla = o.getClass().getDeclaredMethods();
// 遍历所有的方法
for (Method m : cla) {
// 获取方法上的注解
RequestMapping mrm = m.getAnnotation(RequestMapping.class);
// 方法上有注解的情况
if(mrm != null) {
// 获取注解中的值
path += mrm.value();
// 当前控制器实例
handler.setO(o);
// 当前控制器有注解的方法
handler.setM(m);
handlerMap.put(path, handler);
}
}
}
// 查看所有映射与对应的handler
System.out.println("HandlerMap=" + handlerMap);
}
// 根据path获取Handler
public Handler getHandler(String path) {
return handlerMap.get(path);
}
}
Handler
package bean.common;
import java.lang.reflect.Method;
/**
* 存储控制器与对应方法的封装类
*/
public class Handler {
// 封装的控制器实例
private Object o;
// 封装的控制器对应方法
private Method m;
public Object getO() {
return o;
}
public void setO(Object o) {
this.o = o;
}
public Method getM() {
return m;
}
public void setM(Method m) {
this.m = m;
}
}
将simpleMVC项目添加到tomcat容器中,并启动容器,如果一切都正常则会在控制台,显示如下
HandlerMap={hello.do=bean.common.Handler@3cd3e762}
,这行语句就是我们在HandlerMapping的process()
方法中的输出语句。
6.当如上配置一切都正常,接下来,就可以在service()
方法中获取客户端的请求,然后根据请求路径到HandlerMapping的getHandler()
方法中获取对应的控制器与方法,利用反射机制使其运行。
/**
* 接收客户端请求方法
*/
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取客户端请求,一般是localhost:8080/simpleMVC/hello.do这样的一个整体
String requestPath = request.getRequestURI();
System.out.println("requestPath=" + requestPath);
// 需要获取应用名后面的路径hello.do,所以需要现获得应用名simpleMVC/
String app = request.getContextPath();
// 截取hello.do
String path = requestPath.substring(app.length());
// 根据path获取Handler
System.out.println("path=" + path);
Handler handler = handlerMapping.getHandler(path);
// 判断当前handler是否为空,如果路径所对应的Handler不存在的话,为null
if(handler == null) {
// 返回404
response.sendError(404);
return;
}
// handler不为空,获取handler中的controller和method
Object o = handler.getO();
Method m = handler.getM();
try {
// 处理invoke异常,根据反射机制,调用该方法
String viewName = m.invoke(o).toString();
System.out.println("viewName=" + viewName);
request.getRequestDispatcher("/WEB-INF/" + viewName + ".jsp").forward(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
7.当上述方法写完以后,就可以启动tomcat,然后在浏览器上输入localhost:8080/simpleMVC/hello.do
进行测试即可,如果一切正常将会在浏览器上看到