背景
编码一年多,接触的项目每个都用到springmvc,只会用,而不知其原理。今日咱就把它弄清楚了!原本以为会有很多代码好多类,但其实源码很少。
开动
通过写一个自己的springmvc,来深化自己对其的了解。首先展示下代码结构:
然后我们一个个来看!
首先都是一些springmvc的注解类:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyController {
String value() default "";
}
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAutowired {
String value() default "";
}
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
String value() default "";
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyService {
String value() default "";
}
从上面可以看到,这些注解除了一些细微的差别,基本上都是一样的。不一样的地方我们也能想到,无非是区分什么时候起作用@Retention。注解是加在哪里的,类?方法?还是成员变量上?@Target 这里注解不做过多解释。
接下来就是我们的重点来了!DispatchServlet类,springmvc的核心!
咱们一点一点来,先看看它是否继承了什么?有哪些成员变量?
public class MyDispatchServlet extends HttpServlet {
//配置文件属性
private Properties contextConfig = new Properties();
//所有的类路径集合
private List<String> classNameLists = new ArrayList<String>();
//spring的ioc容器
private HashMap<String, Object> iocMap = new HashMap<String, Object>();
//url对应的具体方法
private HashMap<String, Method> handlerMapping = new HashMap<String, Method>();
可以看到继承了HttpServlet,继承了它,然后我们重写一下它的init方法,doget,dopost方法。
@Override
public void init(ServletConfig config) throws ServletException {
//1、加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2、扫描所有类
doScanPackage(contextConfig.getProperty("scan-package"));
//3、初始化ioc容器,通过反射创建所有相关的类实例,并添加到ioc容器中
doCreateInstanceToIOC();
//4、依赖注入-简称DI
doAutowiredFields();
//5、初始化 HandlerMapping
initHandlerMapping();
//6、打印数据
doPrintData();
}
众所周知,springmvc启动后,会加载该类,然后第一步就是初始化,也就是init方法,让我们来看看里面都具体做了什么?
(1)doLoadConfig(config.getInitParameter("contextConfigLocation"));加载配置文件application.properties
路径在web.xml有配置
代码如下:它会把application.properties里的所有配置加载到成员变量contextConfig里,以作后面使用。
/**
* 1 初始化加载配置文件
* @param contextConfigLocation
*/
private void doLoadConfig(String contextConfigLocation) {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(inputStream);
System.out.println("[1].配置文件已经保存在contextConfig.");
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2)doScanPackage(contextConfig.getProperty("scan-package"));扫描指定包下的所有类
代码如下:将所有类的路径保存到成员变量classNameLists里,以作后面使用
/**
* 2 扫描所有包下的类文件,将包下的所有类的路径加入到list里
* @param scanPackage
*/
private void doScanPackage(String scanPackage) {
URL rootPath = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
if (rootPath == null) {
return;
}
File files = new File(rootPath.getFile());
for (File file : files.listFiles()) {
if (file.isDirectory()) {
//递归调用,遍历子目录
doScanPackage(scanPackage + "." + file.getName());
} else {
if (!file.getName().endsWith(".class")) {
//如果不是java类文件,则继续下次循环
continue;
}
String classPath = scanPackage + "." + file.getName().replace(".class", "");
//将类的路径存入集合中
classNameLists.add(classPath);
System.out.println("[2]. {" + classPath + "} 已经存入集合classNameLists中.");
}
}
}
(3)doCreateInstanceToIOC();见其名,知其意,将所有创建的实例,也就是上一步扫描到的所有类,以反射形式创建对象,加载到成员变量iocMap中!这也就是我们经常说的IOC的一种运用,控制反转。
代码如下:
/**
* 3 反射创建所有相关的类实例,并添加到ioc容器中
*/
private void doCreateInstanceToIOC() {
if (classNameLists.isEmpty()) {
return;
}
try {
for (String className : classNameLists) {
Class<?> clazz = null;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//如果是MyController注解
if (clazz.isAnnotationPresent(MyController.class)) {
String beanName = toLowerFirstCase(clazz.getSimpleName());
//创建对象,存入ioc
Object instance = clazz.newInstance();
iocMap.put(beanName, instance);
System.out.println("[3] {" + beanName + "} 已经存入 iocMap.");
} else if (clazz.isAnnotationPresent(MyService.class)) {//如果是MyService注解
String beanName = toLowerFirstCase(clazz.getSimpleName());
//如果该类有自定义名称
MyService myService = clazz.getAnnotation(MyService.class);
if (!"".equals(myService.value())) {
beanName = myService.value();
}
//创建对象,存入ioc
Object instance = clazz.newInstance();
iocMap.put(beanName, instance);
System.out.println("[3] {" + beanName + "} 已经存入 iocMap.");
//如果该类有接口类,同样将该实例存入ioc中,key为接口的全路径
for (Class<?> inter : clazz.getInterfaces()) {
if (iocMap.containsKey(inter.getName())) {
throw new Exception("The Bean Name Is Exist.");
}
iocMap.put(inter.getName(), instance);
System.out.println("[3] {" + inter.getName() + "} 已经存入 iocMap.");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
(4)doAutowiredFields();依赖注入,简称DI,将每个ioc容器中的对象所拥有的的成员变量创建然后注入进该对象中,也就是我们经常用到的@Autowired的原理。
代码如下:
/**
* 4 依赖注入,为该类中注入的成员变量赋值
*/
private void doAutowiredFields() {
if (iocMap.isEmpty()) {
return;
}
//遍历ioc,依次注入
for (Map.Entry<String,Object> entry : iocMap.entrySet()) {
//获取所有成员属性
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(MyAutowired.class)) {
continue;
}
// 获取注解对应的类
MyAutowired myAutowired = field.getAnnotation(MyAutowired.class);
String beanName = myAutowired.value().trim();
// 获取 XAutowired 注解的值
if ("".equals(beanName)) {
System.out.println("[INFO] myAutowired.value() is null");
beanName = field.getType().getName();
}
// 只要加了注解,都要加载,不管是 private 还是 protect
field.setAccessible(true);
try {
field.set(entry.getValue(), iocMap.get(beanName));
System.out.println("[4] field set {" + entry.getValue() + "} - {" + iocMap.get(beanName) + "}.");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
(5)initHandlerMapping();初始化 HandlerMapping,扫描iocMap,解析MyRequestMapping注解,将其对应的方法与其url放入成员变量handlerMapping中。
代码如下:
/**
* 5 将url与方法一一对应
*/
private void initHandlerMapping() {
if (iocMap.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : iocMap.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(MyController.class)) {
continue;
}
//检查类上面是否有MyRequestMapping注解
String baseUrl = "";
if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
baseUrl = clazz.getAnnotation(MyRequestMapping.class).value();
}
Method[] methods = clazz.getMethods();
//遍历类下面的所有方法
for (Method method : methods) {
if (!method.isAnnotationPresent(MyRequestMapping.class)) {
continue;
}
MyRequestMapping myRequestMapping = method.getAnnotation(MyRequestMapping.class);
String url = ("/" + baseUrl + "/" + myRequestMapping.value()).replaceAll("/+", "/");
handlerMapping.put(url, method);
System.out.println("[5] handlerMapping put {" + url + "} - {" + method + "} success.");
}
}
}
(6)doPrintData(); 最后一步,打印日志。代码如下:
/**
* 6 打印相关日志
*/
private void doPrintData() {
System.out.println("[6]----data--------------------");
System.out.println("contextConfig.propertyNames()-->" + contextConfig.propertyNames());
System.out.println("[classNameList]-->");
for (String str : classNameLists) {
System.out.println(str);
}
System.out.println("[iocMap]-->");
for (Map.Entry<String, Object> entry : iocMap.entrySet()) {
System.out.println(entry);
}
System.out.println("[handlerMapping]-->");
for (Map.Entry<String, Method> entry : handlerMapping.entrySet()) {
System.out.println(entry);
}
System.out.println("[6]----done-----------------------");
System.out.println("====启动成功====");
}
可以看到,springmvc加载的时候,就做了很多的工作!
接下来,看看调用是如何开始的,我们知道,在调用与之对应的url时,会调用doget或者dopost方法,然后进入doDispatch(req, resp);
通过url在成员变量hardmapping中获取对应的方法路径,然后反射调用该方法。即完成一次调用!
/**
* 7 拦截,匹配,运行
* @param req
* @param resp
*/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) {
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "");
System.out.println("[7]request url -->" + url);
//如果路径不存在,则报404
if (!this.handlerMapping.containsKey(url)) {
try {
resp.getWriter().write("404 NOT FOUND!!");
return;
} catch (IOException e) {
e.printStackTrace();
}
}
//反射调用具体的方法
Method method = this.handlerMapping.get(url);
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
try {
method.invoke(iocMap.get(beanName), req, resp);
System.out.println("[7] method.invoke success,class is {" + iocMap.get(beanName) + "}.");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@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) {
e.printStackTrace();
resp.getWriter().write("500 Exception Detail:\n" + Arrays.toString(e.getStackTrace()));
}
}
/**
* 将首字母变为小写
* @param className
* @return
*/
private String toLowerFirstCase(String className) {
char[] charArray = className.toCharArray();
charArray[0] += 32;
return String.valueOf(charArray);
}
收尾
以上代码并非源代码所有,一些地方省略了很多。不过足以让我们知道其原理了。码农朋友们,看完之后,是否对springmvc有了新的认知,是不是觉得其实它也就那么回事,如果认同博主的话,点个赞呗!