此篇文章主要是讲解如何手写SpringMVC的思路,其中会涉及到一部分的代码来辅助来说明思路,希望对想手写SpringMVC,而无从下手的朋友有些帮助
一、项目结构
搭建如下架构,其中,web存放自定义写的异常类以及两个核心类用于处理各种异常及处理请求,dao存放类用于通用数据访问
二、代码
1.DispatcherServlet.java
核心的配置类
1、实现思路:在DispatcherServlet类中,实现下面几个步骤:
- 加载配置类
- 扫描当前项目下的所有文件
- 拿到扫描到的类,通过反射机制,实例化。并且放到ioc容器中(Map的键值对)
- 初始化path与方法的映射
2、运行阶段
每一次请求将会调用doGet或doPost方法,它会根据url请求去HandlerMapping中匹配到对应的Method,然后利用反射机制调用Controller中的url对应的方法,并得到结果返回。按顺序包括以下功能:
- 获取请求传入的参数并处理参数
- 通过初始化好的handlerMapping中拿出url对应的方法名,反射调用
package my.framework.web;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID=-3619491573769301637L;
/**
* 默认的配置文件名,可以在web.xml中通过初始化参数configFileName修改
*/
private String configFileName="myweb.xml";
/**
* 配置信息初始化器,加载配置文件并保存
*/
private ControllerMappingManager ctrMappingMgr;
/**
* 存放Controller实例HashMap,
* Key为Controller的全类名,value为Key所指的类的实例
*/
protected Map<String,Object>controllers=new HashMap<String, Object>();
/**
* 为每个Controller实例存放其方法实例,Key为方法的映射信息,value为Method实例
*/
protected Map<String, Method>methods=new HashMap<String, Method>();
public void init(){
//读取web.xml中提供的配置文件的位置信息
String configFileName=this.getServletConfig().getInitParameter("configFileName");
if(!(configFileName==null || (configFileName=configFileName.trim()).equals(""))){
this.configFileName=configFileName;
}
//初始化加载映射配置文件中的信息
ctrMappingMgr=new ControllerMappingManager(this.configFileName);
synchronized (controllers){
controllers.clear();
}
synchronized (methods){
methods.clear();
}
}
public void destroy(){
super.destroy();//Just puts "destroy" string in log
synchronized (controllers){
controllers.clear();
}
synchronized (methods){
methods.clear();
}
this.ctrMappingMgr.getControllerMappings().clear();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
execute(request,response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
/**
* 根据映射信息获取Controller的调用信息并调用
* 映射信息形式为:/aaa/.../zzz
* @param request
* @param response
*/
protected void execute(HttpServletRequest request,HttpServletResponse response){
String path=null;
//先考虑按前缀映射取出Servlet访问路径中包含的映射信息
//即Servlet采用/xxx/*的映射方式
//取到的映射信息是/xxx后面,包含,开头
//先看一下是不是包含动作<jsp:include>发出的请求,如果是,则此项有值
path=(String)request.getAttribute("javax.servlet.include.path_info");
if(path==null){//取不到说明不包含动作,则正常取值
path=request.getPathInfo();
//依然为空说明不是按前缀映射
if(path==null || (path=path.trim()).isEmpty()){
//再考虑按扩展名映射取出Servlet访问路径的映射信息
//即Servlet采用*.xxx的映射方法,取到映射信息包含扩展名部分
//先看一下是不是包含动作<jsp:include>发出的请求,如果是则此项有值
path=(String)request.getAttribute("javax.servlet.include.servlet_path");
if(path==null){//取不到说明不是包含动作,则正常取值
path=request.getServletPath();
}
//把扩展名截掉
int slash=path.lastIndexOf("/");
int period=path.lastIndexOf(".");
if((period>=0)&&(period>slash)){
path=path.substring(0,period);
}
}
}
//如果根据path找不到Controller的信息,抛出PathException异常
if(!ctrMappingMgr.containsKey(path)){
throw new PathException("No Controller & Method is mapped with this path:"+path);
}
ControllerMapping controllerMapping=ctrMappingMgr.getControllerMapping(path);
try {
Object instance=null;
//根据得到的全类名,创建该类的唯一实例
synchronized (controllers){
//从集合属性
instance=controllers.get(controllerMapping.getClassName());
//从集合属性controllers中查找已创建Controller实例
if(instance==null){
instance=Class.forName(controllerMapping.getClassName()).newInstance();
//如未找到,则通过反射加载,并放入集合
controllers.put(controllerMapping
.getClassName(),instance);
}
}
//调用指定方法
Method method=null;
synchronized (method){
//从集合属性methods中查找已创建的Method实例
method=methods.get(path);
//如果未找到,则通过反射加载,并放入集合
if(method==null){
method=instance.getClass().getMethod(controllerMapping.getMethodName(),
HttpServletResponse.class,HttpServletResponse.class);
methods.put(path,method);
}
}
Object result=method.invoke(instance,request,response);
toView(request,request,response);//对响应模式统一进行处理
} catch (Exception e) {
//异常
throw new RuntimeException(e);
}
}
protected void toView(Object result,HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
if(result==null){
return;
}
if(result instanceof String){//字符串类型表示导航路径
boolean isRedirect=false;
String url=(String)result;
if(url.startsWith("redirect:")){
isRedirect=true;
url=url.substring("redirect:".length());
}else if(url.startsWith("forward:")){
url=url.substring("forward:".length());
}
//导航路径必须以"/"开始,表示上下文根路径
if(!(url=url.trim()).startsWith("/")){
throw new ViewPathException();
}
if(isRedirect){
response.sendRedirect(request.getContextPath()+url);
}else{
request.getRequestDispatcher(url).forward(request,response);
}
}else{//其他Java对象类型以JSON格式的响应返回
PrintUtil.write(result,response);
}
}
}
2.ControllerMappingManager.java
以下代码步骤:
- 通过输入流方式获取Controller配置文件路径并且读取映射文件
- 使用双重for循环遍历获取元素中类型以及映射完整路径
- 在遍历期间对各个元素进行检查然后进行异常处理
- 将获取到的URL完整路径和调用方法保存在Map中
package my.framework.web;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class ControllerMappingManager {
public Map<String, ControllerMapping> getControllerMappings() {
return controllerMappings;
}
/**
* 保存URL和Java调用信息的映射关系
*/
private Map<String,ControllerMapping>controllerMappings=new HashMap<String, ControllerMapping>();
/**
* 构造方法
* @param configFileName
* 配置文件名
*/
public ControllerMappingManager(String configFileName){
init(configFileName);
}
/**
* init方法用来加载Controller配置文件
* @param configFileName
*/
public void init(String configFileName){
InputStream is=this.getClass().getResourceAsStream("/"+configFileName);
Document doc;
try {
doc=new SAXReader().read(is);//读取映射文件
} catch (Exception e) {
throw new ConfigLoadingException(e);
}
Element root=doc.getRootElement();
//查找controllers-->controllers元素,并对controller元素进行遍历
Iterator<Element> controllersIt=root.elements("controllers").iterator();
Element controllersEl=controllersIt.next();
for (Iterator<Element> controllerIt=controllersEl.elementIterator("controllers");controllerIt.hasNext();){
Element controller=controllerIt.next();
//获取controllers元素中类型映射
String firstPath=controller.attributeValue("path");
String className=controller.attributeValue("class");
if(className.isEmpty()){
throw new ConfigException("Controller的映射信息不能为空!");
}
ControllerMapping mapping=null;
//遍历每个controller元素中的method员元素
for(Iterator<Element> methodIt=controller.elementIterator("method");methodIt.hasNext();){
Element method=methodIt.next();
//获取method元素中的方法映射信息
String secondPath=method.attributeValue("path");
String methodName=method.getText();
if(methodName.isEmpty()){
throw new ConfigException("方法的映射信息不能为空!");
//得到每个方法完整的映射路径
}
String fullPath=(firstPath+secondPath).replaceAll("//","/");
//检查是否存在重复映射,每个方法调用的映射路径必须是唯一的
if(controllerMappings.containsKey(fullPath)){
throw new ConfigException("不能将路径"+fullPath+"映射到"+className+"."+methodName+"()\n"+
"路径"+fullPath+"已被"+mapping.getClassName()+"."+mapping.getMethodName()+"(映射)");
}
//不重复则将URL和调用信息对应保存在Map中
mapping=new ControllerMapping(className,methodName);
controllerMappings.put(fullPath,mapping);
}
}
}//init()方法结束
/**
* 根据映射信息查询对应的ControllerMapping实例
* @param path
* @return ControllerMapping
*/
public ControllerMapping getControllerMapping(String path){
return this.controllerMappings.get(path);
}
/**
* 判读是在对应的映射信息
* @param path
* @return
*/
public boolean containsKey(String path){
return this.controllerMappings.containsKey(path);
}
}
3.ConfigException.java
自定义异常类用于处理自定义异常(注:其他异常类方法同此)
package my.framework.web;
public class ConfigException extends RuntimeException {
public ConfigException(){
super("程序运行时发生异常!");
}
public ConfigException(String e){
super(e);
}
}
4.web.xml
SpringMvc是基于servlet封装而成的框架,而DispatcherServlet是SpringMvc的访问点,这里的配置就是初始DispatcherServlet的配置。servlet-mapping就是对请求的拦截, /do/*拦截的类型是servlet,也就是说servlet请求都会通过DispatcherServlet去分发
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>myweb</servlet-name>
<servlet-class>my.framework.web.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>myweb</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
</web-app>
5.pom.xml
手写SpringMVC框架所需要的jar包
<dependencies>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
</dependencies>