我用spring系列框架进行开发也有两三年的时间了,但是仅止于使用,偶尔会聊些一些细节的原理什么的,但是对于spring其实还停留在一知半解的地步上_(:зゝ∠)_。这次找到了一个介绍编写spring mvc的小视频,正好从头开始重新系统的了解spring的运行原理。
spring的代码运行分为三个阶段:配置阶段,初始化阶段和运行阶段(如下图)。
1. 配置阶段:先在web.xml里配置dispatchServlet类的路径,并顺便配置了配置文件的路径(application.xml或application.properties),和url-pattern。
2. 初始化阶段:在这个阶段里代码开始对spring容器进行初始化。
3. 运行阶段
具体的三个步骤就是执行了上面说的过程,下面我会跟着代码一步一步的来解说一下。
首先我们知道我们需要在web.xml中要配置DispatchServlet的路径,并且配置文件也要在web.xml里标明(当然配置文件里要有需要扫描的包名),还有url的跟路径,代码如下:
web.xml
Mrh Web Application
mrhmvc
com.mrh.spring.handwrite.mvcframework.servlet.MRHDispatchServlet
contextConfigLocation
application.properties
1
mrhmvc
/*
application.properties:
scanPackage=com.mrh.spring.handwrite.demo
scanPackage就是配置了需要被扫描的包名。
DispatchServlet继承自HttpServlet,我们需要重写doGet(),doPost()和init()方法。
在初始化阶段首先需要完成的是init(), 其实也不是首先了,毕竟整个初始化的流程我们都准备在init方法中完成_(:зゝ∠)_。
@Override
public void init(ServletConfig config) throws ServletException {
// 1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
// 2.扫描并加载相关类
doScanner(contextConfig.getProperty("scanPackage"));
// 3.初始化IOC容器
doInstance();
// 4.反射依赖注入
doAutowired();
// 5.构建handlerMapping, 将url和method建立一对一关系
doInitHandlerMapping();
System.out.println("MRH spring MVC is init.");
}
新建了5个方法,用于完成我们上面列举的五个步骤。
a. 加载配置文件
ServletConfig实际上就是读取web.xml里的配置的一个类, 之前配置的时候我们已经看到我们实际上在web.xml里配置了application.properties的地址,而我们现在要做的就是先找到application.properties文件,并把里面配置的属性加载出来。
private void doLoadConfig(String contextConfigLocation) {
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(null != is) {
try {
is.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
b. 扫描并加载相关类
实际上我们在application.properties文件里也只是配了需要扫描的包名,在这一步中我们就要根据配置的包名扫描出所有需要加载的文件,但是需要注意的是,我们只加载.class文件。因为文件结构可能是多层的,所以我们用递归的方式将所有子目录下的文件都加载进来:首先第一步是把包名转换成相对路径,然后取得路径下的文件列表,判断当前文件是否为文件夹,如果是则将这个文件夹的名字加到路径里,递归调用自己的方法,如果不是则加载这个文件。
说的加载这个文件,只是把这个类的类名存放到我们的类名列表里。
private void doScanner(String scanPackage) {
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classDir = new File(url.getFile());
for(File file : classDir.listFiles()) {
if(file.isDirectory()) {
doScanner(scanPackage + "." + file.getName());
} else {
if(!file.getName().endsWith(".class")) {
continue;
}
String className = scanPackage + "." + file.getName().replace(".class", "");
classNames.add(className);
}
}
}
c. 初始化ioc容器
根据类名列表中的类名,反射加载类,并生成实例放入ioc容器中(在mini代码中,ioc容器就是一个以类名为key,实例为value的hashmap)。
如果类名列表为空,则自动返回。否则遍历类名,并根据类名获取类描述对象,需要注意的是我们只需要加载有注解的类,没有注解的类我们概不加载(注解也是自己写的_(:зゝ∠)_,下一块会放出代码)。而且对于不同注解,我们要用不同的方法处理,controller因为基本上都是不是接口和实现的形式,所以我们对于注解了@Controller的类,只需要对类名的首字母改成小写作为key,生成实体对象为value,存入ioc的map就可以。但是因为@Service注解的类经常有可能是接口的实现类,而且经常会自定义service的名字,所以我们还需要看是否有另外定义名字,如果有另外定义,就用自定义的,如果没有就将类名的首字母改成小写。而且对于service对象,还要用实例对所有实现的接口进行初始化,key就是接口的全类名。
private void doInstance() {
if(classNames.isEmpty()) {
return;
}
try {
for(String className : classNames) {
Class> clazz = Class.forName(className);
if(clazz.isAnnotationPresent(MRHController.class)) {
Object instance = clazz.newInstance();
String beanName = lowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, instance);
} else if(clazz.isAnnotationPresent(MRHService.class)) {
// Service初始化的并不是类的本身,如果是接口的话,而是类对应的实现类
// 1. 如果service不是接口,默认就是类名的首字母小写作为key
MRHService service = clazz.getAnnotation(MRHService.class);
String beanName = service.value();
// 2. 如果用户自定义了beanName,那么要优先使用beanName进行key-value的组合
if("".equals(beanName)) {
beanName = lowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
// 3. 赋值的对象是接口的话,那么要采用一种投机取巧的方式,用接口的全类名做为key,实现的实例作为值,方便依赖注入时使用
Class>[] interfaces = clazz.getInterfaces();
for(Class> i : interfaces) {
if(ioc.containsKey(i.getName())) {
throw new Exception("The " + clazz.getName() + " for " + beanName + " exists!!");
}
ioc.put(i.getName(), instance);
}
} else {
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
对了需要补充一下,将类名首字母小写的私有方法:
private String lowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
d. 反射依赖注入
我们已经把被注解的类的初始化到容器中,但是类中还注解了@Autowired的一些对象,我们在初始化的时候就进行依赖注入。
如果ioc容器此时为空容器,我们就不需要做任何操作。
ioc容器不为空的情况下,我们需要遍历容器中存储的类,取出每一个类的field列表,遍历field是否注解了@Autowired,如果有就需要注入实例(从ioc中取出对应的实例,注入)。需要说明的是:如果字段是private 或者protected 或者是default,这个字段是不可见的,就要强制将它置为可见。
private void doAutowired() {
if(ioc.isEmpty()) {
return;
}
for(Map.Entry entry : ioc.entrySet()) {
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for(Field field : fields) {
if(!field.isAnnotationPresent(MRHAutowired.class)) {
continue;
}
MRHAutowired autowired = field.getAnnotation(MRHAutowired.class);
String beanName = autowired.value();
if("".equals(beanName)) {
beanName = field.getType().getName();
}
// 如果字段是private 或者protected 或者是default,这个字段是不可见的
field.setAccessible(true); // 不管你是否是外部可见,都强制设为可见,暴力访问
try {
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalArgumentException | IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
f.构建handlerMapping
构建handlerMapping主要是建立url和method的一一对应关系,这样请求就能找到对应的方法。
首先我们先要找出所有的controller,先拿出注解在类上的@HandlerMapping注解,取得这个controller的基础url,然后再获取每个方法上注解的@HandlerMapping的值与基础url拼接成这个method对应的url pattern(转成Pattern是因为url的匹配是支持正则表达式的),用controller的类信息和method的信息与url构建一个Handler对象,并把需要的参数和参数的顺序也存入其中。最后HandlerMapping就是一个handler对象的列表。
private void doInitHandlerMapping() {
if(ioc.isEmpty()) {
return;
}
for(Map.Entry entry : ioc.entrySet()) {
Class> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(MRHController.class)) {
continue;
}
String baseUrl = "";
if(clazz.isAnnotationPresent(MRHRequestMapping.class)) {
MRHRequestMapping mrhRequestMapping = clazz.getAnnotation(MRHRequestMapping.class);
baseUrl = mrhRequestMapping.value();
}
Method [] methods = clazz.getMethods();
for(Method method : methods) {
if(!method.isAnnotationPresent(MRHRequestMapping.class)) {
continue;
}
MRHRequestMapping mrhRequestMapping = method.getAnnotation(MRHRequestMapping.class);
String url = ("/" + baseUrl + "/" + mrhRequestMapping.value()).replaceAll("/+", "/");
handlerMapping.add(new Handler(Pattern.compile(url), clazz, method));
System.out.println("Mapped : " + url + "," + method);
}
}
}
private class Handler {
protected Object controller;
protected Method method;
protected Pattern pattern;
protected Map paramIndexMapping;
protected Handler(Pattern pattern, Object controller, Method method) {
this.controller = controller;
this.method = method;
this.pattern = pattern;
paramIndexMapping = new HashMap();
paramIndexMapping(method);
}
private void paramIndexMapping(Method method) {
Annotation [][] pa = method.getParameterAnnotations();
for(int i = 0; i[] paramTypes = method.getParameterTypes();
for(int i=0; i type = paramTypes[i];
if(type == HttpServletRequest.class || type == HttpServletResponse.class) {
paramIndexMapping.put(type.getName(), i);
}
}
}
}
到这一步, mini spring的初始化就算完成了。
根据上面的初始化过程,我们可以看出,spring的运行很大程度上是靠了各种注解。而我们在平常的开发过程中,也常用各种注解,对这些注解可以说已经很熟悉了,如@Controller,@Service, @Autowired,@RequestMapping,@RequestParam。因为是手写迷你spring框架,所以注解我们也是简单的自己写了一下。
@MRHController
package com.mrh.spring.handwrite.mvcframework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MRHController {
String value() default "";
}
@MRHService
package com.mrh.spring.handwrite.mvcframework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MRHService {
String value() default "";
}
@MRHAutowired
package com.mrh.spring.handwrite.mvcframework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MRHAutowired {
String value() default "";
}
@MRHRequestMapping
package com.mrh.spring.handwrite.mvcframework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MRHRequestMapping {
String value() default "";
}
@MRHRequestParam
package com.mrh.spring.handwrite.mvcframework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MRHRequestParam {
String value() default "";
}
中间需要注意的是,@Target是java的元注解(也就是用于注解的一种注解),它是用来配置这个新注解可以注解的对象的,比如@MRHController就只能注解类,@MRHRequestMapping就既能注解类又能注解方法,@MRHRequestParam注解参数,@MRHAutowired就只能注解变量。详细列一下可以配置的值,和对应的意思:
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
而@Retention也是java的元注解,用来配置这个注解被保留的时间长短,也就是生命期,RetentionPolicy.RUNTIME表明是运行时生效:
1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留)
插播了注解的内容,我们的整个容器初始化阶段已经完成了,接下来进进入到运行阶段需要的代码了。在运行阶段,我们会通过url发起请求,spring会找到对应的方法执行后返回返回值后返回给前台。
在初始化DispatchServlet的时候,我们除了初始化了init()方法,还初始化了doGet和doPost两个方法,因为我们只是简单写功能,所以我们就让doGet和doPost走同一套逻辑。
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req, resp);
} catch(Exception e) {
resp.getWriter().write("500 Exception, Detail:" + Arrays.toString(e.getStackTrace()));
}
}
其实最后的执行逻辑还是写在doDispatch()方法里,在doPost()方法中统一处理了一下异常。
然后doDispatch()方法中的逻辑是这样的:先根据url遍历handler找到匹配的handler, 没找到就报404异常。然后从handler中取出method,和对应的参数类型,根据参数类型数组,配置一个对应的值数组(当然目前这个是空的,准备从request里取出来放进去的)。然后从request里取出参数map,进行转换后放到对应的数组index里去。最后再把request和response也放入参数数组里,invoke方法。
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
try {
Handler handler = getHandler(req);
if(handler == null) {
resp.getWriter().write("404 Not Found");
return;
}
// 获取方法的参数列表
Class> [] paramTypes = handler.method.getParameterTypes();
// 保存所有需要自动赋值的参数值
Object[] paramValues = new Object[paramTypes.length];
Map params = req.getParameterMap();
for(Entry param : params.entrySet()) {
String value = Arrays.deepToString(param.getValue()).replaceAll("\\[|\\]", "");
// 如果找到匹配的对象,则开始填充参数值
int index = handler.paramIndexMapping.get(param.getKey());
paramValues[index]= convert(paramTypes[index], value);
}
// 设置方法中的request对象和response对象
int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
String beanName = lowerFirstCase(handler.method.getDeclaringClass().getSimpleName());
handler.method.invoke(ioc.get(beanName), paramValues);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
public Object convert(Class> type, String value) {
if(type.equals(Integer.class)) {
return Integer.valueOf(value);
}
if(type.equals(Double.class)) {
return Double.valueOf(value);
}
if(type.equals(Float.class)) {
return Float.class;
}
if(type.equals(Date.class)) {
return Date.valueOf(value);
}
if(type.equals(Long.class)) {
return Long.valueOf(value);
}
if(type.equals(String.class)) {
return value;
}
Object valueObj = (Object)value;
JSONObject jsonObject = JSONObject.fromObject(valueObj);
return JSONObject.toBean(jsonObject, type);
}
private Handler getHandler(HttpServletRequest req) throws Exception {
if(handlerMapping.isEmpty()) {
return null;
}
// 拿到用户的请求
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "/");
for(Handler handler : handlerMapping) {
if(handler.pattern.matcher(url).matches()) {
return handler;
}
}
return null;
}