一 目的
- 强化所学知识内容(servlet,jsp,filter,aop,ajax,json,反射,注解,... )
- 强化面向对象编程思想
二 MVC
1 什么是MVC
M model 模型层 业务数据处理
V view 视图层 用户界面设计 用户界面数据组装
C controller 控制层 根据请求选择不同的M处理请求,根据处理结果选择不同V组装展示
2 在请求响应过程MVC位置体现
浏览器发送请求到服务器
服务器会将请求交给控制器C
控制器再根据不同的请求,选择对应的模型层M 处理业务请求
模型层业务数据处理完毕后,控制层再根据处理结果,选择指定的视图层V来组装数据,并响应浏览器展示
3 MVC在我们程序中的体现
在我们的程序设计中, Servlet充当控制层C
service+domain+dao 充当模型层M
html,servlet,jsp,freemarker,thymeleaf 充当视图层V
model1 vc (servlet)
model2 c(servlet) v(jsp)
三 请求响应过程图解
1 tomcat管理servlet
- servlet是生命周期托管
- 将servlet对象的创建,调用,删除交给tomcat管理。
- tomcat管理的servlet对象主要有2类
- 自定义Servlet
- tomcat内置的Servlet
- JspServlet --- *.jsp --- 访问jsp网页时,会生成Servlet,再响应处理
- DefaultServlet --- / --- 访问html,css静态网页时,default处理。
2 url-pattern 4种映射配置
<url-pattern>/test.do</url-pattern>匹配一个请求<url-pattern>/*</url-pattern>匹配所有请求<url-pattern>*.jsp</url-pattern>匹配指定后缀的所有请求<url-pattern>/</url-pattern>匹配所有请求
-
注意:浏览器发送的所有请求,在tomcat中都会有对应的servlet,哪怕 html,css,js,jpg等
只不过这些文件资源都是被defaultServlet匹配的。
default主要作用就是用来处理静态资源: 读取文件内容并响应。
3 过滤器中AOP机制的使用
- 过滤器是AOP机制的一种体现
- 面向切面编程
- 要想更好的了解AOP,需要先了解个3个对象
- 起始对象
- 目标对象
- 切面对象
- 过滤器就属于aop机制中的切面对象
- 可以目标之前或之后执行
- 不是本次请求的目标
- 具有一定的公用性
- 例如: 编码字符集设置, 登录认证。
4 图解
四 MVC封装分析与设计
1 请求映射
-
在没有框架的时候,浏览器向服务器发送了n个请求,服务器中就需要有对应的n个controller(servlet)
- 其实每一个请求最终对应的是servlet中的方法
-
有了框架后,希望框架可以实现n个请求对少量controller的n个方法
思考1:如何实现服务器将多个请求交给框架呢?
思考2:框架如何将请求分发给controller的不同方法?
1.1 框架收集请求
-
所谓的收集请求,就是服务器将请求交给框架
-
或者说框架获得服务器请求
-
从技术而言,框架如何来接收服务器的请求呢?
- Servlet springmvc
- Filter struts
/** * 可以收集请求 */ public class DispatcherServlet extends HttpServlet {} -
框架使用者需要在web.xml配置框架可以收集请求
<servlet> <servlet-name>mvc</servlet-name> <servlet-class>com.duyi.mvc.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>mvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> -
思考:url-pattern
-
如果使用/test.do完整匹配。 只能匹配一个请求。 框架需要收集多个请求 NO
-
如果使用/* 可以匹配所有请求,也包括jsp请求,就需要框架来处理jsp
但我们更希望有tomcat/JspServlet来处理 NO
-
如果使用*.do可以匹配所有请求,只需要保证有框架收集的请求都有.do后缀即可
所以后缀匹配可以,而且springmvc框架早期就推荐使用.do后缀
但随着前后端分离技术发展,有一种新的请求规范 restful,新规范不推荐使用后缀 YES(不推荐)
-
现在更推荐使用 / . 但需要考虑到 一旦使用/,tomcat.DefaultServlet就失效了
一旦DefaultServlet失效, 就无法访问静态资源。所以需要框架考虑如何处理静态资源。 YES
-
1.2 框架分发请求
-
所谓请求分发,就是框架收集请求后,根据不同的请求调用controller的不同方法。
-
问题来了:框架怎么知道哪个请求对应的是哪个controller方法呢?
- 需要有一个说明(配置)
-
问题又来了: 这个说明应该是谁来提供的?
- 目前我们的程序有2部分 (自定义部分,引入框架部分)
- 应该由业务程序提供
-
问题继续:使用者随便提供的配置说明框架都认识么?
- 肯定不是
- 使用者应该按照框架的要求提供说明。
-
总结:
- 框架提供配置规则
- 使用者按照规则提供配置
- 框架会按照规则获取配置(就知道哪个请求对应哪个controller方法)
-
框架可以提供两种配置规则 (配置文件 和 配置注解)
配置文件规则
框架要求使用xml配置。
xml格式如下:
<mvc> <mapping name="/test1.do" class="com.TestController" method="t1"> <param-type>int</param-type> <param-type>java.lang.String</param-type> </mapping> <mapping name="/test2.do" class="com.TestController" method="t2"> <param-type>java.lang.String</param-type> <param-type>java.util.Date</param-type> </mapping> <mapping name="/test2.do" class="com.TestController" method="t3" /> </mvc>public class TestController{ public void t1(int i , String s){} public void t2(String s , Date d){} public void t3(){} }
- 注意: 主要考虑方法重载问题
- 需要通过参数列表确定最终的方法(数量,类型)
- 基本类型 直接写名字 int , double
- 引用类型 写类全名 java.lang.string , java.util.Date
- 数组类型
[Ljava.lang.String; -- String[][I -- int[]
配置注解规则
框架提供**@RequestMapping**注解
使用者在controller方法使用这个注解,体现请求映射关系
public class TestController{ @RequestMapping("/test1.do") public void t1(int i , String s){} public void t1(){} @RequestMapping("/test2.do") public void t2(){} }
2 框架调用过程中的AOP
-
有了上述分析,框架就可以收集到请求,并且可以根据使用者提供的配置信息,知道哪个请求对应哪个controller方法
-
按理来说,接下来框架就可以调用对象方法了(反射)
-
这里我们有一个设计 : AOP设计
-
原来在原生的web过程中,存在aop结构 : Filter体现
-
现在将所有的请求都交给框架了,这个Filter的AOP还有效么? 依然有效。
-
尽管如此,框架中依然希望可以提供自己的AOP机制
-
filter虽然可以实现aop,但属于tomcat功能。使用框架时如果还依赖tomcat功能,就会产生功能耦合。所以框架要尽量解耦
-
框架可以对aop设计的颗粒度更细致一些。
例如test1.do和test2.do需要经过aop处理。 test3.do不需要经过aop处理
如果使用过滤器,只能通过*.do将3个请求都收集,然后再分别判断
如果框架内部提供了aop机制,就可以针对于不同的请求来实现aop处理。
-
未来可以使用spring提供的aop机制
-
-
* AOP机制中,有3个重要对象 起始对象, 目标对象 , 切面对象
* 原来起始对象是tomcat
* 目标对象是controller(servlet)
* 切面对象是过滤器
***
* 现在起始对象是框架
* 目标对象是controller
* **切面对象是谁呢 ?**
* 如果使用tomcat的aop机制,按照tomat规则提供切面(filter)
* 如果使用框架的aop机制,安好框架规则提供切面(**拦截器 interceptor**)
* 所以框架应该提供一个拦截器切面的规则(接口)
* 使用者需要利用框架的aop机制时,就自定义实现拦截器接口的切面对象即可。
```java
/**
* 框架:切面规则
*/
public interface Interceptor {
/**
* 会在目标之前执行
* @return true 可以继续向下执行(执行下一个拦截器切面或目标)
*/
public boolean prev(HttpServletRequest request , HttpServletResponse response,Object target);
/**
* 目标之后执行
*/
public void post(HttpServletRequest request , HttpServletResponse response,Object target);
}
```
```java
/**
使用者:自定义拦截器切面
*/
public class MyInterceptor implements Interceptor{
boolean prev(...){
if(){
return true ; //继续向下执行
}else{
return false ;//终止向下执行
}
}
void post(...){}
}
```
* **问题来了:使用者根据规则自定义了拦截器切面后,框架怎么知道有这么一个拦截器切面,框架怎么知道什么时候执行这个拦截器切面?**
拦截器切面的映射
- 这里面的请求,是不同的请求映射对应的拦截器
- 本质就是说明 :哪个请求应该执行哪些拦截器切面
xml配置
<mvc> <mapping /> <!-- 告诉框架,有这么一个拦截器切面 (有aop结构) include="/*" "*.do" "/test1.do,/test2.do,/tet3.do" 指定经过拦截器的请求 exclude="/test3.do,/test4.do" --> <aop-mapping class="com.interceptor.MyInterceptor1" include="/*" exclude="/test1.do" /> <aop-mapping class="com.interceptor.MyInterceptor2" include="/*" exclude="/test2.do,/test3.do" /> </mvc>
注解配置
框架提供一个
@InterceptorAspect注解使用者可以在切面类上使用注解,来指定拦截请求和排除的请求
@InterceptorAspect( include="/*", exclude="/test1.do,/test2.do" ) public class MyInterceptor implements Interceptor{ boolean prev(...){ if(){ return true ; //继续向下执行 }else{ return false ;//终止向下执行 } } void post(...){} }
3 参数处理
-
框架会根据目标映射 和 切面映射。
-
最终就知道哪个请求,应该执行哪些切面,最终执行个目标
-
就可以完成这个调用过程了。
-
一旦调用了目标方法,目标在处理请求时,会需要获得请求传递的相关参数。
-
原来如何获得参数呢?
- request.getParameter("key") , request.getParameterValues("key")
- request.getAttribute("key") 转发携带数据
- session.getAttribute("key")
- request.cookies();--Cookie[]--Cookie{key,value}
- request.getHeader("key");
- 文件上传参数(apache-commons-fileupload)
-
分析:
-
使用原生的方式,需要知道不同参数对应的不同手段
-
有些参数的获得还是比较麻烦的, 如:文件上传。
-
获得的大多数参数,起初都是以String形式存在
-
实际应用中,可能需要将其转换成其他类型 (int , date,double。。。)
-
还有一些逻辑,需要将传递的多个参数,组成一个对象 (add , update)
-
-
现在,这个请求先经过了框架,就考虑让框架负责 接收参数,类型转换,数据组装。
-
问题来了:框架怎么知道需要哪些参数? 框架怎么知道转换哪些类型?怎么知道组装什么对象?
- 需要使用者告诉框架 (传参 , 配置[文件,注解] )
参数映射配置规则
- 框架给使用者提供了参数配置规则
基于xml文件
在请求映射时,就可以根据目标方法的参数列表
可知:此次请求需要几个参数,需要转换成什么类型的参数
但不知道需要哪两个名字的参数,可以为
<param-type>标签增加name属性,指定参数名如果,参数类型不是一个简单的类型,就可以理解成,需要将多个参数组成对象
简单类型:基本类型 + string
组成对象:此时参数列表只有一个参数(Car,domain),需要将哪些请求参数组成对象呢?
还需要知道那些名字的参数组成这个对象。
不需要额外的配置,可以基于Car的属性名,获得同名的参数。
<mvc> <mapping name="/test1.do" class="com.TestController" method="t1"> <param-type name="age" type="cookie">int</param-type> <param-type name="sex" type="header">java.lang.String</param-type> </mapping> <mapping name="/test1.do" class="com.TestController" method="t1"> <param-type name="user">com.domain.User</param-type> </mapping> </mvc>
基于注解
同样, 目标方法的参数列表,对应着此次请求的参数
- 参数列表有2个参数,就表示需要2个请求参数
- 参数列表有一个int参数和一个string参数,就表示需要将请求参数一个转换成int,一个转换成string
接下来还差一个信息,就是参数名称
方法的参数列表,直接就有名字。 那能不能使用参数列表名字作为请求参数的key呢
理论上是可以的,但有不足之处,所以不推荐。
- jdk1.8之后,才可以利用反射获得参数名,而且还需要额外的启动配置
由框架提供一个注解
@RequestParam,使用者利用该注解来指定参数名注意:如果是一个domain类型,表示多个参数组成对象,参数名与属性名一致。
所以此时不需要使用@RequestParam注解来匹配参数名
@RequestMapping("/test1.do") public void t1(@RequestParam("age")int i , @RequestParam("sex")String s){} @RequestMapping("/test2.do") public void t1(@Cookie("age")int i , @RequestHeader("sex")String s){} @RequestMapping("/testUser.do") public void t1(User user){}
- 扩展:目前提供的参数映射,都是既有request的参数。 但还有cookie,header 。
- xml配置,为
<param-type>增加一个type属性指定参数种类 : request,cookie,header - 注解配置,提供@RequestHader , @Cookie
- xml配置,为
4 请求处理(非框架处理)
5 响应处理
-
浏览器发送给服务器的请求,服务器先交给了框架
-
框架根据目标映射和aop映射,完成整个的调用流程
-
并且在最终调用目标时,会根据目标的参数列表传递对应的请求参数
-
接下来业务程序员就可以实现业务处理
-
处理完毕后,需要根据处理结果给与浏览器响应
-
原生响应处理
- 间接
- 转发
request.getRequestDispatcher(url).forward() - 重定向
response.sendRedirect(url)
- 转发
- 直接
response.getWriter().write(str)
- 间接
-
分析:每次响应使用的方法基本一致,只是对应的url和str不同。
-
所以可以考虑将响应的功能代码实现交给框架,使用者只需要为框架提供每次响应的url或str即可
-
框架指定了如下规则
转发规则
目标方法直接返回一个字符串,表示转发的url
public String t1(){ //框架:request.getRequestDispatcher("1.jsp").forward(req,resp); return "1.jsp" ; }有一个问题:转发时可以携带数据
- 框架提供一个ModelAndView类,使用者可以使用该对象提供转发的url和数据
- 最终在方法中直接返回这个ModelAndView对象
public ModelAndView t1(){ ModelAndView mv = new ModelAndView(); mv.setViewName("1.jsp"); mv.addAttribute("name","dmc"); mv.addAttribute("age",18); //框架: request.getRequestDispatcher("1.jsp").forward(req,resp) // request.setAttribute("name","dmc"); return mv ; }
重定向规则
目标方法返回一个字符串,作为重定向的url
- 相比于转发时的处理,需要提供一个标识,区分转发还是重定向。
- 标识形式有很多种,这里面我们参考未来的springmvc:返回值增加一个
redirect:前缀
- 其他方式一:提供一个@Redirect注解
- 其他方式二:返回RedirectModel对象
public String t1(){ //框架:response.sendRedirect("1.jsp"); return "redirect:1.jsp" ; }
直接响应规则
目标方法直接返回响应内容的字符串即可。
- 相比于间接响应,需要提供一个标识,表示此次需要直接响应。
- 参考未来的springmvc,由框架提供@ResponseBody注解,使用者使用该注解表示要直接响应内容
@ResponseBody public String t1(){ //框架:response.getWriter().write("1.jsp"); return "1.jsp" ; }扩展情况
- 有时,需要将一个对象或集合或数组内容直接响应给浏览器 (前后端分离场景)
- 原来需要将这些对象/集合,利用json工具转换成json格式的字符串,然后再响应。
- 我们分析认为,框架也可以使用json工具,实现json转换
- 所以,直接响应时,可以返回对象/集合。 框架底层会将其转换成json再响应
- 常用json工具:
- json-lib
- jackson
- fastjson
- gson
@ResponseBody public List<User> t1(){ //框架: Sring json = JSON.formatObject(users); // resp.getWriter().write(json); return users ; }
五 MVC封装编码实现
1 构建测试项目
1.1 框架项目 test-framework
- 编写框架代码
1.2 案例项目 test-web
- 编写业务代码
- 需要依赖于框架代码 (将框架引入项目)
1.3 将框架项目生成jar文件
1.4 在web用例中引入jar文件
- 原来将jar复制到web/web-inf/lib里,右键-add as lib ... (相对路径引入)
-
此次项目中不推荐这么做: 原因是,框架代码需要频繁修改和测试。
- 如果使用相对路径,每次修改,都要重写生成jar,重新将其复制到相对路径中
- 可以使用绝对路径引入,未来jar文件重新生成, 立刻生效
- web案例所需依赖
- 正常来讲, web案例需要使用框架。 我们已经引入了框架。
- 但还需要引入其他依赖。 因为刚刚引入的框架需要其他依赖。
- 目前需要servlet-api
- 未来还需要xml解析相关的jar
- 未来还需要文件上传处理的jar
- 未来还需要json处理的jar
2 核心入口创建与配置
-
框架中提供一个Servlet作为核心入口,来收集所有需要框架处理的请求
DispatcherServlet -
在web案例中,配置web.xml,指定需要经由框架的请求
- 理论上 / 和 *.do都可以。甚至/ *也可以,具体看需求
- 现在更推荐使用 /
-
servlet.service测试请求收集
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(req.getRequestURI()); }
3 配置信息的读取与存储
- 配置信息只需要在程序启动时,读取一次就可以了。
- 可以使用 静态代码段 或 servlet.init()
3.1 读取配置文件
-
问题来了:框架在init方法中,怎么知道读取的是哪个配置文件呢?
-
方式一: 可以框架直接固定配置文件的位置和名称。
@Override public void init() throws ServletException { new File("f:/z/mvc.xml"); } -
方式二: web.xml来传递配置文件的位置。(我们选择)
<servlet> <servlet-name>mvc</servlet-name> <servlet-class>com.duyi.mvc.DispatcherServlet</servlet-class> <init-param> <param-name>configLocation</param-name> <param-value>classpath:mvc.xml</param-value> <param-value>f:/z/mvc.xml</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet>- 注意: 这个参数信息是由使用者提供的,但使用者为什么这么写,是框架提供的规则。
- 有
classpath:前缀 说明配置文件在src目录中 - 没有前缀,默认就是从磁盘目录读取。
- 有
- 注意: 这个参数信息是由使用者提供的,但使用者为什么这么写,是框架提供的规则。
-
-
注意:读取xml时需要引入2个jar文件
- 在框架项目中需要引入,用来编写框架
- 在测试项目中需要引入,用来使用框架
3.1.1 框架提供Configuration对象
-
专门用来读取和存储配置信息的对象。
/** * 读取, 存储,获得配置信息 */ public class Configuration { public void readXml(){} public void readAnnotation(){} }
3.1.2 框架提供配置信息读取器
-
框架有多种配置信息读取来源
- xml
- classpath ,
- os
- remote
- 注解
- xml
-
将每一个读取操作都提供一个对应的读取器
-
注意1:这里面有3个和xml文件相关的读取器
- 不同的读取器是xml内容的读取来源不同
- 一定读取到配置信息,对配置信息的处理,都是一致的。
- 所以框架为xml读取器提供一个公共个父类,实现公共的读取处理
- 父类在读取信息时需要将信息存储在统一的configuration中(见注意2)
-
注意2:所有的读取器在读取后都需要将配置信息存储在统一的configuration中
- 这就要求所有的读取器提供一个构造器,来接收统一的configuration
3.1.3 框架提供标签实体
-
标签实体用来存储标签对象。
-
目前有3个标签,其关系如下
- mapping与ao-pmapping并列
- mapping与param-type包含
<mapping> <param-type></param-type> </mapping> <aop-mapping></aop-mapping>
-
注意1:
-
mapping和aop-mappping标签中都有class属性,
-
最终使用的一定是其对应的Class对象。
-
所以在赋值class时,就直接反射获得对象
-
-
注意2:
-
mapping标签中有一个method属性,表示目标方法
-
最终会通过反射获得对应的Method对象,并完成调用
-
所以在读取标签时,就根据信息直接获得其对应的Method对象
- 同时需要根据param-type信息,获得对应参数的Class类型
-
-
注意3:
-
切面信息中的include和exclude都可以包含多个请求
-
配置时使用逗号分隔
-
存储时将其以set集合存储
-
3.2 读取配置注解
-
先知道,如何使用注解实现 目标映射 和 切面映射
-
目标映射 : 使用@RequestMapping注解 (框架提供, 使用者应用)
使用@RequestParam注解匹配请求参数
-
切面映射: 使用@InterceptorAspect注解 (框架提供,使用者应用)
切面类需要实现框架提供的Interceptor接口
-
-
利用注解读取配置信息,本质就是使用反射技术
- 反射需要先获得类, 就可以获得方法,就可以获得方法注解。
-
问题来了:框架怎么知道应该通过反射技术,解析哪些类呢?
-
可以通过web.xml的初始化配置,来指定需要反射解析的类
-
如:
<servlet> <servlet-name>mvc</servlet-name> <servlet-class>com.duyi.mvc.DispatcherServlet</servlet-class> <init-param> <param-name>configLocation</param-name> <param-value>classpath:mvc.xml</param-value> </init-param> <init-param> <param-name>configClass</param-name> <param-value> com.controller.TestController1, com.controller.TestController2, com.interceptor.MyInterceptor1, com.interceptor.MyInterceptor2, </param-value> </init-param> <load-on-startup>-1</load-on-startup> </servlet> -
但这样不好, 需要指定很多的类。
-
所以我们还有另一种手段, 可以指定类所在的包。 然后进行包扫描。
- 表示对指定包中的类进行反射解析,获得配置信息
- 同时会对包中子包的类也进行扫描
-
如:
<servlet> <servlet-name>mvc</servlet-name> <servlet-class>com.duyi.mvc.DispatcherServlet</servlet-class> <init-param> <param-name>configLocation</param-name> <param-value>classpath:mvc.xml</param-value> </init-param> <init-param> <param-name>configPackage</param-name> <param-value> com.controller, com.interceptor </param-value> </init-param> <load-on-startup>-1</load-on-startup> </servlet>- 注意:configPackage以及对应的配置内容,都是框架提供的规则。
-
-
下一个问题:如何进行包扫描
- 反射无法通过包路径获得包对象,也无法通过包对象直接反射获得包中所有的类
- 所以无法使用技术直接达到效果。
- 曲线救国:
- 先根据包路径
- 获得对应的文件夹路径
- 再利用File技术获得文件夹中所有的类文件
- 类文件的名字就是类名
- 和包路径就可以组合成完整的类路径了。
3.2.1 【扩展】输出带颜色的字
- 打印时,连接"\33[?m"字符串
- ?位置可以是30-39之间的数字,表示不同颜色(自己尝试)
- 设置的后面所有输出颜色,不一定是当前输出
- 所以输出完毕后还需要还原初始颜色
public static void main(String[] args) {
System.out.println("11111");
// \33[36m 设置颜色
// \33[m 还原初始颜色
System.out.println("\33[36m22222\33[m");
System.out.println("33333333");
}
3.2.2 框架提供@Controller注解
-
在利用反射+注解读取配置信息时
-
目前我们有两中类需要读取, controller, interceptor
-
其中interceptor可以通过@InterceptorAspect注解 或 Interceptor接口,快取确定是一个interceptor
-
但controller没有确定标识
- 也就是说,如果不是interceptor,无法确定就是controller
- 就可能出现没必要的扫描
-
为了提供扫描效率, 专门为controller提供一个@Controller注解。 遇见@Controller注解,再扫描
private void readClass(String classname){ Class<?> c = Class.forName(classname); //目前有两种类需要解析 controller , interceptor Annotation annotation; if((annotation= c.getAnnotation(Controller.class)) != null){ //说明是一个controller类 readController(c); }else if((annotation = c.getAnnotation(InterceptorAspect.class)) != null){ //说明是一个interceptor类 readInterceptor(c); }else { //上述情况都不是,表示是一个不需要扫描处理的类,略过即可。 } }
4 调用过程实现
- 框架根据收集的请求,调用目标方法,在这个过程中,还需要执行切面,还需要参数处理
4.1 准备装载切面和目标
-
无论此次请求执行的是哪个目标,哪些切面,都需要先找到这些对象
-
找的时候,找到一个应该装载一个,而不是执行一个
-
等所有的对象都装载完毕,再执行。
-
所以,框架提供一个 ExecutorProxy来装载并执行对象
-
装载的切面和目标都来源于请求
-
需要在DispatcherServlet的方法中获得此次请求
// http://localhost:8080/web/user/add.do req.getRequestURL() ; // /web/user/add.do req.getRequestURI() ; // /web req.getContextPath() ; // /user/add.do req.getServletPath() ; -
请求所对应的切面和目标,都在配置中(configuration)
-
直接根据请求,从configuration中直接获得装载后的ExecutorProxy即可。
4.2 装载目标
-
根据请求name,找到与之匹配的目标信息(MappingTag)
MappingTag mappingTag = mappingTags.get(name); //根据这个请求name,一定能找到对应的mappingTag么? 不一定。 if(mappingTags == null) { return null ; } ExecutorProxy proxy = new ExecutorProxy() ; proxy.setTarget(mappingTag);
4.3 装载切面
-
遍历所有的切面,找到支持当前请求的切面信息 。 基于include 和 exclude
- 需要先确保include包含,再确保exclude不包含。
-
注意:include和exclude的通配符处理
/*所有请求/user/*指定路径下的所有请求*.do含后缀的所有请求/test.do具体请求
4.4 空映射处理(静态资源访问)
-
有些请求,框架无法找到与之对应的目标
-
服务器的资源主要分两类
- 操作资源: 对象方法
- 文件资源: html , css ,js,jpg 。 jsp
-
现在服务器的请求除了jsp以外,都交给框架了
-
框架需要区分操作资源和文件资源。
- 框架根据请求 + 配置信息,能找到的就是属于操作资源
- 没找到操作资源,就认为是文件资源。
-
文件资源框架直接交给tomcat的default对象处理
String name = req.getServletPath(); ExecutorProxy proxy = config.getExecuteProxy(name); if(proxy == null){ //根据请求没有找到操作资源 //说明应该请求的是一个文件中资源 //文件资源的处理,其实就是IO操作,我们可以自己做,但完全没必要 //因为tomcat提供了可以请求文件资源的对象 defaultServlet //框架准备将文件资源处理,继续交给tomcat来实现 req.getServletContext().getNamedDispatcher("default").forward(req,resp); }else{ //目标存储,是一个操作资源的请求 //就可以按照装载好切面和目标,调用执行了。 }
4.5 切面执行
-
切面是AOP结构中的一部分。 作用是在目标之前或执行执行一些附加功能。
-
这里我们有2种实现方式
-
第一种: 类似于Filter的实现方式 , 只有一个方法。
class MyInterceptor{ doIntercept(chain){ //do before ... //回调链, 让链来调用下一个对象(切面,目标) chain.next(); //do after ... } } -
第二种:一个切面提供2个方法 (prev , post)。
- 在调用目标之前,先执行prev , 在调用目标之后再执行 post
- 未来springmvc框架就是使用的这种方式。
class MyInterceptor{ prev(){} post(){} }
-
-
需要思考整个的执行过程
-
Proxy{切面1,切面2,切面3,目标}
-
正确
切面1 prev 切面2 prev 切面3 prev 目标 切面3 post 切面2 post 切面1 post -
中途出现异常或判断未通过(切面3终止)
- 切面一旦终止,就不会再继续向下执行目标了,同时切面的post后置操作也不会执行了。
- 但切面1和切面2一旦通过了前置操作,说明切面1和切面2是没有问题的
- 所以前置通过了,后置也必须执行。
切面1 prev 切面2 prev 切面3 prev 终止 切面2 post 切面1 post
-
-
注意1:
-
目前我们存储的是拦截器信息,不是拦截器对象
-
所以执行拦截器时,需要产生对象。
-
因为要在目标前和后分别执行拦截器,但拦截器对象一定不能产生2次。如何处理?
-
先考虑,拦截器对象是否全局单例? 。 如果需要全局单例,是真单例还是伪单例?
- 真单例:单实例管理,就可以在加载类信息时创建对象(切面,目标)
伪单例: 可以通过配置,实现单例或多例的管理如果- xml配置,为切面和目标增加一个scope属性 (singleton单例,prototype 多例)
- 注解配置,需要提供一个额外的@Scope注解
-
如果不是全局单例,如何保证 前后执行的是同一个实例。
-
-
-
注意2:
- 在执行拦截器时,每一个拦截器都可能需要request和response对象
- 所以可以将request和response交给Proxy
4.6 目标执行
-
调用目标,就是执行controller.method
-
controller信息和method信息,都在mappingTag(target)
-
只需要反射就可以实现调用。(反射new对象,反射调用方法)
-
但我们之前分析了: 调用方法时需要为方法传递对应的参数。
- 目标方法不再是无参方法,是有可能带参数列表
- 为什么会有参数列表? 参数列表 对应着请求参数
-
在目标方法中执行请求时,原来需要利用request获得参数,现在既然请求经过框架,我们就让框架帮我们获得参数,获得哪几个参数?都叫什么名字?需要转换成哪些类型? 都通过参数列表体现
-
所以执行目标前,需要先为目标方法的参数列表,绑定对应的请求参数数据
-
在绑定数据前,需要先分析都有哪些请求参数,主要就两种
- 普通请求参数 : 字符串, 使用request.getParameter()
- 文件上传请求参数: 字符串,文件 , 使用文件上传组件 apache-common-fileupload
- 其他
-
所以在绑定数据前,需要先收集这些请求参数
4.6.1 收集参数
-
主要就两种,对于使用者而言,他只需要按照逻辑获得所需要的数据即可,不需要知道数据到底是普通请求传来的,还是文件上传请求传来的。
-
在框架内部需要分别获得对应的参数数据
-
注意1:
- 由于文件参数信息比较多,所以需要组成对象MvcFile
-
注意2:
- 由于可能传递多个同名参数,所以最终以数组的形式存储参数
Map<String,String[]>存储字符串参数Map<String,MvcFile[]>存储文件参数
4.6.2 参数绑定分析
-
正常执行的过程 是框架根据请求,最终调用controller.method
-
controller方法,有参数列表
-
所以反射调用方法时,需要传递参数列表对应的参数值
public void t1(@RequestParam("age")int age , String name){} //--------------- method.invoke(controller,[10,"dmc"]) -
controller.method的参数列表作用是什么?
- 用来声明(告诉框架),此次请求,需要获得2个参数,分别是int和string类型的参数,对应的名字...
-
controller.method参数列表对应的参数值是什么?
- 首先参数值一定来自于框架,因为是框架最终通过反射调用了method方法。
- 从逻辑而言,参数值应该是请求携带的参数 paramValues(String) , fileParamValues(MvcFile)
-
什么是参数绑定?
- 根据method的参数列表,找到与之对应的参数值,形成最终的数组。
-
扩展:
-
controller.method在向框架声明参数时,还可以声明Servlet相关类型参数:req,resp,session等
-
有了框架后,许多request和response相关的操作,都可以有框架实现了。
-
使用者就可以不用这些对象。
-
但某些特殊的情况下,使用者可能依然需要使用这些对象,就可以通过参数列表向框架要。
-
-
综上,框架绑定参数目前有这样的3种情况
//参数列表中的一个参数,对应请求的一个参数值 1:1 public void t1(@RequestParam("uname")String uname){} //参数列表中的一个参数,对应请求的多个参数值 1:n //user{uno,uname,upass,age,sex} public void t1(User user){} //参数列表中的一个参数,对应一个servlet相关对象 (req,resp,session) public void t1(HttpServletRequest req); -
注意:
-
虽然目前有3种参数绑定形式
-
但未来框架内部还有可能会出现第4种,第5种,甚至是使用者希望自定义参数绑定。
//希望框架可以将所有的请求参数,绑定在map中 public void t1(Map map){} //希望框架可以将session中uname的值,绑定在这个变量。 public void t2(@SessionAttr("uname")String uname){} -
此时不应该通过多重的if-else判断,来确定最终的绑定形式
- 问题1: 框架底层增加新的绑定方式时,每次都要修改功能代码
- 问题2: 使用者无法增加自定义绑定方式
private Object[] bindParamDataForTargetMethod(){ if(参数1){ }else if(参数2){ }else{ } } -
可以使用策略模式,并对外提供配置接口
-
4.6.3 参数绑定设计
-
将每一种绑定策略,解耦.
-
提供一个策略接口,确保所有的绑定器,都符合这个策略(框架内置,使用者自定义)
- 需要提供一个bind绑定方法
- 所谓的绑定,就是根据参数列表,找到对应的参数数据
- 参数列表:每一个parameter对象
- 参数数据源: paramValues(strings) , fileParamValues(files) , servletValues(req,resp,session)
- 注意:参数数据源确实存在,但都是零散的
- 将所有的参数数据源综合管理 ParameterSource
/** * 参数绑定策略接口。 */ public interface ParameterBindStrategy { /** * 从paramSource中获得parameter这个参数所需要的参数值,目前还有3种<br/> * 1. 1:1 请求参数 <br/> * 2. 1:n 请求参数 <br/> * 3. servlet相关对象参数 <br/> * @param parameter * @param paramSource * @return */ Object bind(Parameter parameter, ParameterSource paramSource); /** * 判断当前这个绑定器是否只支持当前这个参数<br/> * 使用这个绑定策略,能否为当前参数找到对应的参数值 * @return */ boolean isSupport(Parameter parameter); } -
框架内置3个绑定器

-
除了框架内置的绑定器,使用者还可以自定绑定器
-
使用者自定义绑定器,实现绑定策略接口
-
框架提供配置规则, 使用者将自定义参数绑定器配置(告诉)给框架
xml配置
<mvc> other settings <!-- 告诉框架, 我这个程序中为框架提供了参数绑定器 --> <param-binder class="com.util.MapParameterBinder" /> </mvc>注解配置
框架提供一个@ParameterBinder,使用者将其作用在自定义的参数绑定器上
@ParameterBinder public class MapParameterBinder implements ParameterBindStrategy {}
-
4.6.4 参数绑定器的使用(框架)
-
框架内置了一些绑定器, 使用者通过配置提供了一些绑定器 (以类的形式存在)
-
使用时使用的是对象,多次使用,同一个对象即可。
-
所以应该提前创建对象(配置信息加载完毕时)。
-
在绑定数据时, 遍历所有的参数列表
-
基于每一个参数,找到可以对其绑定的绑定器
-
会遍历所有的绑定器,看看哪一个绑定器支持当前的参数特点。
4.6.5 参数绑定器实现
1 normal绑定器
- 支持使用@RequestParam注解声明的列表参数
- 一个列表参数对应一个参数值
- 获取参数值,有可能是文件参数,也可能是非文件参数
- 文件参数,起初获得是MvcFile[] 类型的值
- 非文件参数,起初获得的是String[]类型的值
- 所以接下来还需要根据具体的参数类型,对获得的值进行一个类型转换处理。(待续。。。)
@Override public boolean isSupport(Parameter parameter) { return parameter.getAnnotation(RequestParam.class) != null; }
2 servlet绑定器
- 只针对 request,response,session三种类型进行绑定
- 理论上是不需要@RequestParam注解,直接通过参数类型判断是否支持。
public boolean isSupport(Parameter parameter) { Class<?> type = parameter.getType(); return type == HttpServletRequest.class || type == HttpServletResponse.class || type == HttpSession.class; }
3 实体绑定器
- 组装绑定器时,将其放在了list集合的最后一个
- 按照逻辑, 其他的绑定器如果都不支持参数,我们就认为这应该是一个实体绑定操作
- 既然放在了最后一个,到这里就一定要执行。
- 绑定过程:就是通过反射获得对象的属性,以属性名为key,获得对应的参数值。
@Override public boolean isSupport(Parameter parameter) { return true; }
4 自定义Map参数绑定器
- 支持Map类型和HashMap类型
- 绑定时自定义规定, 将每一个请求参数的第一个值装入自定义 map
@Override public boolean isSupport(Parameter parameter) { Class<?> type = parameter.getType(); return type== Map.class || type == HashMap.class; }
4.6.6 类型转换器设计与实现
-
目前,在普通参数绑定时 和 实体参数绑定时
-
都需要获得一个请求参数。
-
这个请求参数,起初是String[] 或 MvcFile[]类型
-
接下来需要将其转换成 目标方法列表参数类型 或 实体属性类型。
-
大约包括以下情况:
MvcFile[] -- MvcFile[] -- MvcFile (MvcFile[0]) String[] -- String[] -- String (String[0]) -- int Integer (Integer.parseInt(String[0])) -- double Double (Double.parseDouble(String[0])) -- long Long -- int[] -- double[] -- long[] -- Integer[] -- Double[] -- Date (DateFormat(yyyy-MM-dd)(yyyy/MM/dd)) (自定义) -
如何确定具体的转换过程
- 通过 左侧类型 + 右侧类型确定
-
有多种转换方式,不推荐使用if-else组合
- 问题一:每增加一种转换过程,就需要增加一组if-else,就需要修改源码
- 问题二:不利于用户自定义
-
我们采用类似于参数绑定器的方式来实现
- 框架提供转换器规范(接口)
- 框架内部提供一部分转换器。
- 用户可以自定义转换。
- 自定义类型转换器,实现接口
- 配置类型转换器(告诉框架,使用者提供了一个自定义类型转换器)
4.6.7 使用类型转换器
- 读取配置信息时,读取到使用者自定义类型转换器信息
- 配置信息读取完毕后,初始化准备所有的类型转换器
- 使用时,根据数据的类型 和 目标类型 组成类型转换器的key,获得对应的类型转换器
- 注意: 基本类型的处理。需要将基本类型变成对应的包装类获得key
4.6.8 调用方法
- 通过参数绑定器,可以将方法参数列表中所有的参数,都实现数据绑定
- 最终会绑定在一个数组中
- 利用反射调用方法即可。
Object controller = configuration.getSingleObject(target.getMappingClass());
target.getMethod().invoke(controller,values);
5 响应过程实现
- 按照之前的分析
- 在controller.method中,只需要返回要响应的内容即可。
- 转发
- 重定向
- 直接响应
- 很明显,框架需要根据不同的返回情况,选择不同的响应处理
- 我们依然不是用if-else系列,而是继续使用策略来实现。也支持使用者自定响应策略
5.1 响应处理器设计
-
框架提供一个响应规则
- 由于不同的响应情况,格式各不相同
- 所以无法产生一个统一格式的key,来快速获取对应的响应处理器
- 所以需要类似于参数绑定器,提供一个isSupport方法,来指定当前相应处理器支持的特点。
- 返回值有判断 ,是否有redirect前缀
- 返回类型有判断 , String , ModelAndView
- 方法的注解有判断,是否有ResponseBody
/** * 响应处理策略接口 */ public interface ResponseHandleStrategy { void handle(Object result , Method targetMethod, HttpServletRequest request , HttpServletResponse response); boolean isSupport(Object result , Method targetMethod); } -
框架提供一些内置的响应策略
- 转发
- 转发携带数据
- 重定向
- 直接响应string
- 直接响应对象集合(内部json处理)
-
支持使用者提供自定义响应策略
- 自己实现....
5.2 使用响应处理器
-
框架器时,先读取使用者自定义响应器配置
-
初始化响应器
-
目标方法执行完毕后,根据响应结果和目标方法,遍历找到支持此次结果的响应处理器,实现响应
5.3 响应处理器实现
1 字符串转发
不携带数据
支持条件: 返回字符串,没有@ResponseBody直接,字符串内容没有redirect前缀
未来随着响应的情况越来越多, 这里面需要排除就越来越多。
所以有一个小技巧(有一定局限性),通过控制响应处理器顺序,简化判断条件
比如: 将直接响应的处理器放在上面,优先判断
一旦开始检测重定向处理器时,就不用在判断是否有@ResponseBody注解了
因为有这个注解,早在前面的直接响应处理器验证时就已经执行了。
既然执行到了当前位置的重定向处理器,那就说明一定么有@ResponseBody注解
@Override public boolean isSupport(Object result, Method targetMethod) { // return result instanceof String && // targetMethod.getAnnotation(ResponseBody.class) == null && // !((String) result).startsWith("redirect:"); //有没有前缀, 有么有@responsebody都通过响应处理器顺序来控制。 return result instanceof String ; }
2 ModelAndView转发
转发携带数据
框架提供ModelAndView类型
支持条件:返回类型是ModelAndView
将mv对象所有的属性,装入request
request.setAttribute(key,value)根据mv中的viewName实现转发
3 重定向
- 根据响应处理器的加载顺序,不需要判断@Responsebody注解
- 支持条件: 是不是有
redirect:前缀的字符串
4 直接响应字符串
- 支持条件: 方法有@ResponseBody注解,返回值类型是String
5 直接响应对象集合
- 此时需要将对象或集合转换成json格式的字符串,再直接响应
- 需要引入json工具jar文件
- 框架代码要引入。 才能编码
- 应用程序要引入,才能使用框架。
- json-lib, fastjson ....
- 支持条件: 只需要检测是否有@ResponseBody注解即可。
- 因为相应处理器加载顺序,是的字符串的直接响应最先加载
- 所以检测当前响应处理器时,表示不是字符串,还需要直接响应。 自然就需要json处理。