为什么要写MVC框架?
先看看,没有MVC框架时,是怎么开发的?
当开发一个项目时,面对项目的多个功能模块,可能要 写多个 xxxServlet
例如:
开发注册功能 | registerServlet |
开发 登录功能 | loginServlet |
开发用户信息获取 | userInfoServlet |
这样需要写的类太多了,非常繁琐,且不好维护。
因此,我们可以写出一个 dispatcherServlet 类,这个“分发类”中注册了(包含了)所有的功能方法。前端不同的请求都去找“分发类”,由它进行分发请求和响应,这样类比较少,便于优化。
怎么写MVC框架?
仿SpringMVC的运行原理(含十多个步骤),便于提前建立Spring MVC框架的认知。所以只写关键的部分,其余弱化 。
1.所有请求 为“/*.do”的请求都去找 DispatcherServlet这个类
2.在这个servlet中有相应的初始化方法init(),其中包含如下逻辑
2.1.1
a.加载配置文件
b.建立映射地址的池
即说明 xxx.do和 yyy.do 分别对应的是哪个文件的哪个方法,建立一种映射关系
servlet ——>映射器 ——>调用方法——>将结果返回给用户
映射器专门用于映射用户请求的网址,和具体对应的方法,通过映射器,根据用户输入的网址,找到要调用的方法,然后把方法调用结果返回给用户,或者跳转视图。
2.1.2具体做法
在web.xml中配置映射关系
<servlet>
<servlet-name>⾃定义名称</servlet-name>
<servlet-class>处理请求的类的完整路径</servlet-class>
</servlet>
<servlet-mapping><!-- mapping 表示映射 -->
<servlet-name>⾃定义名称</servlet-name>
<url-pattern>请求名</url-pattern>
</servlet-mapping>
标签执行顺序:
请求到达——web.xml——servlet-mapping标签中的<url-pattern>标签中的内容和请求名进行匹配——匹配成功以后找到对应的<servlet-mapping> 标签 中的二级标签<servlet-name>的servlet名称,去找上一个一级标签<servlet>中和<servlet-name>相匹配的name值——去找servlet标签中 的 servlet-class中处理类的完整路径
举例:
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>com.mayday.mvc.DispatcherServlet</servlet-class>
<init-param>
<param-name>contentConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
当有 任何请求(*.do)过来,都会先解析这个web.xml文件,然后先在servlet-mapping中,根据<url-pattern>和<servlet-name>的映射关系,找到 当前请求 应该 到达的 servlet的名字——即<servlet-name>中的servlet名字。然后去<servlet> 容器中根据名字进行匹配,进一步找到该servlet类所在的项目路径,然后加载初始化参数、配置文件,然后执行。
令 DispatcherServlet 类 继承 Java 的 HttpServlet 方法,并重写 doPost(), doGet(),init()获取配置文件中的每一个用于处理请求的类,每一个类中可能含有0-n个用于处理请求的方法
然后通过类加载器的 init()
@Override
public void init(ServletConfig config) throws ServletException {
//获取 web.xml 配置文件中的 初始化配置文件
String path = config.getInitParameter("contentConfigLocation");
InputStream is = DispatcherServlet.class
.getClassLoader().getResourceAsStream(path);
HandlerMapping.load(is);
}
注意到:class.getClassLoader().getResourceAsStream(path)得到一个输入流 is
然后生成一个配置文件的对象, 把 application.properties 中描述的所有的类 都加载到 ppt 这个 Map集合中,输入流
Properties ppt = new Properties();
ppt.load(is) //加载输入流, 那么 application.properties中所有的类都被加载到ppt这个Map集合中
加载到了这些类以后,通过反射技术去读取 application配置文件 里描述的所有 注解的反射情况
因此 在这之前需要先编写注解
@ResponseBody表示此注解下的方法 应该 返回文字内容给请求者
package com.mayday.mvc;
import java.lang.annotation.*;
@Target(ElementType.METHOD) //元注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
* 注解的作用:
* 被此注解添加的方法, 会被用于处理请求。
* 方法返回的内容,会以文字形式返回到客户端
*/
public @interface ResponseBody {
String value();
}
以下 三个都是 元注解: @Target 作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
@Retention 作用:定义被它所注解的注解保留多久
@Documented 作用:只是用来标注生成 javadoc 的时候是否会被记录
定义 注解 @ResponseView 返回跳转的页面给客户端,
package com.mayday.mvc;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
* 注解的作用:
* 被此注解添加的方法, 会被用于处理请求。
* 方法返回的内容,会直接重定向
*/
public @interface ResponseView {
String value();
}
在使用这两个注解之前,先写一个枚举类,用于描述 响应是 文字 还是 视图:
package com.mayday.mvc;
public enum ResponseType {
TEXT,VIEW;
}
2.2映射器
通过映射器去加载相应的关联的方法
在HandlerMapping中包含 N 个 MVCMapping,存放在 Map类型的集合中,用户每次发起请求,就去HandlerMapping下的 Map集合找,匹配 应该用哪个方法来做处理
在 DispatcherServlet 的 init() 初始化加载(load)时,读取配置文件输入流,把配置文件所指向的类里面 (所有用了 这两个注解的类)封装成 Map集合对象。
先写一个 MVCMapping 类 ,这个类包含的属性有对象 Object ,和方法 Method, 枚举类型 ResponseType
package com.mayday.mvc;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 处理程序映射器(包含了大量的网址与方法的对应关系) 即:一个方法用于处理一个 网址的请求
*/
public class HandlerMapping {
/**
* 映射对象,每一个对象封装了一个方法,用于处理请求
*/
public static class MVCMapping{
private Object obj;
private Method method;
private ResponseType type;
public MVCMapping() {
}
public MVCMapping(Object obj, Method method, ResponseType type) {
this.obj = obj;
this.method = method;
this.type = type;
}
/**..省去 get / set方法..**/
}
}
下面就写加载 输入流 中的类的 load(),其作用是把添加了 @ResponseView和@ResponseBody的方法 封装到 Map集合中
package com.mayday.mvc;
/**
* 处理程序映射器(包含了大量的网址与方法的对应关系) 即:一个方法用于处理一个 网址的请求
*/
public class HandlerMapping {
private static Map<String,MVCMapping> data = new HashMap<>();
public static MVCMapping get(String uri){
return data.get(uri);
} //这里用到的uri是什么意思?uri:统一资源标识符,用于标识某一互联网资源名称的字符串
public static void load(InputStream is ){ //在DispatcherServlet类中已获取了输入流
//加载配置文件
Properties ppt = new Properties();
try {
ppt.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//获取配置文件中描述的一个个的类
Collection<Object> values = ppt.values(); //取出properties中所有的值(类)
for (Object cla:values) {
String className = (String) cla; //拿到对象类的名称后,下面需要对其进行反射的处理,
//通过反射创建对象,进而获取对象类中的方法
//反射:1.动态获取 类的信息(属性,方法),2.动态调用 对象的 方法、属性
try {
//加载配置文件中描述的每一个类
Class c = Class.forName(className);//加载完毕后通过反射加载这个类
//创建这个类的对象
Object obj = c.getConstructor().newInstance();
//获取这个类的所有方法
Method[] methods = c.getMethods();
for (Method m:methods) {
Annotation[] as = m.getAnnotations();
if(as != null){
for(Annotation annotation:as){
if(annotation instanceof ResponseBody){
//说明此方法,用于返回字符串给客户端
MVCMapping mapping = new MVCMapping(obj,m,ResponseType.TEXT);
Object o = data.put(((ResponseBody) annotation).value(),mapping);
if(o != null){
//存在了重复的请求地址
throw new RuntimeException("请求地址重复:"+((ResponseBody) annotation).value());
}
}else if(annotation instanceof ResponseView){
//说明此方法,用于返回界面给客户端
MVCMapping mapping = new MVCMapping(obj,m,ResponseType.VIEW);
Object o = data.put(((ResponseView) annotation).value(),mapping);
if(o != null){
//存在了重复的请求地址
throw new RuntimeException("请求地址重复:"+((ResponseView) annotation).value());
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
当服务器启动时, DispatcherServlet 就被加载了, 就会在 init() 中通过 HandlerMapping的 load()方法 加载配置文件application.properties (注意,需要使用类加载器,将配置文件转化为 输入流 才能进行文件间的交流 )。在load()中,我们读取了这个application.properties配置文件的输入流InputStream is,然后 将输入流保存到配置文件对象中【ppt.load()】,并拿到配置文件中描述的一个个的类,并且遍历,对每一个类使用反射技术 加载 类的信息,创建类对象,获取类的方法,获取方法的注解信息,根据注解类型判断返回 文字还是视图
上面提到,我们自定义的两个注解中, @ResponseBody含有 值: String value();
我们需要获取到这个注解的 value值 ,作为 HashMap的 key, 而 MVCMapping对象的 mapping 作为HashMap的 value
data.put(((ResponseBody) annotation).value(), mapping)
为什么要封装在HashMap中?
是先通过提前初始化 并记录所有的注解和方法的对应关系,(记录到HandlerMapping里面的HashMap)这样便于后续,响应 处理客户端的请求。(否则服务器会处理不了路径请求,报404)
以上是通过 init() 中调用 load() 获取了所有类的方法映射关系,
下面需要写一个方法,对这些映射关系进行匹配(使用uri匹配 )获取(调用)相关方法
public static MVCMapping get(String uri){
return data.get(uri);
}
到这里,我们的MVC框架就快写完了
当用户发出请求, doGet(), doPost() , 为什么能执行,是因为, 当用户请求时,其实Tomcat 主动调用的是Service(),
而Service ()中代码第一行就是获取请求的方式,如果请求是 “GET”,就在Service()中调用 doGet()
如果请求是 "POST", 就在Service()调用 doPost() ,所以这两个方法可直接在 Service()中进行处理:
package com.mayday.mvc;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
public class DispatcherServlet extends javax.servlet.http.HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
//获取 web.xml 配置文件中的 初始化配置文件
String path = config.getInitParameter("contentConfigLocation");
InputStream is = DispatcherServlet.class.getClassLoader().getResourceAsStream(path);
HandlerMapping.load(is);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1. 获取用户请求的uri /xx.do
String uri = req.getRequestURI();
HandlerMapping.MVCMapping mapping = HandlerMapping.get(uri);
if( mapping == null){
resp.sendError(404,"MVC:映射地址不存在:"+uri);
return;
}
Object obj = mapping.getObj();
//反射技术中的 Method类,可通过 Method调用对象的方法:Method.invoke(Object obj1, Object obj2..)
Method method = mapping.getMethod();
Object result = null;
//相当于SpringMVC中的 Handler
try {
//使用反射技术 动态调用 指定obj对象的 方法: mothod.invoke()
result = method.invoke(obj, req, resp);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//判断Mapping的类型是什么,如果是 对应类型,就向前台回复(写入)处理的结果
//相当于 SpringMVC的HandlerAdapter
switch (mapping.getType()){
case TEXT:
resp.getWriter().write((String)result);
break;
case VIEW:
resp.sendRedirect((String)result);
break;
}
}
}
下面再来梳理一下逻辑:
当服务器启动时,DispatcherServlet就会加载 application.properties 配置文件,通过反射技术,根据配置文件中的类的全限定名,找到所有类下的方法,并存储到HashMap中。其中HashMap中的 Key是通过反射技术得到的方法名上 注解的 url 值。value是 反射技术得到的 ——由 Object,和 Method, 枚举类ResponseType组成的——封装对象 MVCMapping。
当用户请求DispatcherServlet的时候,就会通过 req.getRequestURI() 先获取用户请求的 URI。
然后拿着这个URI去 HandlerMapping 的HashMap去找有无对应的可用来处理这个请求的方法
如果没有对应方法,则报404,映射地址不存在。
如果有对应方法,则去调用相应方法。
注意:方法一定是要加入了注解才会在HashMap中的。