02.26 不懂IOC?我來手把手教你寫一個簡單的IOC容器和DI

  • <strong>
  • <strong>
  • <strong>
  • <strong>
  • <strong>
  • <strong>

在Spring中IOC是個絕佳的解耦合手段,為了更好的理解我就動手自己寫了一個

預備知識:

註解,反射,集合類,lambda表達式,流式API

不懂IOC?我來手把手教你寫一個簡單的IOC容器和DI

IOC

如何把一個類註冊進去呢?首先我們要讓容器“發現”它,所以使用註解,聲明它應當加入容器

其中的value即對應的是Spring中的Bean name

<code>@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface Part {    String value() default "";}/<code>

掃描包生成類的工具

當然,有人會說hutool的ClassScaner很好用,但是這裡為了加深理解,我就自己寫一個

思路就是利用文件名利用Class.forName()得到類的反射再生成實例

<code>public static List<object> find(String packName, ClassFilter classFilter) throws IOException {        //獲取當前路徑        Enumeration entity = Thread.currentThread().getContextClassLoader().getResources(packName);        HashSet<string> classPaths = new HashSet<>();        ArrayList<object> classes = new ArrayList<>();        //拿到處理後的路徑,處理前為/..../target/classes        //處理後為/..../target/classes        if (entity.hasMoreElements()) {            String path = entity.nextElement().getPath().substring(1);            classPaths.add(path);        }        //這裡跳轉到我寫的一個把路徑下的.class文件生成為類名的方法,後面會講述        //set的元素為類名 比如Entity.Student        Set<string> set = loadClassName(classPaths);        for (String s : set) {            try {                Class> c = Class.forName(s);                //利用過濾器判斷需不需要生成實例                if (classFilter.test(c)){                    //這裡為了簡單使用無參構造器                    Constructor> constructor = c.getConstructor();                    constructor.setAccessible(true);                    //將生成的實例加入返回的list集合中                    classes.add(constructor.newInstance());                }            }catch (ClassNotFoundException| InstantiationException | IllegalAccessException| InvocationTargetException e) {                throw new RuntimeException(e);            }catch (NoSuchMethodException e){                System.err.println(e.getMessage());            }        }        return classes;    }/<string>/<object>/<string>/<object>/<code>

到來其中的一個核心函數loadClassName

<code>/**     * @param classPaths 路徑名集合     * @return 類名的集合     */    private static Set<string> loadClassName(HashSet<string> classPaths){        Queue<file> queue = new LinkedList<>();        HashSet<string> classNames = new HashSet<>();        //對每一個路徑得到對應所有以.class結尾的文件        classPaths.forEach(p -> {            //迭代的方法,樹的層次遍歷            queue.offer(new File(p));            while (!queue.isEmpty()){                File file = queue.poll();                if (file.isDirectory()) {                    File[] files = file.listFiles();                    for (File file1 : files) {                        queue.offer(file1);                    }                }else if(file.getName().endsWith(".class")){                    //對文件名處理得到類名                    // ..../target/classes處理完為  \\....\\target\\classes                    String replace = p.replace("/", "\\\");                    //對於每個.class文件都是以....\\target\\classes開頭,去掉開頭,去掉後綴就是類名了                   String className = file.getPath()                            .replace(replace, "")                            .replace(".class", "").replace("\\\", ".");                    classNames.add(className);                }            }        });        return classNames;    }/<string>/<file>/<string>/<string>/<code> 

好了,現在就可以掃描包了

上面我也提到了不是所有的類都必須放到容器中,現在讓我們看看這個 ClassFilter 過濾器是什麼東西吧

<code>@FunctionalInterfacepublic interface ClassFilter{    boolean test(Class c);}/<code>

是個函數式接口,這就意味著使用lambda表達式會很方便

通過這個接口我們就很容易地構造這麼一個函數幫我們把所有有@Part註解的類生成好

<code>public static List<object> findByAnnotation(String packName, Class annotation) throws IOException{        if (!annotation.isAnnotation()) {            throw new RuntimeException("it not an annotation"+annotation.getTypeName());        }        ClassFilter classFilter =(c) -> c.getAnnotation(annotation) != null;        return find(packName, classFilter);    }/<object>/<code>

IOC容器

上面的準備工作做的差不多了

該動手寫IOC容器了

思考一下在Spring中我們很容易通過bean name得到java bean,所以使用一個Map<string>可以模擬一下。/<string>

這裡我們在IOCContainer中添加一個變量

<code>private Map<string> context;/<string>/<code>

構造函數

<code>public IOCContainer(String packName){        try {            init(packName);        } catch (IOException e) {            e.printStackTrace();        }    }    public IOCContainer(){        //默認掃描所有的包        this("");    }/<code> 

初始化函數:

<code> /**     * @param packName 路徑名在ClassScannerUtil中的函數要使用     * @throws IOException     * @author dreamlike_ocean     */    public void init(String packName) throws IOException {        //做一個bean name 的映射。如果@Part註解中的值不為空則使用value的值做bean name        //如果為空就用這個 java bean的類名做bean name        Function<object> keyMapper = (o) -> {            Class> aClass = o.getClass();            String s = aClass.getAnnotation(Part.class).value();            if (s.isBlank()) {                return o.getClass().getTypeName();            }            return s;        };        context = new HashMap<string>();        //獲取所有添加@Part註解的類實例        List<object> objectList = ClassScannerUtil.findByAnnotation(packName, Part.class);        //先把自己注入進去        context.put("IOCContainer", this);        for (Object o : objectList) {            //利用上面寫好的映射函數接口 獲取bean name            String beanName = keyMapper.apply(o);            //bean name衝突情況,直接報錯            if (context.containsKey(beanName)) {                String msg = new StringBuilder().append("duplicate bean name: ")                        .append(beanName)                        .append("in")                        .append(o.getClass())                        .append(" and ")                        .append(context.get(beanName).getClass()).toString();                throw new RuntimeException(msg);            }            //加入容器            context.put(beanName, o);        }        //幫助垃圾回收,這個複雜度為O(n),理論上objectList = null也能幫助回收        objectList.clear();    }/<object>/<string>/<object>/<code>

對外暴露的獲取Bean的api

<code>    /**     *      * @param beanName     * @return 記得判斷空指針     * @author dreamlike_ocean     */    public Optional<object> getBean(String beanName){        return Optional.ofNullable(context.get(beanName));    }    /**     *      * @param beanName     * @param aclass     * @param  需要返回的類型,類型強轉     * @exception ClassCastException 類型強轉可能導致無法轉化的異常               * @return @author dreamlike_ocean     */    public Optional getBean(String beanName,Class aclass){        return Optional.ofNullable((T)context.get(beanName));    }    /**     *     * @param interfaceType     * @param      * @return 所有繼承這個接口的集合     * @author dreamlike_ocean     */    public List getBeanByInterfaceType(Class interfaceType){        if (!interfaceType.isInterface()) {            throw new RuntimeException("it is not an interface type:"+interfaceType.getTypeName());        }        return context.values().stream()                .filter(o -> interfaceType.isAssignableFrom(o.getClass()))                .map(o -> (T)o)                .collect(Collectors.toList());    }    /**     *      * @param type     * @param      * @return 所有這個類型的集合     * @author dreamlike_ocean     */        public List getBeanByType(Class type){        return context.values().stream()                .filter(o -> type.isAssignableFrom(o.getClass()))                .map(o -> (T)o)                .collect(Collectors.toList());    }    /**     *      * @return 獲取所有值     * @author dreamlike_ocean      */    public Collection<object> getBeans(){        return context.values();    }    /**     *      * @return 獲取容器     * @author dreamlike_ocean     */    public Map<string> getContext(){        return context;    }/<string>/<object> 
/<object>/<code>

DI

上面我們獲取的都是利用無參的構造函數得到的java bean,這和想的差的有點遠,我想要的是一幅畫,他卻給了我一張白紙。這怎麼能行!DI模塊上,給他整個活!

為了區別通過類型注入還是名稱注入,我寫了兩個註解用於區分

<code>@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface InjectByName {    String value();}@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.FIELD,ElementType.METHOD})public @interface InjectByType {}/<code>

首先DI先必須知道到底對哪個容器注入,所以通過構造函數傳入一個

<code> private IOCContainer iocContainer;    public DI(IOCContainer iocContainer) {        Objects.requireNonNull(iocContainer);        this.iocContainer = iocContainer;    }/<code>

先是對字段的按類型注入

<code>/**     *      * @param o 需要被注入的類     * @author dreamlike_ocean               */    private void InjectFieldByType(Object o){        try {            //獲取內部所有字段            Field[] declaredFields = o.getClass().getDeclaredFields();            for (Field field : declaredFields) {                //判斷當前字段是否有註解標識                if (field.getAnnotation(InjectByType.class) != null) {                    //防止因為private而拋出異常                    field.setAccessible(true);                    List list = iocContainer.getBeanByType(field.getType());                    //如果找不到,那麼注入失敗                    //這裡我選擇拋出異常,也可給他賦值為null                    if(list.size() == 0){                        throw new RuntimeException("not find "+field.getType());                    }                    //多於一個也注入失敗,和Spring一致                    if (list.size()!=1){                        throw new RuntimeException("too many");                    }                    //正常注入                    field.set(o, list.get(0));                }            }        } catch (IllegalAccessException e) {            e.printStackTrace();        }    }/<code>

對字段按名稱注入

<code> /**     *     * @param o 需要被注入的類     * @author dreamlike_ocean     */    private void InjectFieldByName(Object o){        try {            Field[] declaredFields = o.getClass().getDeclaredFields();            for (Field field : declaredFields) {                InjectByName annotation = field.getAnnotation(InjectByName.class);                if (annotation != null) {                    field.setAccessible(true);                    //通過註解中的bean name尋找注入的值                    //這裡optional類沒有發揮它自己的函數式優勢,因為我覺得在lambda表達式裡面寫異常處理屬實不好看                    //借用在Stack overflow看的一句話,Oracle用受檢異常把lambda玩砸了                    Object v = iocContainer.getBean(annotation.value()).get();                    if (v != null) {                        field.set(o, v);                    }else{                        //同樣找不到就拋異常                        throw new RuntimeException("not find "+field.getType());                    }                }            }        } catch (IllegalAccessException e) {            e.printStackTrace();        }    }/<code> 

對函數按類型注入

<code> /**     * 這個函數必須是setter函數     * @param o 要被注入的類     * @author dreamlike_ocean     */    private void InjectMethod(Object o){        Method[] declaredMethods = o.getClass().getDeclaredMethods();        try {            for (Method method : declaredMethods) {                //獲取添加註解的函數                if (method.getAnnotation(InjectByType.class) != null) {                    //獲取參數列表                    Class>[] parameterTypes = method.getParameterTypes();                    method.setAccessible(true);                    int i = method.getParameterCount();                    //為儲存實參做準備                    Object[] param = new Object[i];                    //變量重用,現在它代表當前下標了                    i=0;                    for (Class> parameterType : parameterTypes) {                        List> list = iocContainer.getBeanByType(parameterType);                        if(list.size() == 0){                            throw new RuntimeException("not find "+parameterType+"。method :"+method+"class:"+o.getClass());                        }                        if (list.size()!=1){                            throw new RuntimeException("too many");                        }                        //暫時存儲實參                        param[i++] = list.get(0);                    }                    //調用對應實例的函數                    method.invoke(o, param);                }            }        } catch (IllegalAccessException e) {            e.printStackTrace();        } catch (InvocationTargetException e) {            e.printStackTrace();        }    }/<code>

你會發現上面都是私有方法,因為我想對外暴露一個簡潔的API

<code>  /**     * 對字段依次進行按類型注入和按名稱注入     * 再對setter方法注入     * @author dreamlike_ocean     */    public void inject(){        iocContainer.getBeans().forEach(o -> {            InjectFieldByType(o);            InjectFieldByName(o);            InjectMethod(o);        });    }/<code>

測試

做好了,來讓我們測一測

<code>@Part("testA")class A{    @InjectByType    private B b;    public A(){    }    public B getB() {        return b;    }}@Partclass B{    private UUID uuid;public B(){    uuid = UUID.randomUUID();}    public UUID getUuid() {        return uuid;    }}@Partclass C{    public C(){    }}/<code>

測試方法

<code>@Testpublic void test(){ IOCContainer container = new IOCContainer();       DI di = new DI(container);       di.inject();       System.out.println(container.getBeanByType(A.class).get(0).getB().getUuid());       System.out.println(container.getBeanByType(B.class).get(0).getUuid());}/<code>


不懂IOC?我來手把手教你寫一個簡單的IOC容器和DI

好了這就可以了


作者:dreamlike
原文鏈接:https://juejin.im/post/5e561077518825492c0504fd


分享到:


相關文章: