手写Spring框架,加深对Spring工作机制的理解!

在我们的日常工作中,经常会用到Spring、Spring Boot、Spring Cloud、Struts、Mybatis、Hibernate等开源框架,有了这些框架的诞生,平时的开发工作量也是变得越来越轻松,我们用 Spring Boot 分分钟可以新建一个Web项目。

记得自己刚开始工作的时候还是在用ServletWeb项目,自己写数据库连接池,用原生JDBC操作数据库,好了不发散了。回到这篇文章的主题,今天通过手写Spring框架,帮大家深入了解一下Spring的工作机制,文中涉及的代码只用来帮助大家理解Spring,不会在线上使用,有不严谨的地方还请大家掠过。

项目结构

file

框架部分实现

  1. 为了区分框架部分代码和业务部分代码,我们将这两部分分别划分在不同的包内 com.mars.democom.mars.framework,以便随后只扫描业务代码。
  2. 这里是自己手写Spring框架,所以不会引入任何Spring项目相关的包。
  3. 由于是一个Web项目,所有我们需要引入 servlet-api 包,仅供编译器使用,所有配置 scopeprovided

新建一个Servlet

首先新建一个 HttpServlet 的实现类 MarsDispatcherServlet,用来接收请求。

public class MarsDispatcherServlet extends HttpServlet {


    @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 {
        //6. 处理请求
    }

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

    }

配置web.xml

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Spring Mvc Education</display-name>

    <servlet>
        <servlet-name>marsmvc</servlet-name>
        <servlet-class>com.mars.framework.servlet.MarsDispatcherServlet</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>marsmvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>
  1. 首先配置了一个 servlet, 名字是 marsmvc, 类全路径是 com.mars.framework.servlet.MarsDispatcherServlet
  2. 设置了初始化参数名和值(这里的值是整个项目的配置文件)。
  3. 配置 load-on-startup, 标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法)。
  4. 配置 servlet-mapping, 将所有请求转发到这个servlet处理。

配置application.properties

scanPackage=com.mars.demo

这个比较好理解,仅配置了一项内容,意思是要扫描的包,随后我们会获取这个值去加载容器。

定义我们常用的注解

  1. MarsAutowired
  2. MarsController
  3. MarsRequestMapping
  4. MarsRequestParam
  5. MarsService

这里仅列举两个,其他都大同小异,需要源码的可以去我的代码仓库fork。

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

充实Servlet功能

先列出框架在初始化的时候都要做那些事情

  1. 加载配置文件
  2. 扫描所有相关联的类
  3. 初始化所有相关联的类,并且将其保存在IOC容器里面
  4. 执行依赖注入(把加了@Autowired注解的字段赋值)
  5. 构造HandlerMapping,将URL和Method进行关联

接下来我们一步步完成上面的操作

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("===================");
        //1.加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
        
        //2.扫描所有相关联的类
        doScanner(contextConfig.getProperty("scanPackage"));
        
        //3.初始化所有相关联的类,并且将其保存在IOC容器里面
        doInstance();
        
        //4.执行依赖注入(把加了@Autowired注解的字段赋值)
        doAutowired();

        //Spring 和核心功能已经完成 IOC、DI
        
        //5.构造HandlerMapping,将URL和Method进行关联
        initHandlerMapping();

        System.out.println("Mars MVC framework initialized");

    }

加载配置文件

    private Properties contextConfig = new Properties();

    private void doLoadConfig(String location) {
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(location);

        try {
            contextConfig.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

扫描所有相关联的类

    private void doScanner(String basePackage) {
        //获取要扫描包的url
        URL url =  this.getClass().getClassLoader().getResource("/" + basePackage.replaceAll("\\.", "/"));

        File dir = new File(url.getFile());
        //遍历包下面所有文件
        for(File file: dir.listFiles()) {
            if(file.isDirectory()){
                //递归扫描
                doScanner(basePackage + "." + file.getName());
            } else {
                String className = basePackage + "." + file.getName().replace(".class", "");

                classNames.add(className);

                System.out.println(className);
            }
        }

    }

初始化所有相关联的类,并且将其保存在IOC容器里面

private void doInstance() {

        if(classNames.isEmpty()) return;

        for(String className: classNames) {

            try {
                Class<?> clazz = Class.forName(className);


                if(clazz.isAnnotationPresent(MarsController.class)) {

                    Object instance = clazz.newInstance();
                    String beanName = lowerFirstCase(clazz.getSimpleName());
                    ioc.put(beanName, instance);

                } else if (clazz.isAnnotationPresent(MarsService.class)) {

                    MarsService service = clazz.getAnnotation(MarsService.class);

                    //2.优先使用自定义命名
                    String beanName = service.value();

                    if("".equals(beanName.trim())) {
                        //1.默认使用类名首字母小写
                        beanName = lowerFirstCase(clazz.getSimpleName());
                    }

                    Object instance = clazz.newInstance();

                    ioc.put(beanName, instance);

                    //3.自动类型匹配(例如:将实现类赋值给接口)

                    Class<?> [] interfaces = clazz.getInterfaces();

                    for(Class<?> inter: interfaces) {
                        ioc.put(inter.getName(), instance);
                    }

                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    //利用ASCII码的差值
    private String lowerFirstCase(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

执行依赖注入(把加了@Autowired注解的字段赋值)

private void doAutowired() {

        if(ioc.isEmpty()) return;

        for(Map.Entry<String, Object> entry: ioc.entrySet()) {
            //注入的意思就是把所有的IOC容器中加了@Autowired注解的字段赋值
            //包含私有字段
            Field[] fields = entry.getValue().getClass().getDeclaredFields();

            for(Field field : fields) {

                //判断是否加了@Autowired注解
                if(!field.isAnnotationPresent(MarsAutowired.class)) continue;

                MarsAutowired autowired = field.getAnnotation(MarsAutowired.class);

                String beanName = autowired.value();

                if("".equals(beanName)) {
                    beanName = field.getType().getName();
                }

                //如果这个字段是私有字段的话,那么要强制访问
                field.setAccessible(true);
                try {
                    field.set(entry.getValue(), ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

构造HandlerMapping,将URL和Method进行关联

private void initHandlerMapping() {
        if(ioc.isEmpty()) return;

        for(Map.Entry<String, Object> entry : ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();

            if(!clazz.isAnnotationPresent(MarsController.class)) continue;

            String baseUrl = "";

            if(clazz.isAnnotationPresent(MarsRequestMapping.class)) {
                MarsRequestMapping requestMapping = clazz.getAnnotation(MarsRequestMapping.class);
                baseUrl = requestMapping.value();
            }

            Method[] methods = clazz.getMethods();

            for(Method method : methods) {

                if(!method.isAnnotationPresent(MarsRequestMapping.class)) continue;

                MarsRequestMapping requestMapping = method.getAnnotation(MarsRequestMapping.class);

                String regex = requestMapping.value();

                regex = (baseUrl + regex).replaceAll("/+", "/");

                Pattern pattern = Pattern.compile(regex);
                handlerMapping.add(new Handler(entry.getValue(), method, pattern));

                System.out.println("Mapping: " + regex + "," + method.getName());
            }
        }

    }

编写业务代码

新建一个Controller

@MarsController
@MarsRequestMapping("/demo")
public class DemoApi {

    @MarsAutowired
    private DemoService demoService;

    @MarsRequestMapping("/query")
    public void query(HttpServletRequest req,
                      HttpServletResponse resp,
                      @MarsRequestParam("name") String name) {
        System.out.println("name: " + name);
        String result = demoService.get(name);

        try{
            resp.getWriter().write(result);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @MarsRequestMapping("/add")
    public void add(HttpServletRequest req,
                    HttpServletResponse resp,
                    @MarsRequestParam("a") Integer a,
                    @MarsRequestParam("b") Integer b) {
        try {
            resp.getWriter().write(String.format("%d+%d=%d", a, b, (a+b)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

提供两个接口,一个通过请求名称返回响应的介绍内容,另一个将请求的两个Integer相加并返回。

创建一个Service

public interface DemoService {
    String get(String name);
}

@MarsService
public class DemoServiceImpl implements DemoService {
    public String get(String name) {
        return String.format("My name is %s.", name);
    }
}

添加Jetty插件

我们的项目运行在Jetty中,所以添加相关插件以及配置:

<plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>7.1.6.v20100715</version>
    <configuration>
        <stopPort>9988</stopPort>
        <stopKey>foo</stopKey>
        <scanIntervalSeconds>5</scanIntervalSeconds>
        <connectors>
            <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
                <port>8080</port>
                <maxIdleTime>60000</maxIdleTime>
            </connector>
        </connectors>
        <webAppConfig>
            <contextPath>/</contextPath>
        </webAppConfig>
    </configuration>
</plugin>

运行

file

点击 jetty:run 运行项目

浏览器访问: http://localhost:8080/demo/query?name=Mars

file

浏览器访问:http://localhost:8080/demo/add?a=10&b=20

file

仓库地址

欢迎访问我的个人博客

关注公众号:JAVA九点半课堂,这里有一批优秀的程序猿,加入我们,一起探讨技术,共同进步!回复“资料”获取 2T 行业最新资料!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容