Spring學習之——手寫Mini版Spring原始碼

我恰芙蓉王發表於2020-07-28

前言

Sping的生態圈已經非常大了,很多時候對Spring的理解都是在會用的階段,想要理解其設計思想卻無從下手。前些天看了某某學院的關於Spring學習的相關視訊,有幾篇講到手寫Spring原始碼,感覺有些地方還是說的挺好的,讓博主對Spring的理解又多了一些,於是在業餘時間也按照視訊講解實現一遍SpringIOC、DI、MVC的設計思想,加深鞏固記憶,也便於初學者理解,我們不用重複造輪子,但得知道輪子怎麼造。

 

開發工具

環境:jdk8 + IDEA + maven

jar包:javax.servlet-2.5

 

實現步驟

視訊中這張圖畫得很好,我們按照這張圖來概括一下實現步驟

1.配置階段:即配置web.xml和application.properties,以及相關自定義註解;

2.初始化階段:初始化Ioc容器,將掃描到的類例項化交給IoC容器管理,然後注入例項的各屬性,接著將請求路徑與執行方法的對映載入到HandlerMapping中;

3.執行階段:由HandlerMapping根據請求路徑將請求分發到指定方法,返回執行結果。

 

附上自己的專案結構供參考

 

 

 

 

具體實現

配置階段

application.properties  配置掃描路徑,這裡為了方便,所以沒有使用xml檔案去解析

scanPackage=com.wqfrw

 

web.xml  配置Servlet和application檔案的路徑

<servlet>
    <servlet-name>mymvc</servlet-name>
    <servlet-class>com.framework.servlet.MyDispatcherServlet</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>mymvc</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

 

pom.xml 引入servlet.jar

<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>servlet-api</artifactId>
  <version>2.5</version>
</dependency>

 

相關自定義註解

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

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

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

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

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

 

初始化階段

這部分的操作才是最重要的,註釋我都詳細的寫在程式碼中

MyDispatcherServlet

public class MyDispatcherServlet extends HttpServlet {

    //IoC容器
    private Map<String, Object> ioc = new HashMap<>();

    //載入配置檔案物件
    private Properties contextConfig = new Properties();

    //掃描到的類名帶包路徑
    private List<String> classNameList = new ArrayList<>();

    //url與方法的對映 也就是請求分發器
    private Map<String, Method> handlerMapping = new HashMap<>();

    @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) {
            e.printStackTrace();
            resp.getWriter().write("500 Exception Detail:" + Arrays.toString(e.getStackTrace()));
        }
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        //載入配置檔案 拿到需要掃描的包路徑
        doLoadConfig(config.getInitParameter("contextConfigLocation"));

        //掃描相關的類
        doScanner(contextConfig.getProperty("scanPackage"));

        //例項化Bean到IoC容器
        doInstance();

        //依賴注入(DI)
        doAutowired();

        //初始化HandlerMapping url和對應方法的鍵值對
        doInitHandlerMapping();

        //初始化完成
        System.out.println("MySpring framework is init!");
    }

    /**
     * 功能描述: 接收到瀏覽器的請求,執行方法
     *
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年07月28日 20:54:04
     * @param req
     * @param resp
     * @return: void
     **/
    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        String url = req.getRequestURI();
        String contextPath = req.getContextPath();
        url = url.replaceAll(contextPath,"");
        
        //根據請求路徑如果沒有找到對應的方法則丟擲404錯誤
        if (!handlerMapping.containsKey(url)) {
            resp.getWriter().write("404 Not Found!");
            return;
        }

        Map<String,String[]> params = req.getParameterMap();
        Method method = handlerMapping.get(url);
        String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
        //方法呼叫
        method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]});
    }

    /**
     * 功能描述: 初始化HandlerMapping
     *
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年07月28日 20:56:09
     * @param
     * @return: void
     **/
    private void doInitHandlerMapping() {
        ioc.forEach((k, v) -> {
            Class<?> clazz = v.getClass();

            //加了MyController註解的類才操作
            if (clazz.isAnnotationPresent(MyController.class)) {

                String baseUrl = "";
                //如果類上面加了MyRequestMapping註解,則需要拿到url進行拼接
                if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
                    MyRequestMapping annotation = clazz.getAnnotation(MyRequestMapping.class);
                    baseUrl = annotation.value();
                }

                //獲取所有public修飾的方法
                Method[] methods = clazz.getMethods();
                //過濾拿到所有MyRequestMapping註解的方法,put到handlerMapping中
                String finalBaseUrl = baseUrl;
                Stream.of(methods)
                        .filter(m -> m.isAnnotationPresent(MyRequestMapping.class))
                        .forEach(m -> {
                            MyRequestMapping annotation = m.getAnnotation(MyRequestMapping.class);
                            String url = (finalBaseUrl + annotation.value()).replaceAll("/+", "/");
                            handlerMapping.put(url, m);
                        });
            }

        });
    }

    /**
     * 功能描述: 依賴注入(DI)
     *
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年07月28日 20:55:57
     * @param
     * @return: void
     **/
    private void doAutowired() {
        //迴圈IoC容器中所管理的物件 注入屬性
        ioc.forEach((k, v) -> {
            //拿到bean所有的欄位 包括private、public、protected、default
            Field[] Fields = v.getClass().getDeclaredFields();
            //過濾拿到所有加了MyAutowired註解的欄位並迴圈注入
            Stream.of(Fields)
                    .filter(f -> f.isAnnotationPresent(MyAutowired.class))
                    .forEach(f -> {

                        MyAutowired annotation = f.getAnnotation(MyAutowired.class);
                        String beanName = annotation.value().trim();
                        if ("".equals(beanName)) {
                            beanName = f.getType().getName();
                        }

                        //強制訪問
                        f.setAccessible(true);

                        try {
                            //賦值
                            f.set(v, ioc.get(beanName));
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    });
        });
    }

    /**
     * 功能描述: 例項化bean至IoC容器
     *
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年07月28日 20:55:39
     * @param
     * @return: void
     **/
    private void doInstance() {
        classNameList.forEach(v -> {
            try {
                Class<?> clazz = Class.forName(v);

                //只初始化加了MyController註解和MyService註解的類
                if (clazz.isAnnotationPresent(MyController.class)) {
                    String beanName = toLowerFirstCase(clazz.getSimpleName());
                    ioc.put(beanName, clazz.newInstance());
                } else if (clazz.isAnnotationPresent(MyService.class)) {
                    // 1.預設首字母小寫
                    String beanName = toLowerFirstCase(clazz.getSimpleName());

                    // 2.自定義beanName
                    MyService myService = clazz.getAnnotation(MyService.class);
                    if (!"".equals(myService.value())) {
                        beanName = myService.value();
                    }

                    // 3.如果是介面 必須建立實現類的例項
                    for (Class<?> i : clazz.getInterfaces()) {
                        if (ioc.containsKey(i)) {
                            throw new Exception("This beanName is exists!");
                        }
                        beanName = i.getName();
                    }
                    //將例項化的物件放入IoC容器中
                    ioc.put(beanName, clazz.newInstance());
                }

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

    /**
     * 功能描述: 掃描相關的類 加入到classNameList
     *
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年07月28日 20:55:10
     * @param scanPackage
     * @return: void
     **/
    private void doScanner(String scanPackage) {
        //獲取根目錄  拿到com.wqfry替換成/com/wqfrw
        URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
        File classFile = new File(url.getFile());
        for (File file : classFile.listFiles()) {
            //如果file是資料夾  則遞迴呼叫
            if (file.isDirectory()) {
                doScanner(scanPackage + "." + file.getName());
            } else {
                //如果非class檔案 則跳過
                if (!file.getName().endsWith(".class")) {
                    continue;
                }
                String className = (scanPackage + "." + file.getName()).replace(".class", "");
                //類名+包路徑放入到類名集合中  方便後續例項化
                classNameList.add(className);
            }
        }
    }

    /**
     * 功能描述: 載入配置檔案
     *
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年07月28日 20:54:57
     * @param contextConfigLocation
     * @return: void
     **/
    private void doLoadConfig(String contextConfigLocation) {
        InputStream resource = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try {
            //載入配置檔案
            contextConfig.load(resource);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //關閉檔案流
            if (resource != null) {
                try {
                    resource.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 首字母轉小寫
     *
     * @param className
     * @return
     */
    private String toLowerFirstCase(String className) {
        char[] chars = className.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }
}

 

執行階段

部分程式碼在上面,這裡我貼出Controller與Server的程式碼

TestController

@MyController
@MyRequestMapping("/test")
public class TestController {

    @MyAutowired
    private ITestService testService;

    @MyRequestMapping("/query")
    public  void query(HttpServletRequest req, HttpServletResponse resp, @MyRequestParam("name") String name){
        String result = testService.query(name);
        try {
            resp.getWriter().write(result);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @MyRequestMapping("/add")
    public  void add(HttpServletRequest req, HttpServletResponse resp, @MyRequestParam("name") String name){
        System.out.println("add");
    }

    @MyRequestMapping("/remove")
    public  void remove(HttpServletRequest req, HttpServletResponse resp, @MyRequestParam("name") String name){
        System.out.println("remove");
    }

}

 

ITestService

public interface ITestService {

    String query(String name);
    
}

 

TestServiceImpl

@MyService
public class TestServiceImpl implements ITestService {

    @Override
    public String query(String name) {
        return "hello  " + name + "!";
    }
    
}

 

 

實際呼叫

 

 

 

總結

以上只是簡略實現了Spring的核心思想,真正的Spring當然要比此複雜許多,但是學習都是由淺至深的,希望大家不僅會用工具,並且都能知道為什麼要這樣用。

附上原視訊連結:https://live.gupaoedu.com/watch/1284875

相關文章