- <strong>
- <strong>
- <strong>
- <strong>
- <strong>
- <strong>
在Spring中IOC是個絕佳的解耦合手段,為了更好的理解我就動手自己寫了一個
預備知識:
註解,反射,集合類,lambda表達式,流式API
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 { //獲取當前路徑 Enumerationentity = 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 staticList<object> findByAnnotation(String packName, Class /<code>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>
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 /<object>/<code>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>
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>
好了這就可以了
作者:dreamlike
原文鏈接:https://juejin.im/post/5e561077518825492c0504fd
閱讀更多 追逐仰望星空 的文章