最簡單IOC容器和DI


最簡單IOC容器和DI /手把手教你


在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 {
//獲取當前路徑

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>@FunctionalInterface
public 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;
}
}
@Part
class B{
private UUID uuid;
public B(){
uuid = UUID.randomUUID();
}

public UUID getUuid() {
return uuid;
}
}
@Part
class C{
public C(){
}

}/<code>

測試方法

<code>@Test
public 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容器和DI /手把手教你

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


分享到:


相關文章: