使用springmvc运行流程分析,手写spring框架尝试
目录
- 1.配置阶段
- 2.初始化阶段
- 3.运行阶段
- springMVC介绍以及执行流程
- 什么是SpringMVC?
- SpringMVC的优势是什么?
该文章主要是分析Springmvc启动的流程(配置阶段、初始化阶段和运行阶段),可以让自己对spring框架有更深一层的理解。对框架比较感兴趣的朋友都可以了解阅读下,对于我所描述的内容有错误的还望能不吝指出。
对于springmvc中的整个流程我个人把他分为这几个阶段,包括个人手写的spring也是参照此按阶段实现:
1.配置阶段
根据web.xml ,先定义DispatcherServlet并且定义该sevlet传入的参数和路径。
2.初始化阶段
初始化阶段中又可以分为IOC、DI和MVC阶段:
- (1)IOC:初始化配置文件和IOC容器,扫描配置的包下的类,通过反射机制将需要实例化的类放入IOC容器,既将带有spring注解的类进行实例化后存放到 IOC 容器中。IOC容器的实质就是一个集合;
- (2)DI:DI阶段(其实就是依赖注入)。对需要赋值的实例属性进行赋值(一般较多都是处理带有注解的@Autowrized的属性)
- (3)MVC:构造出HandlerMapping集合,主要作用就是用于存放对外公开的API和Method之间的关系,一个API一般会对应一个可执行的Method.
3.运行阶段
运行阶段中,当接受到一个url后,会到HandleMapping集合中,找到对应Method、通过反射机制去执行invoker,再返回结果给调用方。
这样就大体完成了springmvc整个运行阶段,所描述的都仅为个人观点,如果有误请在评论中指出。
其整体流程可以参照下图:
接下来就来尝试手写一个类似springmvc的框架了,这个手写的过程还是相当有成就感的!
1.创建一个空的JavaWeb工程,引入依赖,其实因为我们是要手写spring,所以基本不需要什么外部的依赖工具,只需要导入servlet-api即可,如下:
<dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency>
2.根据上述的流程描述,接下来就是对web.xml进行配置:
<servlet> <servlet-name>appServlet</servlet-name> <servlet-class>com.wangcw.cwframework.sevlet.CwDispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>application.properties</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>
对于配置中的CwDispatcherServlet其实就是个人自定义一个作用与spring中DispatcherServlet相同的Servlet,此处先创建一个空的CwDispatcherServlet,继承 javax.servlet.http.HttpServlet即可,具体实现后面会描述。
此处因为是手写spring的部分功能,所以配置也不用写太多,此处仅拿一个包扫描的配置(scanPackage),各位少侠可自行拓展。
CwDispatcherServlet中初始化的配置文件application.properties内容如下:
scanPackage=com.wangcw
3.相信spring中又一部分注解都是大家比较熟悉的,接下来我们先从这几个注解着手吧。(此处就不指出各个注解的作用了,相信百度上已经很多了)
spring注解 | 自定义注解 |
@Controller | @CwController |
@Autowired | @CwAutowired |
@RequestMapping | @CwRequestMapping |
@RequestParam | @CwRequestParam |
@Service | @CwService |
然后实现下各个自定义的注解,直接贴代码:
/* * 创建一个类似@Controller作用的注解类 */ import java.lang.annotation.*; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CwController { String value() default ""; }
/* * 创建一个类似@Autowried作用的注解类 */ import java.lang.annotation.*; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CwAutowried { String value() default ""; }
/* * 创建一个类似@RequestMapping作用的注解类 */ import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CwRequestMapping { String value() default ""; }
/* * 创建一个类似@RequsetParam作用的注解类 */ import java.lang.annotation.*; @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CwRequestParam { String value() default ""; }
/* * 创建一个类似@Service作用的注解类 */ import java.lang.annotation.*; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CwService { String value() default ""; }
4.创建一个简单的控制层和业务层交互 Demo,加上自定的注解,具体注解的功能,后面赘述。
Controller.java
@CwController @CwRequestMapping("/demo") public class Controller { @CwAutowried private Service service; @CwRequestMapping("/query") public void query(HttpServletRequest req, HttpServletResponse resp,@CwRequestParam("name") String name) throws IOException { resp.getWriter().write(service.query(name)); } @CwRequestMapping("/add") public void add(HttpServletRequest req, HttpServletResponse resp, @CwRequestParam("a") Integer a, @CwRequestParam("b") Integer b) throws IOException { resp.getWriter().write("a+b="+(a+b)); } }
Service.java
public interface Service { String query(String name); }
ServiceImpl.java
@CwService public class ServiceImpl implements Service{ @Override public String query(String name) { return "I am "+name; } }
5.上面的controller层和service层已经把我们上述的自定注解都使用上去了,接下来我们开始手写spring的核心功能了,也就是实现CwDispatcherServlet.java这个HttpServlet的子类。
首先需要重写父类中的init方法,因为我们要在Init过程中实现出跟spring一样的效果。
理一理init()过程中都需要做哪些事情呢?整理了一下init()中主要需要以下几步操作
@Override public void init(ServletConfig config) { /* 1.加载配置文件*/ doLoadConfig(config.getInitParameter("contextConfigLocation")); /* 2.扫描scanPackage配置的路径下所有相关的类*/ doScanner(contextConfig.getProperty("scanPackage")); /* 3.初始化所有相关联的实例,放入IOC容器中*/ doInstance(); /*4.实现自动依赖注入 DI*/ doAutowired(); /*5.初始化HandlerMapping */ initHandlerMapping(); }
第一步很简单,在类中定义一个Properties实例,用于存放Servlet初始化的配置文件。导入配置代码略过,IO常规读写即可。
private Properties contextConfig = new Properties();
第二步通过上面获取到的配置,取到需要扫描的包路径,然后在根据路径找到对应文件夹,做一个递归扫描即可。将扫描到的文件名去除后缀,保存到一个集合中,那么该集合就存放了包下所有类的类名。
String scanFileDir = contextConfig.getProperty("scanPackage"); /* 用于存放扫描到的类 */ private List<String> classNames = new ArrayList<String>(); /*扫描获取到对应的class名,便于后面反射使用*/ String className = scanPackage + "." + file.getName().replace(".class", ""); classNames.add(className);
第三步就是IOC阶段,简而言之就是对上面集合中所有的类进行遍历,并且创建一个IOC容器,将带有@CwController和@CwService的类置于容器内。
/* 创建一个IOC容器 */ private Map<String, Object> IOC = new HashMap<String, Object>(); for (String classNme : classNames){ if( 对加了 @CwController 注解的类进行初始化){ /* 对于初始化的类还需要放入IOC容器, 对于存入IOC的实例,key值是有一定规则的,默认类名首字母小写;*/ /* toLowerFirstCase是自定义的一个工具方法,用于将传入的字符串首字母小写 */ String beanName = toLowerFirstCase(clazz.getSimpleName()); IOC.put(beanName, clazz.newInstance()); } else if (对加了 @CwService 注解的类进行初始化){ /* 对于存入IOC的实例,key值是有一定规则的,而Service层的规则相对上面更复杂一些,因为注解可以有自定义实例名,并且可能是接口实现类 */ IOC.put(beanName, instance); } else { //对于扫描到的没有注解的类,忽略初始化行为 continue; } }
第四步是DI操作,将IOC容器中需要赋值的实例属性进行赋值,即带有Autowired注解的实例属性。伪代码如下:
/*遍历IOC中的所有实例*/ for(Map.Entry<String, Object> entry : IOC.entrySet()){ /* 使用getDeclaredFields暴力反射 */ Field [] fields = entry.getValue().getClass().getDeclaredFields(); for (Field field : fields){ /*1.判断属性是否有加注解@CwAutowried.对于有注解的属性才需要赋值*/ .... /*属性授权*/ field.setAccessible(true); field.set(entry.getValue(), IOC.get(beanName)); } }
第五步要处理Controller层的Method与请求url的匹配关系,让请求能准确的请求到对应的url。篇幅问题,此处还是上传伪代码。
/* 创建HandlerMapping存放url,method的匹配关系 其中类Handler是我自己定义的一个利用正则去匹配url和method, 只要用户传入url,Handler就可以响应出其对应的method*/ private List<Handler> handlerMapping = new ArrayList<Handler>(); /* 遍历IOC容器 */ for (Map.Entry<String, Object> entry : IOC.entrySet()){ Class<?> clazz = entry.getValue().getClass(); /* 只对带有CwController注解的类进行处理 */ 定义一个url,由带有CwController的实例类上的@CwRequestMapping注解的值和Method上@CwRequestMapping注解的值组成 /* (1).判断类上是否有CwRequestMapping注解 ,进行拼接 url*/ /* (2).遍历实例下每个Method,并且需要判断该方法是否有【 @CwRequestMapping 】注解,拼接url*/ /* 最后将匹配关系以正则的形式,放到HandlerMapping集合中 */ String regex = (url); Pattern pattern = Pattern.compile(regex); handlerMapping.add(new Handler(pattern,method)); }
到这里就基本完成了springmvc的初始化阶段,之后的工作就是重写一下CwDispatcherServlet.java父类的doGet()/doPost()方法。根据request中的URI和参数来执行对应的Method,并且响应结果。
/* 利用反射执行其所匹配的方法 */ handler.method.invoke(handler.controller, paramValues);
到此整个步骤就完成了,此时可以愉快的启动项目,并访问对应的url进行测试了。
根据上面Controller定义的方法可以知道其匹配的url为 : /demo/query 和 /demo/add,并且有使用@CwRequestParam注解定义了其各个参数的名称。
测试结果如下:
http://localhost:8080/spring/demo/query?name=James
http://localhost:8080/spring/demo/add?a=222222&b=444444
再来测试个url,是controller中没有声明出@CwRequestMapping注解的,看看结果。
http://localhost:8080/spring/demo/testNoUrl
springMVC介绍以及执行流程
什么是SpringMVC?
SpringMVC是一个实现了MVC设计模式的轻量级web层框架,使用起来简单方便。
SpringMVC的优势是什么?
1、清晰的角色划分:
- 前端控制器(DispatcherServlet)
- 请求到处理器映射(HandlerMapping)
- 处理器适配器(HandlerAdapter)
- 视图解析器(ViewResolver)
- 处理器或页面控制器(Controller)
- 验证器( Validator)
- 命令对象(Command 请求参数绑定到的对象就叫命令对象)
- 表单对象(Form Object 提供给表单展示和提交到的对象就叫表单对象)。
2、分工明确,而且扩展点相当灵活,可以很容易扩展,虽然几乎不需要。
3、由于命令对象就是一个 POJO,无需继承框架特定 API,可以使用命令对象直接作为业务对象。
4、和 Spring 其他框架无缝集成,是其它 Web 框架所不具备的。
5、可适配,通过 HandlerAdapter 可以支持任意的类作为处理器。
6、可定制性, HandlerMapping、 ViewResolver 等能够非常简单的定制。
7、功能强大的数据验证、格式化、绑定机制。
8、利用 Spring 提供的 Mock 对象能够非常简单的进行 Web 层单元测试。
9、本地化、主题的解析的支持,使我们更容易进行国际化和主题的切换。
10、强大的 JSP 标签库,使 JSP 编写更容易。
还有比如RESTful风格的支持、简单的文件上传、约定大于配置的契约式编程支持、基于注解的零配置支持等等。
与Struts2的对比:
共同点:都是基于MVC设计模式的表现层框架,底层实现都离不开原始的Servlet,处理请求的机制都是一个核心控制器;
区别:Spring MVC 的入口是 Servlet, 而 Struts2 是 Filter
Spring MVC 是基于方法设计的,而 Struts2 是基于类, Struts2 每次执行都会创建一个动作类。所以 Spring MVC 会稍微比 Struts2 快些。
相比来说,SpringMVC已经全面超越了Struts2。
执行流程:
DispatcherServlet: 是整个springmvc框架的核心。
前端控制器/核心控制器:所有的请求和响应都由此控制器进行分配。
前端控制器的所有工作都是基于组件完成的:
三大组件:
HandlerMapping
: 它负责根据客户端的请求寻找对应的hadler,找到以后把寻找的handler返回给DispatcherServlet;HandlerAdapter
:它负责执行寻找到的Handler的方法,方法执行完后将返回值给HandlerAdapter, HandlerAdapter将返回值传给DispatcherServlet;ViewResolver
:它根据DispatcherServlet指定的返回结果寻找对应的页面,找到后将结果返回给DispatcherServlet。DispatcherServlet
负责最终的响应,默认是转发的操作。
执行流程图:
图画的有些灵魂,但是大致的流程就是这样,大家可以参考着理解下。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。