依赖注入框架解惑

何为依赖注入?

就是非自己主动初始化依赖,而是通过外部传入依赖的方式,我们就称之为依赖注入。
而好处是什么呢?

  • 解耦,将依赖之间解耦。
  • 方便单元测试。

何为依赖注入框架?

就是一个框架 来做依赖注入这件事,这样我们就可以把自己的重心放到核心的逻辑上去。
而一个依赖注入框架需要解决的事情:

  1. 生成 被注入对象
  2. 被注入对象 与 注入的类中的 引用 绑定。
  3. 额外的 被注入对象 生命周期管理。

一个框架完成前两部分就ok了,而后一部分更像是基于此的扩充的额外功能。
典型的例子如ButterKnife 和 AndroidAnnotations,对于Android 控件View控件的初始化,只需要一个注解,便省去了findViewbyId的繁琐,自动完成了对象和引用的绑定。然后View 的 生命周期便被Activity或者Fragment托管。
但是从另一个方面去考虑的话,其实对象的生成是已经在inflate view 时产生出来了,和我们理解的从外部传入又有些不一致,并不是所谓的“依赖注入”。
这里发现其实View也是可以直接注入的,哈哈 ,逃)

不同的依赖注入框架实现

  • Spring IoC
    IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。


    Spring 容器

    Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。
    最后使用xml配置,使用java的类反射机制生成实例,直接使用。


  • Roboguice
    这个是google提供的依赖注入框架,基于注解 ,基于反射,�效率上在Android 设备肯定不是最优的,现在google 也不再维护,而是推荐了其他框架如dagger和dagger2 这些,这里大家感兴趣的话,可以参考下 依赖注入框架性能对比
    有空的话,我再详细的分析下源码实现。
    //TODO 的分割线

  • dagger
    square 出品,必属精品。�注入框架是以JSR-330为标准的,后面的dagger2也参照了这个标准。
    下面是一个demo的例子:
public class MainActivity extends AppCompatActivity {

    @Inject Test1 mTest1;
    @Inject TestManager15 mTestManager15;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ObjectGraph objectGraph = ObjectGraph.create(new Dagger1Module(), new ModelModule());
        objectGraph.inject(this);
        objectGraph.inject(mTest1);
        Log.d(MainActivity.class.getSimpleName(), "onCreate(): " + mTest1.toString());
        mTestManager15.start();
    }

}

dagger 用到了apt生成部分代码,在编译完成后我们可以看到下图目录是有代码的,apt生成的代码。


apt生成的代码

我们看下这个类内部实现,如下图:


具体实现

可以看出这个类在实际的代码中是没有被引用到了,但是这个类却完成在attch方法中完成了被注入的类的实例化工作,在injectMembers中完成了绑定的工作。
显而易见的,这个类是通过反射实例化出来的。

于是,我们知道了dagger的实现思路:

* 通过apt生成一个单一容器类(injectadapter),这个类完成下面两个逻辑任务:
1. 生成 被注入对象 的逻辑。
2. 将  被注入对象  与 注入的类中的  引用  绑定 的逻辑。

* 通过反射实例化这个�容器(injectadapter),�注入对象。

具体的实现我们可以参考Linker和�下面的源码:


/**
 * Handles loading/finding of modules, injection bindings, and static injections by use of a
 * strategy of "load the appropriate generated code" or, if no such code is found, create a
 * reflective equivalent.
 */
public final class FailoverLoader extends Loader {
  /*
   * Note that String.concat is used throughout this code because it is the most efficient way to
   * concatenate _two_ strings.  javac uses StringBuilder for the + operator and it has proven to
   * be wasteful in terms of both CPU and memory allocated.
   */

  private final Memoizer<Class<?>, ModuleAdapter<?>> loadedAdapters =
      new Memoizer<Class<?>, ModuleAdapter<?>>() {
        @Override protected ModuleAdapter<?> create(Class<?> type) {
          ModuleAdapter<?> result =
              instantiate(type.getName().concat(MODULE_ADAPTER_SUFFIX), type.getClassLoader());
          if (result == null) {
            throw new IllegalStateException("Module adapter for " + type + " could not be loaded. "
                + "Please ensure that code generation was run for this module.");
          }
          return result;
        }
      };

  /**
   * Obtains a module adapter for {@code module} from the first responding resolver.
   */
  @SuppressWarnings("unchecked") // cache ensures types match
  @Override public <T> ModuleAdapter<T> getModuleAdapter(Class<T> type) {
    return (ModuleAdapter<T>) loadedAdapters.get(type);
  }

  @Override public Binding<?> getAtInjectBinding(
      String key, String className, ClassLoader classLoader, boolean mustHaveInjections) {
    Binding<?> result = instantiate(className.concat(INJECT_ADAPTER_SUFFIX), classLoader);
    if (result != null) {
      return result; // Found loadable adapter, returning it.
    }
    Class<?> type = loadClass(classLoader, className);
    if (type.equals(Void.class)) {
      throw new IllegalStateException(
          String.format("Could not load class %s needed for binding %s", className, key));
    }
    if (type.isInterface()) {
      return null; // Short-circuit since we can't build reflective bindings for interfaces.
    }
    return ReflectiveAtInjectBinding.create(type, mustHaveInjections);
  }

  @Override public StaticInjection getStaticInjection(Class<?> injectedClass) {
    StaticInjection result = instantiate(
          injectedClass.getName().concat(STATIC_INJECTION_SUFFIX), injectedClass.getClassLoader());
    if (result != null) {
      return result;
    }
    return ReflectiveStaticInjection.create(injectedClass);
  }
}

Dagger 相关概念

Module:也叫 ModuleClass,指被 @Module 注解修饰的类,为 Dagger 提供需要依赖注入的 Host 信息及一些 Dependency 的生成方式。

ModuleAdapter:指由 APT 根据 @Module 注解自动生成的类,父类是 Dagger 的 ModuleAdapter.java,与 ModuleClass 对应,以 ModuleClass 的 ClassName 加上 $$ModuleAdapter 命名,在 ModuleClass 的同一个 package 下。

InjectAdapter:每个属性或构造函数被 @Inject 修饰的类都会生成一个 继承自 Binding.java 的子类,生成类以修饰类的 ClassName 加上 $$InjectAdapter 命名,在该类的同一个 package 下。

ProvidesAdapter:每个被 @Provides 修饰的生成函数都会生成一个继承自 ProvidesBinding.java 的子类,ProvidesBinding.java 继承自 Binding.java,生成类以 Provide 函数名首字母大写加上 ProvidesAdapter 命名,是 Provide 函数所在 Module 对应生成的ModuleAdapter中的静态内部类。

Binding:指由 APT 根据 @Inject 注解和 @Provides 注解自动生成,最终继承自 Binding.java 的类。为下面介绍的 DAG 图中的一个节点,每个 Host 及依赖都是一个 Binding。

Binding 安装:指将 Binding 添加到 Binding 库中。对 Dagger Linker.java 代码来说是将 Binding 添加到 Linker.bindings 属性中,Linker.bindings 属性表示某个 ObjectGraph 已安装的所有 Binding。对于下面的 DAG 图来说是将节点放到图中,但尚未跟其他任何节点连接起来。


DAG

Binding 连接:把当前 Binding 和它内部依赖的 Binding 进行连接,即初始化这个 Binding 内部的所有 Binding,使它们可用。对 DAG 的角度说,就是把某个节点与其所依赖的各个节点连接起来。

UML类图

上图是 Dagger 整体框架最简类关系图。大致原理可以描述为:Linker通过Loader加载需要的Binding并把它们拼装成合理的依赖关系图 ObjectGraph,由ObjectGraph(其子类DaggerObjectGraph)最终实现依赖注入的管理。
ObjectGraph 是个抽象类,DaggerObjectGraph 是它目前唯一的子类,对 Dagger 的调用实际都是对 DaggerObjectGraph 的调用。

初始化

  private static ObjectGraph makeGraph(DaggerObjectGraph base, Loader plugin, Object... modules) {
     //储存静态的字符串key,value 是module的类名字
      Map<String, Class<?>> injectableTypes = new LinkedHashMap<String, Class<?>>();
    //静态属性
      Map<Class<?>, StaticInjection> staticInjections
          = new LinkedHashMap<Class<?>, StaticInjection>();
     //Object ,被注入的对象,以类名称为key,value为生成的$$ProvideAdapter
      StandardBindings baseBindings =
          (base == null) ? new StandardBindings() : new StandardBindings(base.setBindings);
      BindingsGroup overrideBindings = new OverridesBindings();

      Map<ModuleAdapter<?>, Object> loadedModules = Modules.loadModules(plugin, modules);
      for (Entry<ModuleAdapter<?>, Object> loadedModule : loadedModules.entrySet()) {
        ModuleAdapter<Object> moduleAdapter = (ModuleAdapter<Object>) loadedModule.getKey();
        for (int i = 0; i < moduleAdapter.injectableTypes.length; i++) {
          injectableTypes.put(moduleAdapter.injectableTypes[i], moduleAdapter.moduleClass);
        }
        for (int i = 0; i < moduleAdapter.staticInjections.length; i++) {
          staticInjections.put(moduleAdapter.staticInjections[i], null);
        }
        try {
          BindingsGroup addTo = moduleAdapter.overrides ? overrideBindings : baseBindings;
          //从moduleadapter中获取到将要绑定的provideAdapter对象,以@Provide的类名为key,ProvidesAdapter为value,
          moduleAdapter.getBindings(addTo, loadedModule.getValue());
        } catch (IllegalArgumentException e) {
          throw new IllegalArgumentException(
              moduleAdapter.moduleClass.getSimpleName() + ": " + e.getMessage(), e);
        }
      }

      // Create a linker and install all of the user's bindings
      Linker linker =
          new Linker((base != null) ? base.linker : null, plugin, new ThrowingErrorHandler());
      //将map值放进到Linker中的bindings,在injectAdapter中调用requestBinding时使用
      linker.installBindings(baseBindings);
      linker.installBindings(overrideBindings);

      return new DaggerObjectGraph(
          base, linker, plugin, staticInjections, injectableTypes, baseBindings.setBindings);
    }

在MainActivity attch时,调用:

 @Override
  @SuppressWarnings("unchecked")
  public void attach(Linker linker) {
    mTest1 = (Binding<com.nimbledroid.demo.dagger1.test.Test1>) linker.requestBinding("com.nimbledroid.demo.dagger1.test.Test1", MainActivity.class, getClass().getClassLoader());
    mTestManager15 = (Binding<com.nimbledroid.demo.dagger1.manager.TestManager15>) linker.requestBinding("com.nimbledroid.demo.dagger1.manager.TestManager15", MainActivity.class, getClass().getClassLoader());
  }

此时调用,默认后两个参数为true:

  public Binding<?> requestBinding(String key, Object requiredBy, ClassLoader classLoader,
      boolean mustHaveInjections, boolean library) {
    assertLockHeld();

    Binding<?> binding = null;
    for (Linker linker = this; linker != null; linker = linker.base) {
      binding = linker.bindings.get(key);
      if (binding != null) {
        if (linker != this && !binding.isLinked()) throw new AssertionError();
        break;
      }
    }

    if (binding == null) {
      // We can't satisfy this binding. Make sure it'll work next time!
      Binding<?> deferredBinding =
          new DeferredBinding(key, classLoader, requiredBy, mustHaveInjections);
      deferredBinding.setLibrary(library);
      deferredBinding.setDependedOn(true);
      toLink.add(deferredBinding);
      attachSuccess = false;
      return null;
    }

    if (!binding.isLinked()) {
      toLink.add(binding); // This binding was never linked; link it now!
    }

    binding.setLibrary(library);
    binding.setDependedOn(true);
    return binding;
  }

在inject 传入需要赋给引用的对象后,会调用到:

  /**
   * Links all requested bindings plus their transitive dependencies. This
   * creates JIT bindings as necessary to fill in the gaps.
   *
   * @throws AssertionError if this method is not called within a synchronized block which
   *     holds this {@link Linker} as the lock object.
   */
  public void linkRequested() {
    assertLockHeld();
    Binding<?> binding;
    while ((binding = toLink.poll()) != null) {
      if (binding instanceof DeferredBinding) {
        DeferredBinding deferred = (DeferredBinding) binding;
        String key = deferred.deferredKey;
        boolean mustHaveInjections = deferred.mustHaveInjections;
        if (bindings.containsKey(key)) {
          continue; // A binding for this key has since been linked.
        }
        try {
        //////实例化出需要注入的对象
          Binding<?> resolvedBinding =
              createBinding(key, binding.requiredBy, deferred.classLoader, mustHaveInjections);
          resolvedBinding.setLibrary(binding.library());
          resolvedBinding.setDependedOn(binding.dependedOn());
          // Fail if the type of binding we got wasn't capable of what was requested.
          if (!key.equals(resolvedBinding.provideKey) && !key.equals(resolvedBinding.membersKey)) {
            throw new IllegalStateException("Unable to create binding for " + key);
          }
          // Enqueue the JIT binding so its own dependencies can be linked.
          Binding<?> scopedBinding = scope(resolvedBinding);
          toLink.add(scopedBinding);
          putBinding(scopedBinding);
        } catch (InvalidBindingException e) {
          addError(e.type + " " + e.getMessage() + " required by " + binding.requiredBy);
          bindings.put(key, Binding.UNRESOLVED);
        } catch (UnsupportedOperationException e) {
          addError("Unsupported: " + e.getMessage() + " required by " + binding.requiredBy);
          bindings.put(key, Binding.UNRESOLVED);
        } catch (IllegalArgumentException e) {
          addError(e.getMessage() + " required by " + binding.requiredBy);
          bindings.put(key, Binding.UNRESOLVED);
        } catch (RuntimeException e) {
          throw e;
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
      } else {
        // Attempt to attach the binding to its dependencies. If any dependency
        // is not available, the attach will fail. We'll enqueue creation of
        // that dependency and retry the attachment later.
        attachSuccess = true;
        binding.attach(this);
        if (attachSuccess) {
          binding.setLinked();
        } else {
          toLink.add(binding);
        }
      }
    }

    try {
      errorHandler.handleErrors(errors);
    } finally {
      errors.clear();
    }
  }

然后是检测是否有循环依赖:

 private static void detectCircularDependencies(Collection<Binding<?>> bindings,
      List<Binding<?>> path) {
    for (Binding<?> binding : bindings) {
      if (binding.isCycleFree()) {
        continue;
      }

      if (binding.isVisiting()) {
        int index = path.indexOf(binding);
        StringBuilder message = new StringBuilder()
            .append("Dependency cycle:");
        for (int i = index; i < path.size(); i++) {
          message.append("\n    ").append(i - index).append(". ")
              .append(path.get(i).provideKey).append(" bound by ").append(path.get(i));
        }
        message.append("\n    ").append(0).append(". ").append(binding.provideKey);
        throw new IllegalStateException(message.toString());
      }

      binding.setVisiting(true);
      path.add(binding);
      try {
        ArraySet<Binding<?>> dependencies = new ArraySet<Binding<?>>();
        binding.getDependencies(dependencies, dependencies);
        //获取当前binding的依赖,进行递归检测
        detectCircularDependencies(dependencies, path);
        binding.setCycleFree(true);
      } finally {
        path.remove(path.size() - 1);
        binding.setVisiting(false);
      }
    }
  }

  • dagger2
    dagger2则更进一步的进行了优化,利于接口的特性和apt的�强大功能,完全摆脱的反射,所有的类,绑定实现都是依靠自动生成的代码进行。
    对外部提供的入口类在编译时�生成,逻辑的交互也就依靠接口来实现。
    在使用dagger2时,需要理解几个概念 :
@Inject: 通常在需要依赖的地方使用这个注解。换句话说,你用它告诉Dagger这个类或者字段需要依赖注入。这样,Dagger就会构造一个这个类的实例并满足他们的依赖。

@Module: Modules类里面的方法专门提供依赖,所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例的时候,就知道从哪里去找到需要的依赖。modules的一个重要特征是它们设计为分区并组合在一起(比如说,在我们的app中可以有多个组成在一起的modules)。

@Provide: 在modules中,我们定义的方法是用这个注解,以此来告诉Dagger我们想要构造对象并提供这些依赖。

@Component: Components从根本上来说就是一个注入器,也可以说是@Inject和@Module的桥梁,它的主要作用就是连接这两个部分。Components可以提供所有定义了的类型的实例,比如:我们必须用@Component注解一个接口然后列出所有的@Modules组成该组件,如果缺失了任何一块都会在编译的时候报错。所有的组件都可以通过它的modules知道依赖的范围。

@Scope: Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域。后面会演示一个例子,这是一个非常强大的特点,因为就如前面说的一样,没必要让每个对象都去了解如何管理他们的实例。在scope的例子中,我们用自定义的@PerActivity注解一个类,所以这个对象存活时间就和activity的一样。简单来说就是我们可以定义所有范围的粒度(@PerFragment, @PerUser, 等等)。

Qualifier: 当类的类型不足以鉴别一个依赖的时候,我们就可以使用这个注解标示。例如:在Android中,我们会需要不同类型的context,所以我们就可以定义qualifier注解“@ForApplication”和“@ForActivity”,这样当注入一个context的时候,我们就可以告诉Dagger我们想要哪种类型的context。

而后,我们来分析一个简单的dagger2的例子:
定义的Subcomponent:

/**
 * Scope: activity, it will be instantiated when the MainActivity starts.
 */
@ActivityScope
@Subcomponent(modules = {MainModule.class})
public interface MainComponent {

    void inject(MainActivity activity);

}

而后我们在MainActivity中写下如下逻辑:

public class MainActivity extends AppCompatActivity {

    @Inject
    @Named("AppName")
    String mAppName;
    @Inject
    @Named("UserName")
    String mUserName;
    @Inject
    @Named("title")
    String mTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        MainComponent mainComponent = App.userComponent().plusMain();
        mainComponent.inject(this);

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ActionBar actionBar = getSupportActionBar();
        actionBar.setTitle(mTitle);

        TextView textView = (TextView) findViewById(R.id.text_view);
        textView.setText(mAppName);
        TextView userView = (TextView) findViewById(R.id.user_view);
        userView.setText(getString(R.string.current_user, mUserName));
    }
    
}

我们注入了3个�String字符串,并将MainActivity的引用传递给了MainComponent 接口。显而易见的,我们知道inject后,mainComponent 获取到MainActivity,下一步的动作便是为@Inject的成员变量赋值。
我们看下具体�apt生成的类:

@Generated(
  value = "dagger.internal.codegen.ComponentProcessor",
  comments = "https://google.github.io/dagger"
)
public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {
  private final Provider<String> mAppNameProvider;

  private final Provider<String> mUserNameProvider;

  private final Provider<String> mTitleProvider;

  public MainActivity_MembersInjector(
      Provider<String> mAppNameProvider,
      Provider<String> mUserNameProvider,
      Provider<String> mTitleProvider) {
    assert mAppNameProvider != null;
    this.mAppNameProvider = mAppNameProvider;
    assert mUserNameProvider != null;
    this.mUserNameProvider = mUserNameProvider;
    assert mTitleProvider != null;
    this.mTitleProvider = mTitleProvider;
  }

  public static MembersInjector<MainActivity> create(
      Provider<String> mAppNameProvider,
      Provider<String> mUserNameProvider,
      Provider<String> mTitleProvider) {
    return new MainActivity_MembersInjector(mAppNameProvider, mUserNameProvider, mTitleProvider);
  }

  @Override
  public void injectMembers(MainActivity instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.mAppName = mAppNameProvider.get();
    instance.mUserName = mUserNameProvider.get();
    instance.mTitle = mTitleProvider.get();
  }

  public static void injectMAppName(MainActivity instance, Provider<String> mAppNameProvider) {
    instance.mAppName = mAppNameProvider.get();
  }

  public static void injectMUserName(MainActivity instance, Provider<String> mUserNameProvider) {
    instance.mUserName = mUserNameProvider.get();
  }

  public static void injectMTitle(MainActivity instance, Provider<String> mTitleProvider) {
    instance.mTitle = mTitleProvider.get();
  }
}

dagger2通过接口的定义规避了�dagger通过 反射 获取 apt生成实现类 的问题,进一步的提高了注入的效率,确实值得学习。

延伸思考

但是dagger2也存在一些问题:

  1. 我们在使用一个框架或者第三方类库,这个东西首先是易于学习和上手,但是dagger2的一些概念确实让新手“望而生畏”,学习上确实需要一定成本,不能拿来就用。
  2. 对象生命周期的管理�,如何确保不发生内存泄漏的问题,很大程度上依赖�使用者。

基于以上两点,我们可以做一些优化拓展:
首先是接口的定义,我们是希望给使用者提供一些简单,可用,灵活性高的api,代码的生成上,我们不仅可以利用到apt,甚至可以在编译阶段使用javasist等工具改变�造类。
其次我们可以构建一个“对象容器”,就像Spring一样,管理对象的生命周期。

TODO

上面的分析�只是粗浅的概要,一些细节的实现如单例如何注入以及Scope均未分析到,可以参考文末的链接,另外我也会一直完善这个文档的。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,364评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,717评论 6 342
  • 一,每天早上六点起床跑步半小时 二,每天早上家人早安问候 三,每天三个群分享 感恩之心离财富最近,我深知自己还需要...
    天生赢家杨永刚阅读 312评论 0 0
  • 刘润五分钟商学院 彼得原理:在一个组织中大部分员工会趋向于上升到他所不能胜任的岗位上。简单的说就是大部分管理岗...
    MU_f57b阅读 210评论 0 0