该文章主要是分析Springmvc启动的流程(配置阶段、初始化阶段和运行阶段),可以让自己对spring框架有更深一层的理解。对框架比较感兴趣的朋友都可以了解阅读下,对于我所描述的内容有错误的还望能不吝指出。
对于springmvc中的整个流程我个人把他分为这几个阶段,包括个人手写的spring也是参照此按阶段实现:
根据web.xml ,先定义DispatcherServlet并且定义该sevlet传入的参数和路径。
初始化阶段中又可以分为IOC、DI和MVC阶段:
运行阶段中,当接受到一个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是一个实现了MVC设计模式的轻量级web层框架,使用起来简单方便。
1、清晰的角色划分:
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
负责最终的响应,默认是转发的操作。执行流程图:
图画的有些灵魂,但是大致的流程就是这样,大家可以参考着理解下。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。