封装属于自己的SpringMVC | 青训营

170 阅读33分钟

一 目的

  • 强化所学知识内容(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类
    1. 自定义Servlet
    2. tomcat内置的Servlet
      • JspServlet --- *.jsp --- 访问jsp网页时,会生成Servlet,再响应处理
      • DefaultServlet --- / --- 访问html,css静态网页时,default处理。

2 url-pattern 4种映射配置

  1. <url-pattern>/test.do</url-pattern> 匹配一个请求
  2. <url-pattern>/*</url-pattern> 匹配所有请求
  3. <url-pattern>*.jsp</url-pattern> 匹配指定后缀的所有请求
  4. <url-pattern>/</url-pattern> 匹配所有请求
  • 注意:浏览器发送的所有请求,在tomcat中都会有对应的servlet,哪怕 html,css,js,jpg等

    ​ 只不过这些文件资源都是被defaultServlet匹配的。

    ​ default主要作用就是用来处理静态资源: 读取文件内容并响应。

3 过滤器中AOP机制的使用

  • 过滤器是AOP机制的一种体现
    • 面向切面编程
  • 要想更好的了解AOP,需要先了解个3个对象
    • 起始对象
    • 目标对象
    • 切面对象
  • 过滤器就属于aop机制中的切面对象
    • 可以目标之前或之后执行
    • 不是本次请求的目标
    • 具有一定的公用性
    • 例如: 编码字符集设置, 登录认证。

4 图解

01.png

四 MVC封装分析与设计

1 请求映射

  • 在没有框架的时候,浏览器向服务器发送了n个请求,服务器中就需要有对应的n个controller(servlet)

    • 其实每一个请求最终对应的是servlet中的方法
  • 有了框架后,希望框架可以实现n个请求对少量controller的n个方法

02.png

思考1:如何实现服务器将多个请求交给框架呢?

思考2:框架如何将请求分发给controller的不同方法?

1.1 框架收集请求

  • 所谓的收集请求,就是服务器将请求交给框架

  • 或者说框架获得服务器请求

  • 从技术而言,框架如何来接收服务器的请求呢?

    1. Servlet springmvc
    2. 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机制

      1. filter虽然可以实现aop,但属于tomcat功能。使用框架时如果还依赖tomcat功能,就会产生功能耦合。所以框架要尽量解耦

      2. 框架可以对aop设计的颗粒度更细致一些。

        例如test1.do和test2.do需要经过aop处理。 test3.do不需要经过aop处理

        如果使用过滤器,只能通过*.do将3个请求都收集,然后再分别判断

        如果框架内部提供了aop机制,就可以针对于不同的请求来实现aop处理。

      3. 未来可以使用spring提供的aop机制

03.png

*   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

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文件

04.png

06.png

07.png

1.4 在web用例中引入jar文件

  • 原来将jar复制到web/web-inf/lib里,右键-add as lib ... (相对路径引入)

08.png

  • 此次项目中不推荐这么做: 原因是,框架代码需要频繁修改和测试。

    • 如果使用相对路径,每次修改,都要重写生成jar,重新将其复制到相对路径中
    • 可以使用绝对路径引入,未来jar文件重新生成, 立刻生效

09.png

  • 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
    • 注解
  • 将每一个读取操作都提供一个对应的读取器

10.png

  • 注意1:这里面有3个和xml文件相关的读取器

    • 不同的读取器是xml内容的读取来源不同
    • 一定读取到配置信息,对配置信息的处理,都是一致的。
    • 所以框架为xml读取器提供一个公共个父类,实现公共的读取处理
      • 父类在读取信息时需要将信息存储在统一的configuration中(见注意2)

11.png

18.png

  • 注意2:所有的读取器在读取后都需要将配置信息存储在统一的configuration中

    • 这就要求所有的读取器提供一个构造器,来接收统一的configuration

13.png

12.png

3.1.3 框架提供标签实体

  • 标签实体用来存储标签对象。

  • 目前有3个标签,其关系如下

    • mapping与ao-pmapping并列
    • mapping与param-type包含
    <mapping>
    	<param-type></param-type>
    </mapping>
    <aop-mapping></aop-mapping>
    

14.png

  • 注意1:

    • mapping和aop-mappping标签中都有class属性,

    • 最终使用的一定是其对应的Class对象。

    • 所以在赋值class时,就直接反射获得对象

15.png

  • 注意2:

    • mapping标签中有一个method属性,表示目标方法

    • 最终会通过反射获得对应的Method对象,并完成调用

    • 所以在读取标签时,就根据信息直接获得其对应的Method对象

      • 同时需要根据param-type信息,获得对应参数的Class类型

17.png

16.png

  • 注意3:

    • 切面信息中的include和exclude都可以包含多个请求

    • 配置时使用逗号分隔

    • 存储时将其以set集合存储

19.png

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获得参数,现在既然请求经过框架,我们就让框架帮我们获得参数,获得哪几个参数?都叫什么名字?需要转换成哪些类型? 都通过参数列表体现

  • 所以执行目标前,需要先为目标方法的参数列表,绑定对应的请求参数数据

  • 在绑定数据前,需要先分析都有哪些请求参数,主要就两种

    1. 普通请求参数 : 字符串, 使用request.getParameter()
    2. 文件上传请求参数: 字符串,文件 , 使用文件上传组件 apache-common-fileupload
    3. 其他
  • 所以在绑定数据前,需要先收集这些请求参数

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个绑定器

    1669973963833转存失败,建议直接上传图片文件

  • 除了框架内置的绑定器,使用者还可以自定绑定器

    1. 使用者自定义绑定器,实现绑定策略接口

    2. 框架提供配置规则, 使用者将自定义参数绑定器配置(告诉)给框架

      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,就需要修改源码
    • 问题二:不利于用户自定义
  • 我们采用类似于参数绑定器的方式来实现

    • 框架提供转换器规范(接口)
    • 框架内部提供一部分转换器。
    • 用户可以自定义转换。
      1. 自定义类型转换器,实现接口
      2. 配置类型转换器(告诉框架,使用者提供了一个自定义类型转换器)

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处理。