该文章主要是分析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的类置于容器内。(为了防止篇幅过长,所以没有上传所有代码,仅以思想为主,后续会把完整代码上传到CSDN资源和我的github)
/* 创建一个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
注:文章中很多内容都是使用伪代码进行实现的,主要原因是怕文章太长,看着太枯燥。手写的spring整个工程已经在整理了,后续会发布到github,到时候会把链接补上。需要的也可以留下邮箱。欢迎大佬们指出有误的地方。
1毛足够感动我!!!