手写Spring框架

Posted by Kaka Blog on December 12, 2018

前言

本文通过手写Spring,实现Spring MVC基本功能,为学习Spring源码做准备。

Spring介绍

Spring主要由三个阶段:配置阶段、初始化阶段和运行阶段。

  • 配置阶段:主要完成application.properties和Annotation配置。
  • 初始化阶段:主要加载并解析配置信息,然后初始化IOC容器,完成容器的DI操作,以及完成HandlerMapping的初始化。
  • 运行阶段:主要完成Spring容器启动后,完成用户请求的内部调度,并返回响应结果。

开始

1、新建Java Web项目

项目结构如下: img

提前安装好Tomcat

New Project -> Java Enterprise -> 勾选Web Application

2、配置阶段

1、创建Servlet类,重写init()、doGet()和doPost()方法。

public class JDispatchServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
    }
}

2、在web.xml添加配置。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <servlet>
        <servlet-name>wjmvc</servlet-name>
        <servlet-class>com.fang.JDispatchServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/application.properties</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>wjmvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

<init-param>中配置了一个初始化加载Spring主配置文件路径,application.properties文件放在/WEB-INF/下,内容如下:

scanPackage=com.fang.demo

3、创建Annotation注解。

@JController注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JController {
    String value() default "";
}

@JService注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JService {
    String value() default "";
}

@JAutowired注解:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JAutowired {
    String value() default "";
}

@JRequestMapping注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JRequestMapping {
    String value() default "";
}

@JRequestParam注解:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JRequestParam {
    String value() default "";
}
  • ElementType 这个枚举类型的常量提供了一个简单的分类:注释可能出现在Java程序中的语法位置。
  • RetentionPolicy这个枚举类型的常量描述保留注释的各种策略,它们与元注释(@Retention)一起指定注释要保留多长时间。
  • Documented注解表明这个注释是由 javadoc记录的,在默认情况下也有类似的记录工具。 如果一个类型声明被注释了文档化,它的注释成为公共API的一部分。

4、配置注解

创建com.fang.demo包,这application.properties里的包一致。

IDemoService接口:

public interface IDemoService {
    String sayHello(String name);
}

DemoService实现类:

@JService
public class DemoServiceImpl implements IDemoService {
    public String sayHello(String name) {
        return "Hello, " + name;
    }
}

DemoController控制器:

@JController
public class DemoController {

    @JAutowired
    private IDemoService demoService;

    @JRequestMapping("/say")
    public String say(@JRequestParam("name") String name) {
        return demoService.sayHello(name);
    }
}

至此,配置阶段的代码就都已完成。

3、初始化阶段

1、声明成员变量

// 保存所有被扫描到的相关类  com.fang.demo.DemoController,com.fang.demo.DemoServiceImpl
public List<String> clazzNames = new ArrayList<>();
// 保存所有初始化的Bean demoController new DemoController()
public Map<String, Object> iocMap = new HashMap<>();
// 保存所有的url和Method的映射关系 /say Method
public Map<String, Method> handlerMap = new HashMap<>();
// 和web.xml里init-param的值一致
private static final String LOCATION = "contextConfigLocation";
// 保存配置的所有信息
private Properties p = new Properties();

2、重写init方法,实现拿到主配置文件的路径,读取配置文件中的信息,扫描所有相关的类,初始化相关类的实例并保存到IOC容器,从ICO容器取出对应的实例给字段赋值,即依赖注入,最后将url和Method进行关联。

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    // 1、加载配置文件
    doLoadConfig(config.getInitParameter(LOCATION));
    // 2、扫描所有相关的类
    doScanner(p.getProperty("scanPackage"));
    // 3、初始化所有相关类的实例,并保存到IOC容器中
    doInstance();
    // 4、依赖注入
    doAutowired();
    // 5、构造HandlerMapping
    initHandlerMapping();
    // 6、等待请求,匹配URL,定位方法,反射
    System.out.println("Jun Spring is init");
}

doLoadConfig获取主配置文件路径,读取内容保存到Properties对象中:

private void doLoadConfig(String location) {
    InputStream is = null;
    System.out.println("location = " + location);
    is = getServletContext().getResourceAsStream(location);
    try {
        p.load(is);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (null != is) {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • Class.getResourceAsStream(String path) : path 不以’/’开头时默认是从此类所在的包下取资源,以’/’开头则是从ClassPath根下获取。其只是通过path构造一个绝对路径,最终还是由ClassLoader获取资源。
  • Class.getClassLoader.getResourceAsStream(String path) :默认则是从ClassPath根下获取,path不能以’/’开头,最终是由ClassLoader获取资源。
  • ServletContext. getResourceAsStream(String path):默认从WebAPP根目录下取资源,Tomcat下path是否以’/’开头无所谓,当然这和具体的容器实现有关。

doScanner扫描相关类并保存。

private void doScanner(String packageName) {
    // com.fang.demo -> com/fang/demo
    URL url = this.getClass().getClassLoader().getResource("/" + packageName.replaceAll("\\.", "/"));
    File dir = new File(url.getFile());
    for (File file : dir.listFiles()) {
        if (file.isDirectory()) {
            doScanner(packageName + "." + file.getName());
        }
        else {
            clazzNames.add(packageName + "." + file.getName().replace(".class", "").trim());
        }
    }
}

正则表达式:\.表示除换行符\n之外的任何单字符,\\.表示.

doInstance根据类名实例化,并放到IOC容器中。IOC容器的key默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。因此,要先写一个针对类名首字母处理的工具方法。

private String lowerFirstCase(String simpleName) {
    char[] chars = simpleName.toCharArray();
    chars[0] += 32;
    return String.valueOf(chars);
}
private void doInstance() {
    if (clazzNames.size() == 0) {
        return;
    }
    for (String clazzName : clazzNames) {
        try {
            Class<?> clazz = Class.forName(clazzName);
            if (clazz.isAnnotationPresent(JController.class)) {
                String beanName = lowerFirstCase(clazz.getSimpleName());
                iocMap.put(beanName, clazz.newInstance());
            }
            else if (clazz.isAnnotationPresent(JService.class)) {
                JService service = clazz.getAnnotation(JService.class);
                String beanName = service.value();
                if (!"".equals(beanName.trim())) {
                    iocMap.put(beanName, clazz.newInstance());
                    continue;
                }
                Class<?>[] interfaces = clazz.getInterfaces();
                for (Class<?> i : interfaces) {
                    iocMap.put(lowerFirstCase(i.getSimpleName()), clazz.newInstance());
                }
            }
            else {
                continue;
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}
  • Class.forName():查找并加载指定的类,返回Class对象。
  • Class.getName():获取包名+类名。
  • Class.getSimpleName():获取类名。

doAutowired将初始化到IOC容器中的类赋值给有@JAutowired的字段。

private void doAutowired() {
    if (iocMap.isEmpty()) {return;}
    for (Map.Entry<String, Object> entry : iocMap.entrySet()) {
        // 获取所有的属性
        Field[] fields = entry.getValue().getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(JAutowired.class)) {
                JAutowired autowired = field.getAnnotation(JAutowired.class);
                String beanName = autowired.value();
                if ("".equals(beanName)) {
                    beanName = field.getType().getSimpleName();
                    beanName = lowerFirstCase(beanName);
                }
                // 设置可访问
                field.setAccessible(true);
                try {
                    // 给对象的属性赋值
                    field.set(entry.getValue(), iocMap.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • Class.getFields():获取一个类的public成员变量,包括基类。
  • Class.getDeclaredFields():获取一个类的所有成员变量,不包括基类。
  • Field.setAccessible成员变量为private,必须进行此操作。

initHandlerMapping将JRequestMapping中配置的信息和Method进行关联,并保存这些关系。

private void initHandlerMapping() {
    if (iocMap.isEmpty()) {
        return;
    }
    for (Map.Entry<String, Object> entry : iocMap.entrySet()) {
        Class<?> clazz = entry.getValue().getClass();
        if (!clazz.isAnnotationPresent(JController.class)) {continue;}
        StringBuilder baseUrl = new StringBuilder("/");
        if (clazz.isAnnotationPresent(JRequestMapping.class)) {
            JRequestMapping requestMapping = clazz.getAnnotation(JRequestMapping.class);
            baseUrl.append(requestMapping.value());
        }
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(JRequestMapping.class)) {
                JRequestMapping requestMapping = method.getAnnotation(JRequestMapping.class);
                baseUrl.append("/" + requestMapping.value());
                String url = baseUrl.toString().replaceAll("/+", "/");
                handlerMap.put(url, method);
                System.out.println("url:" + baseUrl + "," + method);
            }
        }
    }
}

至此,初始化阶段的代码都已完成。

4、运行阶段

在doGet方法调用doPost方法,在doPost方法中再调用doDispach()方法。

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    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: "+ Arrays.toString(e.getStackTrace()));
    }
}

doDispatch处理多个//,url请求不存在的返回404,通过url查找到对应的方法进行调用并返回。

private void doDispatch(HttpServletRequest req, HttpServletResponse resp)  throws Exception{
    if (handlerMap.isEmpty()) {return;}
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replace(contextPath, "").replaceAll("/+", "/");
    if (!handlerMap.containsKey(url)) {
        resp.getWriter().write("404 Not Founded");
        return;
    }
    Map<String, String[]> params = req.getParameterMap();
    Method method = handlerMap.get(url);
    Class<?>[] parameterTypes = method.getParameterTypes();
    Object[] paramValues = new Object[parameterTypes.length];
    for (int i = 0; i < parameterTypes.length; i++) {
        Class<?> paramType = parameterTypes[i];
        if (paramType == HttpServletRequest.class) {
            paramValues[i] = req;
        }
        else if (paramType == HttpServletResponse.class) {
            paramValues[i] = resp;
        }
        else if (paramType == String.class) {
            for (Map.Entry<String, String[]> entry : params.entrySet()) {
                String value = Arrays.toString(entry.getValue())
                        .replaceAll("\\[|\\]", "")
                        .replaceAll("\\s", "");
                paramValues[i] = value;
            }
        }
    }
    String beanName = lowerFirstCase(method.getDeclaringClass().getSimpleName());
    String result = (String) method.invoke(iocMap.get(beanName), paramValues);
    resp.getWriter().write(result);
}

正则表达式:\\[|\\]表示[] \s表示任何空白字符

至此,一个mini版的Spring就完成了,点击启动,在浏览器输入:http://localhost:8080/say?name=fang,正常返回:Hello, fang,输入其它地址则返回404。

Tomcat配置:Run -> Edit Configuration,Run Configuration点击”+” -> Tomcat Server -> Local,创建一个新的Tomcat,选择本地安装的Tomcat目录。 JavaWeb测试:Run -> Edit Configurations,进入“Run Configurations”窗口,选择之前配置好的Tomcat,点击“Deployment”选项卡,点击“+” -> “Artifact”-> 选择创建的web项目的Artifact…

参考

Tom原创——我是这样手写Spring的,麻雀虽小五脏俱全