1. 程式人生 > >聊一個自己寫的MVC框架

聊一個自己寫的MVC框架

xml文件 ast target 實現類 讀取 能說 位置 加載 -i

  也有個一周沒有更新博客了,其實我沒有偷懶,因為之前一直在看Spring源碼,所以想著去寫一個類Spring的框架,也沒有給自己定什麽高的要求,簡單實現MVC、AOP、IOC等功能就行。現在這個框架基本上已經成型了,自己也測試過了,因此拿出來和大家分享一下。

  我本文就不寫標題了,因為自己的思路是跟著代碼走的,所以可能說著說著MVC就跳到DI那一塊了。首先我在開始的時候,也是跟隨著大部分人的思路,先從DispatcherServlet入手。因為為DispatcherServlet是和用戶交互的。和DispatcherServlet交互的有四個模塊,分別是:HandlerMapping、HandlerAdapter、ViewResolver、ModelAndView,我定義了四個類分別對應著這四個模塊,先不說他們是怎麽工作的,先聊一下DispatcherServlet。

技術分享圖片

  它和HandlerMapping先進行交互,然後HandlerMapping處理拿到對象。

private void initHandlerMappings(LusheApplicationContext context) {
        //按照我們通常的理解應該是一個Map,Map<String,Method> map;map.put(url,Method)
        //首先從容器中取到所有的實例
        String [] beanNames = context.getBeanDefinitionNames();
        try {
            
for (String beanName : beanNames) { //到了MVC層,對外提供的方法只有一個getBean方法 Object proxy = context.getBean(beanName); Object originObject = LusheAopProxyUtils.getOriginObject(proxy); Class<?> clazz = originObject.getClass();
if (!clazz.isAnnotationPresent(Controller.class)) { continue; } String baseUrl = ""; if (clazz.isAnnotationPresent(RequestMapping.class)) { RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class); baseUrl = requestMapping.value(); } //掃描所有的public方法 Method[] methods = clazz.getMethods(); for (Method method : methods) { if (!method.isAnnotationPresent(RequestMapping.class)) { continue; } RequestMapping requestMapping = method.getAnnotation(RequestMapping.class); String regex = ("/" + baseUrl + requestMapping.value().replaceAll("\\*", ".*")).replaceAll("/+", "/"); Pattern pattern = Pattern.compile(regex); this.handlerMappings.add(new LusheHandlerMapping(pattern, originObject, method)); System.out.println("Mapping: " + regex + " , " + method); } } }catch (Exception e){ e.printStackTrace(); } }

  我是這麽處理的,我只給Servlet獲得實例的接口,然後再讓它來解析這些實例,放到定義好的HandlerMapping數組中。那麽這裏就要考慮給getBean的類了,之前在Spring中他們是在一個終極父類裏定義到了這個方法,我的處理是簡化,直接定義一個ApplicationContext,把它當做BeanFactory,所有的處理都在這裏面進行,對外給出getBean方法。那麽來看一下ApplicationContext這個類。按照Spring的思想,首先是定義一個refresh方法,它的作用就是:根據XML文件定義的屬性來進行IOC容器的初始化。

public void refresh(){
        //定位
        this.reader = new BeanDefinitionReader(configureLocations);
        //加載
        List<String> beanDefinitions = this.reader.loadBeanDefinitions();
        //註冊
        doRegisty(beanDefinitions);
        //依賴註入(lazy-init = false),自動調用getBean方法
        doDependencyInjection();
    }

  我按照定位、加載、註冊和依賴註入四部分來處理。首先是定位,就是對配置文件進行定位讀取解析,這裏自己定義了一個BeanDefinitionReader的類,把配置文件存放在它內部一個Properties變量中。並且把配置文件中劃好的包中的類給拿出來,

private void doScanner(String packName) {
        URL url = this.getClass().getClassLoader().getResource("/" + packName.replaceAll("\\.","/"));

        File classDir = new File(url.getFile());

        for(File file : classDir.listFiles()) {
            if(file.isDirectory()) {
                doScanner(packName + "." + file.getName());
            } else {
                registyBeanClasses.add(packName + "." + file.getName().replace(".class",""));
            }
        }
    }

  然後是加載,在BeanDefinitionReader裏,我將屬性和類名做成一個Map,存放起來。之後就是註冊,還是在BeanDefinitionReader中,因為我之前已經將屬性和名稱做成了一個Map,這樣我可以通過類名就能得到它對應的BeanDefinition。

public LusheBeanDefinition registerBean(String className) {
        if(this.registyBeanClasses.contains(className)) {
            LusheBeanDefinition lusheBeanDefinition = new LusheBeanDefinition();
            lusheBeanDefinition.setBeanClassName(className);
            lusheBeanDefinition.setFactoryBeanName(StringUtils.lowerFirstCase(className.substring(className.lastIndexOf(".") + 1)));
            return lusheBeanDefinition;
        }
        return null;
    }

  回到ApplicationContext的refresh方法,把我們所有定義好的BeanDefinition放到BeanDefinitionMap中,也就是Spring的Cache裏。因為之前只是將所有的BeanDefinition放進去,還沒有做進一步的處理,因此就在這一塊進行處理了。

private void doRegisty(List<String> beanDefinitions){
        try {
            for(String className : beanDefinitions) {
                Class<?> beanClass = Class.forName(className);

                //判斷是不是接口,如果是接口,則用其實現類來實現
                if(beanClass.isInterface()) {
                    continue;
                }
                //beanName 為類名首字母小寫
                //自定義beanName
                LusheBeanDefinition lusheBeanDefinition = this.reader.registerBean(className);
                if(lusheBeanDefinition != null) {
                    beanDefinitionMap.put(lusheBeanDefinition.getFactoryBeanName(), lusheBeanDefinition);
                }
                //接口註入
                Class<?>[] interfaces = beanClass.getInterfaces();
                for(Class<?> interf : interfaces) {
                    //多個接口功能相同
                    //如果多個屬性類只能覆蓋,因為Spring沒有那麽智能
                    this.beanDefinitionMap.put(interf.getName(), lusheBeanDefinition);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  首先看看是不是個接口,如果是,就直接跳過,因為我會在後面給每個類的每個接口賦予相同的BeanDefinition,這裏的確有一點瑕疵,但是我也在思考更好地處理辦法。最後我得到的BeanDefinitionMap,這裏註冊的功能就完成了。

  然後就是依賴註入這塊,我是這麽做的:

private void populateBean(String beanName, Object instance) {
        Class clazz = instance.getClass();
        if(clazz.isAnnotationPresent(Controller.class) || clazz.isAnnotationPresent(Service.class)) {

            //讀取所有字段
            Field[] fields = clazz.getDeclaredFields();
            for(Field field : fields) {
                if(field.isAnnotationPresent(AutoWried.class)) {
                    AutoWried autoWried = field.getAnnotation(AutoWried.class);
                    String autoWriedBeanName = autoWried.value().trim();

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

                    field.setAccessible(true);

                    try {
                        field.set(instance,this.beanWrapperMap.get(autoWriedBeanName).getWrapperInstance());
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                } else {
                    continue;
                }
            }
        } else {
            return;
        }
    }

  先不說這個,因為這裏我們還沒有得到Instance,我是通過反射獲得的這個實例

private Object instantionBean(LusheBeanDefinition lusheBeanDefinition) {
        String className = lusheBeanDefinition.getBeanClassName();
        Object beanInstance = null;
        try {
            synchronized (this) {
                if (!beanCacheMap.containsKey(className)) {
                    Class<?> clazz = Class.forName(className);
                    beanInstance = clazz.newInstance();
                    beanCacheMap.put(className, beanInstance);
                } else {
                    beanInstance = beanCacheMap.get(className);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return beanInstance;
    }

  然後再看一下依賴註入這一塊,也就是populateBean,會傳入一個bean的實例並拿到它的類,如果這個類已經聲明為Controller或者Service類,那麽就把它所有的字段拿出來,如果某個字段被標註為AutoWried,那麽就從包裝Map中把實例給拿出來,具體的實現就是這樣的。

  上面提到包裝類BeanWrapper,它的作用就是方便對其進行功能拓展,比如說增加AOP切點之類的。我是這麽構造一個包裝類實例的:

public LusheBeanWrapper(Object Instance) {
        //添加動態代理
        this.wrapperInstance = aopProxy.getProxy(Instance);

        //原始對象
        this.originalInstance = Instance;
    }

我包裝類的構造函數是傳入一個原始類實例,然後並將這個實例保存起來,然後通過AOP模塊的動態代理拿到包裝類實例。

//這裏傳入原始對象,Spring中傳入的事ClassLoader
    public Object getProxy(Object object) {
        this.target = object;
        Class<?> clazz = this.target.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(), this);
    }

  Spring中在Aop動態代理創建的時候傳入的是ClassLoader,而我這裏為了簡便直觀,傳入的是原始類實例,然後拿到這個實例的類,並調用Proxy的newProxyInstance方法獲得這個被代理對象。接下來就是討論AOP是如何實現的,也就是如何對原始方法進行增強的。下面是我AopProxy類中invoke方法,它的作用就是對被代理對象切入點方法進行增強。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //之前
        if(aopConfigure.contains(method)) {
            //對於某個方法,看看是不是切入點
            LusheAopConfigure.LusheAspect aspect = aopConfigure.get(method);

            aspect.getPoints()[0].invoke(aspect.getAspect(),args);
        }

        //反射調用原始方法
        Object obj = method.invoke(this.target, args);

        //之後
        if(aopConfigure.contains(method)) {
            LusheAopConfigure.LusheAspect aspect = aopConfigure.get(method);

            aspect.getPoints()[1].invoke(aspect.getAspect(),args);
        }

        //可以考慮一下為什麽這麽寫
        //這裏面用到了循環
        return obj;
    }

  首先來解釋一下AopConfigure這個類的作用,它是對ApplicationContext中expression的封裝,對目標代理對象的一個方法增強,增強的方法就是自己定義邏輯。配置文件告訴我們哪些類哪些方法需要增強和需要增強的內容,我用一個Map去存放這些信息,key是目標對象的方法名,value是我自己定義的一個實體Bean,存放的用來增強的類和它的方法(這裏我只是簡單的定義了兩個:before和after)。之後再回到上面的那個invoke方法,這裏是這麽處理的,首先看一下這個方法是不是一個需要增強的方法,如果是,那麽先搞定before方法增強,然後這個方法的執行,最後執行方法後增強。

  最後再回到getBean,當我們使用到這個方法的時候,默認已經完成了IOC容器的初始化和AOP切面方法增強,最後getBean返回給DispatcherServlet的,並不是原始的BeanInstance,而是一個BeanWrapper的實例,換句話說,getBean的作用就是傳入一個Bean的類名,然後讀取BeanDefinition的信息,通過反射機制創建實例並返回。在這個框架中,用到了Spring中用到的思想,它用BeanMapper把原始Bean包裝起來(具體怎麽包裝我在上面已經說了),然後作用是不僅保存了之前的OP關系,而且方便了之後的拓展和增強。

public Object getBean(String beanName) {
        LusheBeanPostProcessor lusheBeanPostProcessor = new LusheBeanPostProcessor();

        LusheBeanDefinition lusheBeanDefinition = beanDefinitionMap.get(beanName);
        Object beanInstance = instantionBean(lusheBeanDefinition);
        lusheBeanPostProcessor.postProcessBeforeInitialization(beanInstance,beanName);

        if(beanInstance == null) {
            return null;
        }
        LusheBeanWrapper lusheBeanWrapper = new LusheBeanWrapper(beanInstance);

        //生成通知事件

        try {
            lusheBeanWrapper.setAopConfigure(instantionAopConfigure(lusheBeanDefinition));
        } catch (Exception e) {
            e.printStackTrace();
        }

        lusheBeanWrapper.setLusheBeanPostProcessor(lusheBeanPostProcessor);

        beanWrapperMap.put(beanName, lusheBeanWrapper);

        lusheBeanPostProcessor.postProcessAfterInitialization(beanInstance,beanName);

        //populateBean(beanName,beanInstance);

        return beanWrapperMap.get(beanName).getWrapperInstance();
    }

  首先從已經初始化好的BeanDefinitionMap中拿到BeanDefinition,然後調用instantionBean方法拿到實例Bean(這個方法的介紹在上面),如果這個實例存在,那我們就對它進行包裝並進行AOP方法切入點的增強(前提是有),並將生成的包裝實例放在Map中保存,並返回包裝器中的包裝實例,可以肯定的說,實例的類不是原始類,而是一個動態代理生成的類,類似以$Proxy0這種。

  回到DispatcherServlet的initHandlerMapping方法:

private void initHandlerMappings(LusheApplicationContext context) {
        //按照我們通常的理解應該是一個Map,Map<String,Method> map;map.put(url,Method)
        //首先從容器中取到所有的實例
        String [] beanNames = context.getBeanDefinitionNames();
        try {
            for (String beanName : beanNames) {
                //到了MVC層,對外提供的方法只有一個getBean方法
                Object proxy = context.getBean(beanName);
                Object originObject = LusheAopProxyUtils.getOriginObject(proxy);

                Class<?> clazz = originObject.getClass();

                if (!clazz.isAnnotationPresent(Controller.class)) {
                    continue;
                }

                String baseUrl = "";

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

                //掃描所有的public方法
                Method[] methods = clazz.getMethods();
                for (Method method : methods) {
                    if (!method.isAnnotationPresent(RequestMapping.class)) {
                        continue;
                    }

                    RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                    String regex = ("/" + baseUrl + requestMapping.value().replaceAll("\\*", ".*")).replaceAll("/+", "/");
                    Pattern pattern = Pattern.compile(regex);
                    this.handlerMappings.add(new LusheHandlerMapping(pattern, originObject, method));
                    System.out.println("Mapping: " + regex + " , " + method);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

  我們通過getBean拿到實例,然後拿到他的原始類實例,我是這麽處理的:

 /**
     * 找到代理對象的原始對象
     * @param proxy
     * @return
     * @throws Exception
     */
    public static Object getOriginObject(Object proxy) throws Exception {
        //不是代理對象
        if(!isProxy(proxy)) {
            return proxy;
        } else {
            return getProxyTargetObject(proxy);
        }
    }

    private static boolean isProxy(Object object) {
        return Proxy.isProxyClass(object.getClass());
    }

    private static Object getProxyTargetObject(Object proxy) throws Exception {

        //在代理模式中看到過,原始類的信息就被存放在類型為InvocationHandler的變量中,名字就叫h
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
        h.setAccessible(true);
        LusheAopProxy  aopProxy = (LusheAopProxy)h.get(proxy);
        Field target = aopProxy.getClass().getDeclaredField("target");
        target.setAccessible(true);
        return target.get(proxy);
    }

  因為在動態代理的過程中,原始類的信息就已經被存放在InvocationHandler中了,所以就很方便的拿到了。

  再回來,我首先判斷一下這個類是不是Controller,不是就走了,是的話,再看一下它的方法中,哪個是RequestMapping方法,將他存放起來。

  然後是調用initHandlerAdapters方法,完成存放的方法的參數的處理。

  最後再說一下類是怎麽和AOP配置匹配起來的,這裏我建立了一個instantionAopConfigure方法,在建立BeanWrapper的時候調用:

private LusheAopConfigure instantionAopConfigure(LusheBeanDefinition beanDefinition) throws Exception{
        LusheAopConfigure aopConfigure = new LusheAopConfigure();
        String expression = reader.getConfigure().getProperty("pointCut");

        String[] before = reader.getConfigure().getProperty("aspectBefore").split("\\#");
        String[] after = reader.getConfigure().getProperty("aspectAfter").split("\\#");

        String className = beanDefinition.getBeanClassName();

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

        //解析成正則去匹配
        Pattern pattern = Pattern.compile(expression);

        //指出切面類,因為我們在上面已經將切面方法的位置分成了兩部分,第一部分是包類名,第二部分是方法名
        //因此我們這裏就取第一部分就可以了,然後因為兩個切面方法屬於一個類,為了方便,就拿第一個就可以了。
        Class<?> aspectClass = Class.forName(before[0]);

        //拿出被代理類的所有方法
        //舉例:com.spring.test.LusheMVC.framework.context.LusheApplication.getBean(java.lang.String)
        //而我們的正則是:
             //public.* com\.spring\.test\.LusheMVC\.demo\.service\..*ServiceImpl\..*\(.*\)
        //這個肯定匹配不上 而
             //com.spring.test.LusheMVC.demo.service.impl.DemoServiceImpl.get(java.lang.String)
        //這個就匹配上了
        for(Method method : clazz.getMethods()) {
            Matcher matcher = pattern.matcher(method.toString());

            //如果匹配上了
            if(matcher.matches()) {
                //把能滿足切面規則的類添加到AOP配置中
                aopConfigure.put(method,clazz.newInstance()
                        ,new Method[]{aspectClass.getMethod(before[1]),aspectClass.getMethod(after[1])});
            }
        }

        return aopConfigure;
    }

  首先拿到切點類的定義,用來做正則判斷,格式類似於public.* com\.spring\.test\.LusheMVC\.demo\.service\..*ServiceImpl\..*\(.*\),然後拿出增強內容方法的信息(就是before和after的信息),並將它們處理到String數組裏,第一個位置放的是類名,第二個是方法名。然後將BeanDefinition中存放的類名拿出來生成這個類的Class,將起所有方法拿出來進行正則匹配,如果匹配到了,說明這個方法是個切入點,需要被增強,那麽就將這個信息存起來,處理完所有的之後,將這些內容給BeanWrapper,它會在AOP的時候用到。

  差不多就說這些吧,如果有補充我會更新的。

聊一個自己寫的MVC框架