磕磕碰碰,终于开始翻看spring源码了。spring使用已经很多,之前一直想学习spring源码,但是一直没有开始。这篇博客开始学习spring源码,手动实现一个简单的基于servlet的mvc框架。
这个之前我们看过了很多资料,但是一直没有真实理解,毕竟没有一个实际的感受。MVC调用具体流程图如下(个人理解)
简单点说,手写一个mvc调用框架(超级简单的版本)。更通俗点说,用代码实现上述流程图。
新建一个web项目,这一步就不详细说了。
自己的SelfController注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfController {
String value() default "";
}
自己的RequestMapping注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfRequestMapping {
String value() default "";
}
自己的RequestParam注解
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfRequestParam {
String value() default "";
}
自己的Autowired注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfAutowired {
String value() default "";
}
自己的service注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfService {
String value() default "";
}
package com.learn.springmvc.servlet.V2;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* autor:liman
* comment: 自己的servlet
*/
public class SelfServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
/**
* 初始化方法
*
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
}
}
selfServlet
com.learn.springmvc.servlet.V2.SelfServletV2
contextConfigLocation
application.properties
1
selfServlet
/*
其中的init-param是给servlet一个初始化的参数,这个后面会详细谈到。
Servlet中的init方法中,会完成HandlerMapping,IOC相关内容的初始化,具体如下所示。
public void init(ServletConfig config) throws ServletException {
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2.扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//3.初始化扫描的类
doInstance();
//4.完成依赖注入
doAutowired();
//5.初始化handleMapping
initHandlerMapping();
System.out.println("自己写的spring mvc 初始化完成");
}
加载配置文件,在这里面是最简单的操作,只是将配置文件读取到内存的Properties对象中即可。
//用于保存application.properties配置文件中的内容,这个实例使用properties文件代替xml文件
private Properties contextConfig = new Properties();
/**
* 加载配置文件
*/
private void doLoadConfig(String contextConfigLocation) {
InputStream fis = null;
fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(fis);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这里只是简单的一个mvc简单的示例,配置文件中只有一项配置:
scanPackage=com.learn.springmvc
上述的工作只是将该配置属性读取到属性contextConfig中。该配置中指定了我们需要扫描的基础包名
//用于保存所有扫描出来的类名
private List classNames = new ArrayList();
/**
* 扫描出相关的类
*
* @param scanPackage
*/
private void doScanner(String scanPackage) {
//主要是将包路径转换为文件路径
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classPath = new File(url.getFile());
for (File file : classPath.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);
}
}
}
扫描出所有的类名,将类名放入到List
//传说中的IOC容器,为了简化程序,这里不用ConcurrentHashMap
private Map ioc = new HashMap();
/**
* 初始化,为DI做准备
* 加了注解的类才能初始化,这里只列出加了@Controller和@Service注解的类
*/
private void doInstance() {
if (classNames.isEmpty()) {
return;
}
try {
for (String className : classNames) {
Class> clazz = Class.forName(className);
//如果有Controller注解
if (clazz.isAnnotationPresent(SelfController.class)) {//如果是Controller
Object instance = clazz.newInstance();
//首字母小写,并放入ioc容器中
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, instance);
} else if (clazz.isAnnotationPresent(SelfService.class)) {//如果有Service注解
//获取自定义的beanName——@Service("test")获取其中的test
SelfService service = clazz.getAnnotation(SelfService.class);
String beanName = service.value();//获得注解的值(一般自己制定service的名称的时候)
if ("".equals(beanName.trim())) {
//如果没有指定名称,service 默认首字母小写
beanName = toLowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
//放入IOC容器
ioc.put(beanName, instance);
//注入的时候是接口注入的方式,投机取巧,就将接口作为key,实例作为值
for (Class> i : clazz.getInterfaces()) {
if (ioc.containsKey(i.getName())) {//这样就限制了,一个接口只有一个实现类
throw new Exception("the " + i.getName() + " is exists");
}
ioc.put(i.getName(), instance);
}
} else {
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将首字母小写
*
* @param simpleName
* @return
*/
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
//之所以加,是因为大小写字母的ASCII码相差32,
// 而且大写字母的ASCII码要小于小写字母的ASCII码
//在Java中,对char做算学运算,实际上就是对ASCII码做算学运算
chars[0] += 32;
return String.valueOf(chars);
}
这一步完成了IOC容器的初始化,这一步也算是了解了IOC的基本结构 ——一个类名与类实例的键值对。
/**
* 完成依赖注入
*/
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(SelfAutowired.class)) {
continue;
}
//获取有@Autowired注解的属性上的注解对象
SelfAutowired selfAutowired = field.getAnnotation(SelfAutowired.class);
//获取注解的值
String beanName = selfAutowired.value().trim();
if ("".equals(beanName)) {
//如果没有自定义,默认根据类型注入
beanName = field.getType().getName();
}
//设置属性的访问属性
field.setAccessible(true);
try {
//设置属性的值
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
依赖注入其实也没有想象中的复杂,只是遍历IOC容器中的实体,并遍历每个实体中的属性,如果属性上有注解,则从IOC中取出指定类型的实例,完成初始化。
这是最关键的一步,也是最复杂的一步,同时这一步也引出了Handler与HandlerMapping的关系,HandlerMapping其实就是一个映射,主要完成url与对应的controller中method的映射关系。
private List handlerMapping = new ArrayList();
/**
* 初始化HandlerMapping
* HandlerMapping——url和method一对一的映射
*/
private void initHandlerMapping() {
if (ioc.isEmpty()) {
return;
}
for(Map.Entry entry:ioc.entrySet()){
Class> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(SelfController.class)){
continue;
}
String baseUrl = "";
if(clazz.isAnnotationPresent(SelfRequestMapping.class)){
SelfRequestMapping selfRequestMapping = clazz.getAnnotation(SelfRequestMapping.class);
baseUrl = selfRequestMapping.value();
}
//默认获取所有的public方法
for(Method method:clazz.getMethods()){
if(!method.isAnnotationPresent(SelfRequestMapping.class)){
continue;
}
SelfRequestMapping requestMapping = method.getAnnotation(SelfRequestMapping.class);
String url = ("/"+baseUrl+"/"+requestMapping.value()).replaceAll("/+","/");
this.handlerMapping.add(new HandlerMapping(url,method,entry.getValue()));
System.out.println("Mapped:"+url+":"+method);
}
}
}
为了方便后面的操作,如果HandlerMapping中只有url属性和Method属性是远远不够的,在实际编写代码过程中为了方便通过反射获取方法的参数,需要维护方法所在的类类型,因此最终确定的HandlerMapping的属性如下:
/**
* 暂时将HandlerMapping写成一个静态内部类
*/
public static class HandlerMapping {
private String url;
private Method method;
private Object controller;
private Class>[] paramTypes;//参数类型列表
private Map paramIndexMapping = new HashMap<>();//参数索引位置
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public Class>[] getParamTypes() {
return paramTypes;
}
/**
* 构造函数
*
* @param url
* @param method
* @param controller
*/
public HandlerMapping(String url, Method method, Object controller) {
this.url = url;
this.method = method;
this.controller = controller;
this.paramTypes = method.getParameterTypes();
putParamIndexMapping(method);
}
/**
* 初始化参数的索引位置
*
* @param method
* @return
*/
private void putParamIndexMapping(Method method) {
//提取方法中加了注解的参数
Annotation[][] pa = method.getParameterAnnotations();
for (int i = 0; i < pa.length; i++) {
for (Annotation a : pa[i]) {
if (a instanceof SelfRequestParam) {
String paramName = ((SelfRequestParam) a).value();
if (!"".equals(paramName.trim())) {
this.paramIndexMapping.put(paramName, i);
}
}
}
}
//提取方法中的request和response参数
Class>[] paramsTypes = method.getParameterTypes();
for (int i = 0; i < paramsTypes.length; i++) {
Class> type = paramsTypes[i];
if (type == HttpServletRequest.class
|| type == HttpServletResponse.class) {
this.paramIndexMapping.put(type.getName(), i);
}
}
}
}
paramIndexMapping中维护了参数与参数索引的对应关系。
/**
* 找到指定的一致的handlercMapping处理对象
* @param req
* @return
*/
private HandlerMapping getHandler(HttpServletRequest req){
if(handlerMapping.isEmpty()){
return null;
}
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath,"").replaceAll("/+","/");
for(HandlerMapping handler:this.handlerMapping){
if(handler.getUrl().equals(url)){
return handler;
}
}
return null;
}
上述其实就是从HandlerMapping集合中获取指定的HandlerMapping。至此,上一步就完成Spring mvc的初始化,IOC容器,DI的注入,以及初始化HandlerMapping,接下来就差最后一步了,完成调用,Method对象的调用需要三个属性:1、方法所在的对象,2、所有的参数值,3、对应的Method。
/**
* 开始调用自己写的mvc框架
*
* @param req
* @param resp
*/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
//获取指定的handler
HandlerMapping handler = getHandler(req);
if(handler == null){//没有找到对应的处理器
resp.getWriter().write("404 Not Found!!!");
return;
}
//获取方法的形参列表
Class> [] paramTypes = handler.getParamTypes();
Object [] paramValues = new Object[paramTypes.length];
Map params= req.getParameterMap();
for(Map.Entry param:params.entrySet()){
String value = Arrays.toString(param.getValue()).
replaceAll("\\[|\\]","").replaceAll("\\s",",");
if(!handler.paramIndexMapping.containsKey(param.getKey())){continue;}
int index = handler.paramIndexMapping.get(param.getKey());
paramValues[index] = convert(paramTypes[index],value);
}
if(handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())){
int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
}
if(handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())){
int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
}
//调用handler的方法,也就是目标方法
Object returnValue = handler.method.invoke(handler.controller,paramValues);
if(returnValue == null || returnValue instanceof Void){
return;
}
resp.getWriter().write(returnValue.toString());
}
走到最后一步,也就没有什么了,主要是组装参数列表然后直接调用method对象的invoke方法即可。
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//6.调用
try {
doDispatch(req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
对应的测试controller
package com.learn.springmvc.controller;
import com.learn.springmvc.Annotation.SelfAutowired;
import com.learn.springmvc.Annotation.SelfController;
import com.learn.springmvc.Annotation.SelfRequestMapping;
import com.learn.springmvc.Annotation.SelfRequestParam;
import com.learn.springmvc.service.IDemoService;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//虽然,用法一样,但是没有功能
@SelfController
@SelfRequestMapping("/demo")
public class DemoController {
@SelfAutowired
private IDemoService demoService;
@SelfRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@SelfRequestParam("name") String name){
String result = "My name is " + name;
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@SelfRequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp,
@SelfRequestParam("a") Integer a, @SelfRequestParam("b") Integer b){
try {
resp.getWriter().write(a + "+" + b + "=" + (a + b));
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个就是自己编写的Controller,启动之后在浏览器中输入:localhost:8080/demo/add?name=test,可以得到以下结果
这个实例参考了某个大牛的公开课,但是整个代码对理解spring mvc的启动流程还是有帮助的,在这个实例中反射用的非常多,无非就是利用反射获取注解的值,然后利用反射获取方法的参数列表,处理调用参数,整个过程不管如何总结,其实就是文章开头的流程图。每一步对应的就是指定的函数,但是函数中针对request和response都做了处理,显得些许臃肿。代码书写完之后,博客整理的异常凌乱,一下没理清整理的思路,具体源码可以参见如下地址:一个简单的mvc框架源码地址