1. 程式人生 > >如何使用反射和註解實現簡單的依賴注入

如何使用反射和註解實現簡單的依賴注入

Spring Bean 應該是Spring被使用最多的功能 ,通過註解/xml配置 ,可以簡化大量的依賴程式碼 ,同時也提高了效率 .在試用了Guice和閱讀了部分Spring BeanFactory的原始碼後 ,也瞭解了一部分依賴注入的原理 ,自己寫了一個小框架來印證一下.

依賴注入

依賴注入 ,也就是所依賴的物件不由自己處理 ,而是由外部系統去分配 .就像排隊取餐時 ,不是每個人自己去盛菜 ,而是告訴服務員自己需要什麼 ,由服務員準備好給你 .

這樣的好處在於 ,我們不再需要 ‘盛菜’(new) 這個動作 ,只需要告訴 ‘服務員’(DI 容器) 我需要 ‘菜’(依賴的物件) 就可以了 .
對於很多情況下 ,需要的還可能是 ‘扳手’ 這樣可複用的工具 ,那麼我們就可以避免大量的new動作 ,節省很多物件資源 .

如何注入

當我們需要使用某個物件時 ,我們會把它引入到類中 :

class A{
    B object;
    void method(){
        object.do();
    }
}

直接執行的話 ,object是null ,程式異常 .我們需要在執行method方法前把B的例項(instance)放到object這個位置 .

  • constructor - 通過A的構造器傳入 - [必須在A初始化前初始化B]
  • set - 設定一個set方法 - [需要顯式的呼叫set/使用反射呼叫set…]
  • setField - 反射直接設定屬性值

constructor方式需要分析物件依賴的順序 ,如果有環 ,那麼就會死鎖 ; set方法和物件的例項化是分離的 ,如果所依賴的物件還沒有例項 ,就等待它例項化後再set到需要的類中 ;後兩種都是Spring使用過的方法 ,都是使用反射 ;setField不需要生成set方法 ,程式碼看起來會清潔一點 .

這裡寫圖片描述

如上圖 ,按照配置檔案順序載入 ,如果依賴物件未存在則等待 ,依賴物件例項化後立即回填這個依賴 .

Cute Koala

首先 ,為這個框架取了一個可愛的名字 Github - Cute Cute Koala ,然後按照上圖的流程進行設計.

配置檔案 - 使用註解來標識Bean

使用@Module標識的UserModule.class其實就相當於一個配置檔案 ,本身沒有實際的作用 ,註解看起來更加整潔一些.

@Module //標識這是一個模組 ,不同模組的bean互相獨立 
public class UserModule {

  @Koala //標識Bean的註解
HttpService httpService; @Koala(UserServiceImpl.class) //如果Bean是一個介面 ,可以指定其實現 ;同一module內不允許設定不同的實現 UserService userService; @Koala(scope = ScopeEnum.NOSCOPE) //Bean是非單例的 RandomTool randomTool; }

框架程式碼:@Koala註解用來標識Bean的實現(如果宣告是Interface) 和 是否單例

public @interface Koala {
  Class<?> value() default Koala.class;
  ScopeEnum scope() default ScopeEnum.SINGLETON;
  enum ScopeEnum {
    SINGLETON, NOSCOPE
  }
}

通過讀取@Module註解的class檔案 ,就可以分析出裡面所有的Bean

衍生問題

  • 巢狀依賴
    依賴關係一般是多層相互交錯的 ,Bean可能依賴另一個Bean ,分析Module Class只是將最表一層的Bean載入到了容器中 ,但他們裡面還會有依賴關係的存在 . 如下 ,UserService (實現為UserServiceImpl ) 裡面還依賴了其他Bean ,因此我們在掃描Bean的時候需要遞迴掃描 .
public class UserServiceImpl implements UserService {
  @Koala(UserDaoImpl.class)
  UserDao userDao;

  @Koala(scope = ScopeEnum.NOSCOPE)
  RandomTool randomTool;

  public void welcome() {
    System.out.println("Welcome," + userDao.getName());
  }
}

框架程式碼: 掃描@Module中的@Koala標記 ,註冊到當前@Module的Bean容器中 ;掃描Bean中還有沒有@Koala標記 .

class ModuleScanner {
  /**
   * 掃描
   */
  private void scanModule(KoalaModule beanModule) {
    log.info("開始掃描模組:{}", beanModule.getModuleName());
    scanClass(beanModule, beanModule.moduleClass);
  }

  /**
   * 掃描類 ,方便樹型操作
   */
  private void scanClass(KoalaModule beanModule, Class moduleClass) {
    log.info("開始掃描目標類[{}],路徑[{}]", moduleClass.getSimpleName(), moduleClass.getName());
    Arrays.asList(moduleClass.getDeclaredFields())
        .forEach(field -> scanComponent(beanModule, field));
    log.info("掃描類[{}]完畢", moduleClass.getSimpleName());
  }

  /**
   * 掃描需要注入依賴的欄位
   */
  private void scanComponent(KoalaModule beanModule, Field field) {
    if (Objects.isNull(field.getAnnotation(Koala.class))) {
      return;
    }

    Class defineType = field.getType();
    Class implementType = field.getAnnotation(Koala.class).value();
    if (implementType.equals(Koala.class)) {
      implementType = defineType;
    }
    Boolean isSingleton = field.getAnnotation(Koala.class).scope().equals(ScopeEnum.SINGLETON);

    log.info("掃描Bean,宣告型別[{}] ,實現型別[{}],是否單例[{}]", defineType.getName(), implementType.getName(),
        isSingleton);

    beanModule.createBean(defineType, implementType, isSingleton);

    //遞迴掃描子模組
    log.info("開始掃描[{}]欄位的依賴類[{}]", field.getName(), implementType.getSimpleName());
    scanClass(beanModule, implementType);
  }
}

建立Module

框架程式碼: 通過@Module進行配置 ,然後交給框架的中心控制器KoalaFactory進行掃描 ,就會按照配置生成相應KoalaModule類 ,裡面包含Bean容器 ,掃描到的Bean就會進行例項化放入Bean容器 .

public class KoalaFactory {

  @Getter
  private List<Class> moduleList;

  private Map<Class, KoalaModule> moduleMap = new HashMap<>();
  private ModuleScanner scanner = new ModuleScanner();

  //可以切換當前module
  private int index;
  private KoalaModule currentModule;

  public static KoalaFactory of(Class... modules) {
    return new KoalaFactory().build(modules);
  }

  /**
   * 構建
   */
  private KoalaFactory build(Class... moduleClasses) {
    moduleList = Arrays.asList(moduleClasses);
    scanAndBuild(moduleClasses);
    first();//設定第一個註冊module為當前使用的module
  }

  /**
   * 掃描器掃描並生成 module ,預設設定第一個module為當前的操作module
   */
  private void scanAndBuild(Class... moduleClasses) {
    for (Class moduleClass : moduleClasses) {
      scanner.createModule(moduleClass)//掃描module
          .ifPresent(beanModule -> moduleMap.put(moduleClass, beanModule));
    }
  }
}

註冊例項Bean

框架程式碼: 掃描器在掃描到KoalaModule中符合要求的Bean時 ,就會告知KoalaModule註冊這個Bean到容器中.

class ModuleScanner {

  /**
   * 建立一個module
   */
  Optional<KoalaModule> createModule(Class moduleClass) {
    if (!isModule(moduleClass)) {
      return Optional.empty();
    }
    KoalaModule newModule = new KoalaModule(moduleClass);
    scanModule(newModule);
    return Optional.of(newModule);
  }

  /**
   * 檢查註解標記
   */
  private Boolean isModule(Class moduleClass) {
    return !Objects.isNull(moduleClass.getAnnotationsByType(Module.class));
  }
  //本段程式碼在前面有
  private void scanComponent(KoalaModule beanModule, Field field) {
    //省略
    log.info("掃描Bean,宣告型別[{}] ,實現型別[{}],是否單例[{}]",
        defineType.getName(), implementType.getName(),isSingleton);

    beanModule.createBean(defineType, implementType, isSingleton);
    //省略
  }
}

框架程式碼: KoalaModule會建立一個BeanPool ,由這個緩衝池來管理Bean的存取和依賴關係的解決

beanPool.addBean(defineType, implementType, isSingleton);

框架程式碼: 使用BeanWrapper包裝Bean的Class/instance/是否單例等資訊 .

public class BeanWrapper {
  private Class defineType;//宣告型別
  private Class implementType;//例項型別
  private Object instance;//例項物件
  private Boolean singleton;

  public static BeanWrapper of(Class classType, Class implementType, Boolean isSingleton) {
    //new + set
    return beanWrapper;
  }

  /**
   * 根據Bean的條件 建立instance
   * - 單例直接生成物件
   * - 非單例存放class ,在getBean時再例項化
   * - 代理物件(Http RPC ,後續說明)
   */
  void initInstance() {
    try {
      if (!singleton) {
        //非單例 ,object存放實現型別 ,getBean時自動例項化
        instance = implementType;
        return;
      }
      //單例的代理介面 ,生成代理物件
      if (!Objects.isNull(defineType.getAnnotation(HttpKoala.class))) {
        instance = HttpProxyBeanFactory.getProxyInstance(defineType);
        return;
      }
      //單例的Class 或 介面 ,實現類直接例項化
      instance = implementType.newInstance();
    } catch (Exception e) {
      log.error(e.getMessage(), e);
    }
  }
}

框架程式碼: 如果不存在Bean ,則註冊Bean放入緩衝池 ;然後檢查Bean的依賴關係.

public void addBean(Class defineType, Class implementType, Boolean isSingleton) {
    BeanWrapper beanWrapper = beanCache.get(defineType);
    if (Objects.isNull(beanWrapper)) {
      //建立新的bean放入cache
      beanWrapper = BeanWrapper.of(defineType, implementType, isSingleton);
      beanCache.put(beanWrapper);
    } else {
      beanWrapper.checkConflict(implementType, isSingleton);
    }

    if (beanWrapper.getSingleton()) {
      //檢查自身是否依賴其他bean
      checkRelyFrom(beanWrapper, this::waitOrResolveRely);
    } else {
      log.info("非單例Bean ,獲取Bean時再解決依賴");
    }
    //檢查自身是否被其他類依賴
    checkRelyTo(beanWrapper);
  }

衍生問題

  • 依賴解決 ,在BeanPool中設定了2個快取Map ,一個是放置BeanWrapper的Bean容器 ;一個是依賴關係的管理器 .
private final BeanWrapperCache beanCache = new BeanWrapperCache();
//private Map<Class, BeanWrapper> - Class:註冊的Bean
private final BeanRelyCache relyCache = new BeanRelyCache();
//private ListMultimap<Class, BeanWrapper> - Class:List<需要Class例項的其他Bean>

框架程式碼: 檢查是否依賴其他物件 -> 內部有沒有@Koala標記的Field ; 處理依賴 (有則set/無則等待)

public class BeanPool {
  private void checkRelyFrom(BeanWrapper beanWrapper,
      BiConsumer<BeanWrapper, Field> biConsumer) {

    log.info("[{}]正在載入 ,檢查是否依賴其他物件", beanWrapper.getImplementType().getName());
    //檢視其實現類(也就是對應的例項物件)中是否依賴其他Bean
    Arrays.asList(beanWrapper.getImplementType().getDeclaredFields()).forEach(field -> {
      if (!Objects.isNull(field.getAnnotation(Koala.class))) {
        biConsumer.accept(beanWrapper, field);
      }
    });
  }

 private void waitOrResolveRely(BeanWrapper beanWrapper, Field field) {
    //獲取依賴的Bean的實現型別(對應物件的型別)
    Class implementType = field.getAnnotation(Koala.class).value();
    if (implementType.equals(Koala.class)) {
      implementType = field.getType();
    }

    //檢視例項是否已存在
    BeanWrapper oldBeanWrapper = beanCache.get(implementType);
    //檢查依賴Bean是否已載入 ,未載入則等待載入
    if (Objects.isNull(oldBeanWrapper)) {
      //放入關係池 等待依賴物件
      relyCache.put(implementType, beanWrapper);
    } else {
      //發現依賴物件set到自身(非單例快取的是class ,需要在get的時候再set)
      if (beanWrapper.getSingleton()) {
        resolveRely(beanWrapper.getInstance(), field, oldBeanWrapper.getInstance());
      }
    }
  }

  private void resolveRely(Object parent, Field field, Object instance) {
    try {
      log.info("!!!\t\t處理依賴:[{}]已載入到[{}]的[{}]欄位中.", instance.getClass().getSimpleName(),
          parent.getClass().getName(), field.getName());

      field.setAccessible(true);
      field.set(parent, instance);
    } catch (IllegalAccessException e) {
      log.error(e.getMessage(), e);
    }
  }
}

框架程式碼: 檢查是否被其他物件依賴 -> 依賴關係中有沒有依賴列表

/**
   * 獲取到 #classType 型別的物件 ,開始檢索依賴關係並set到對應位置
   */
  private void checkRelyTo(BeanWrapper beanWrapper) {
    log.info("[{}-{}]正在載入,檢查是否被其他物件依賴", beanWrapper.getDefineType(),
        beanWrapper.getImplementType().getName());

    //檢查自身的實現型別是否被其他類需要
    relyCache.get(beanWrapper.getImplementType())
        .forEach(parentBeanWrapper -> {
          Object parent = parentBeanWrapper.getInstance();
          //獲取parent中屬於bean (class一致) 的field ,把bean的instanceset進去
          CollectionTool.dealFirst(//獲取列表的第一個符合要求的元素
              parent.getClass().getDeclaredFields(),
              beanWrapper::matchType,//field的型別與bean一致
              t -> resolveRely(parent, t, beanWrapper.getObjectOfInstance())//resolveRely見上一段程式碼
          );
        });

    relyCache.remove(beanWrapper.getImplementType());
  }

邏輯就和最開始的圖上展現的一樣 ,有則載入無則等待 ;載入的Bean發現被別人依賴 ,會主動set到需要的地方 .

獲取Bean

框架程式碼: 單例的Bean在註冊時已經例項化 ,所以直接取instance即可 ;非單例的需要現場newInstance ,如果他依賴了其他單例的Bean ,再現場set一下 ,這樣就可以得到符合要求的非單例Bean了 .

 public class BeanPool {

  private final BeanWrapperCache beanCache = new BeanWrapperCache();
  private final BeanRelyCache relyCache = new BeanRelyCache();

  /**
   * 獲取對應型別的Bean物件
   */
  public <T> T getBean(Class<T> classType) {
    BeanWrapper scope = beanCache.get(classType);
    if (scope.getSingleton()) {
      log.info("當前Bean是單例,直接返回緩衝池中的物件.");
      return (T) scope.getInstance();
    }
    try {
      log.info("當前Bean是非單例的,建立新的物件.");
      //如果是遠端代理的Bean ,直接生成遠端代理
      if (classType.isInterface() && !Objects.isNull(classType.getAnnotation(HttpKoala.class))) {
        return (T) HttpProxyBeanFactory.getProxyInstance(classType);
      }

      T instance = ((Class<T>) scope.getInstance()).newInstance();
      //建立物件時檢視內部是否有依賴關係 ,有則set到instance裡
      checkRelyFrom(scope, (beanWrapper, field) ->
          resolveRely(instance, field, getBean(field.getType()))
      );
      return instance;
    } catch (Exception e) {
      log.error(e.getMessage(), e);
    }
    return null;
  }

* 一個DI工具就基本成型了 ,測試列印一下 ,巢狀的Bean和容器的快照對比 ,都是正確的 ;單例物件一致 ,非單例為不同物件*

    KoalaFactory beanFactory = KoalaFactory.of(UserModule.class);
    HttpService httpService = beanFactory.getBean(HttpService.class);
    UserService userService = beanFactory.getBean(UserService.class);
    RandomTool randomTool = beanFactory.getBean(RandomTool.class);
通過Factory.getBean
com.koala.services.impl.UserServiceImpl |   com.koala.services.impl.UserServiceImpl@16267862
com.koala.services.impl.UserServiceImpl |   com.koala.services.impl.UserServiceImpl@16267862
com.koala.daos.impl.UserDaoImpl |   com.koala.daos.impl.UserDaoImpl@453da22c
com.koala.daos.impl.UserDaoImpl |   com.koala.daos.impl.UserDaoImpl@453da22c
com.koala.daos.impl.UserDaoImpl |   com.koala.daos.impl.UserDaoImpl@453da22c
com.koala.webservice.HttpService$$EnhancerByCGLIB$$9c1900a1   |   github.koala.webservice.resetful.HttpProxyObject@71248c21
com.koala.utils.RandomTool  |   com.koala.utils.RandomTool@442675e1
com.koala.utils.RandomTool  |   com.koala.utils.RandomTool@6166e06f
容器:
com.koala.utils.RandomTool  |   class com.koala.utils.RandomTool
com.koala.services.impl.UserServiceImpl |   com.koala.services.impl.UserServiceImpl@16267862
com.koala.daos.UserDao  |   com.koala.daos.impl.UserDaoImpl@453da22c
com.koala.services.UserService  |   com.koala.services.impl.UserServiceImpl@16267862
com.koala.webservice.HttpService    |   github.koala.webservice.resetful.HttpProxyObject@71248c21
com.koala.daos.impl.UserDaoImpl |   com.koala.daos.impl.UserDaoImpl@453da22c

其他功能

  • 模板生成 : 娛樂功能 ,讀取yaml配置 生成對應的 Module.java檔案
modules:
- moduleName: user-http
  packageName: com.koala
  beans:
  - beanName: httpService
    path: com.koala.services.HttpService

  - beanName: userService
    path: com.koala.services.UserService
    implPath: com.koala.services.impl.UserServiceImpl

  - beanName: randomTool
    path: com.koala.utils.RandomTool
    scope: no

To

@github.koala.core.annotation.Module
public class UserHttpModule {

    @github.koala.core.annotation.Koala
  HttpService httpService;

    @github.koala.core.annotation.Koala(value = com.koala.services.impl.UserServiceImpl.class)
    com.koala.services.UserService userService;

    @github.koala.core.annotation.Koala(scope = github.koala.core.annotation.Koala.ScopeEnum.NOSCOPE)
    com.koala.utils.RandomTool randomTool;
}
  • Http RPC : 對Restful的Http服務做了一個代理
@HttpKoala("http://localhost:9999/api")
public interface HttpService {

  @HttpKoalaMethod( "/user")
  User getUser(@HttpKoalaParameter("name") String name);

  @HttpKoalaMethod( "/users")
  User[] getUsers(@HttpKoalaParameter("names") String... names);

  @HttpKoalaMethod(value = "/userList", httpMethod = HttpMethod.POST)
  List<User> getUserList(@HttpKoalaParameter("names") List<String> names);

  @HttpKoalaMethod("/userMap")
  Map<String, User> getUserMap(@HttpKoalaParameter("name") String name);
}

框架程式碼: 通過@HttpKoala/@HttpKoalaMethod/@HttpKoalaParameter來確定呼叫的Http URL和方法 ,使用cglib生成了一個代理物件 ,把方法代理成Http呼叫 .

public class HttpProxyObject implements MethodInterceptor {

  /**
   * 代理方法 ,託管給http完成
   *
   * @param o 當前的物件
   * @param method 呼叫的目標方法
   * @param args 方法引數
   * @param methodProxy cglib派生的子物件
   */
  @Override
  public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
      throws Throwable {
    Instant start = Instant.now();

    //遮蔽toString ,equals等方法
    Object parentMethodResult = checkProxyMethod(method, args);
    if (parentMethodResult != null) {
      return parentMethodResult;
    }
    log.info("----------------------------------------------");
    log.info("[{}]呼叫遠端方法[{}].", className, method.getName());

    String url = rootUrl + method.getAnnotation(HttpKoalaMethod.class).value();
    HttpMethod httpMethod = method.getAnnotation(HttpKoalaMethod.class).httpMethod();

    //建立請求request
    Request request;
    if (httpMethod.equals(HttpMethod.GET)) {
      request = httpClient.initGet(url + requestParser.formatParameter2Url(method, args));
    } else {
      request = httpClient.initPost(url, requestParser.getMediaType(),
          requestParser.formatParameter2Body(method, args));
    }
    //一個切面 可以檢查request
    requestParser.checkRequest(request);

    //執行
    Response response = httpClient.execute(request);

    //解析
    Object result = responseParser.parserResponse(response, method.getReturnType());

    log.info("[{}]遠端方法[{}]代理執行時間:[{}]", className, method.getName(),
        Duration.between(start, Instant.now()).toMillis());
    log.info("----------------------------------------------\n");
    return result;
  }
}

框架程式碼: Http的請求訊息和響應訊息都做了一個可替換的parser ,預設是JSON ,只要實現對應的介面 ,在@HttpKoala中設定就可以替換了.

public @interface HttpKoala {
  String value();
  Class responseParser() default JsonResponseParser.class;
  Class requestParser() default JsonRequestParser.class;
}

框架程式碼: HttpClientPool 是封裝的OkHttp ,對外沒有依賴 ,比較簡潔也很好修改 ;

public class HttpProxyObject implements MethodInterceptor {

  private String className;
  private String rootUrl;

  private AbstractResponseParser responseParser;
  private AbstractRequestParser requestParser;

  private HttpClientPool httpClient = new HttpClientPool();

  /**
   * 建立代理物件 ,新增序列化工具
   */
  HttpProxyObject(Class<?> classType) {
    this.className = classType.getName();
    this.rootUrl = classType.getAnnotation(HttpKoala.class).value();

    //檢查解析器
    Class<?> resParserClass = classType.getAnnotation(HttpKoala.class).responseParser();
    Class<?> reqParserClass = classType.getAnnotation(HttpKoala.class).requestParser();
    if (resParserClass.isAssignableFrom(AbstractResponseParser.class) && reqParserClass
        .isAssignableFrom(AbstractRequestParser.class)) {
      log.error("對應的訊息解析器必須繼承自AbstractResponseParser和AbstractRequestParser.");
      return;
    }

    try {
      responseParser = (AbstractResponseParser) resParserClass.newInstance();
      requestParser = (AbstractRequestParser) reqParserClass.newInstance();
    } catch (Exception e) {
      e.printStackTrace();
    }

    log.info("生成[{}]遠端代理Bean,使用[{}]進行結果解析", classType.getName(), resParserClass.getName());
  }

總結

程式碼還是要多寫才行