V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
shellquery
V2EX  ›  程序员

信不信, 200 行 Java 代码就能实现依赖注入框架

  •  
  •   shellquery · 2018-05-02 08:19:06 +08:00 · 3999 次点击
    这是一个创建于 2178 天前的主题,其中的信息可能已经有所发展或是发生改变。

    依赖注入框架并不神秘,其实它是非常简单的东西。不要去看 spring 的依赖注入源码,因为你只要一去看就意味着你再也写不敢下手自己撸了,它的功能因为过于强大,所以设计也过于复杂,普通程序员一眼看去只能望洋兴叹。

    我也并没有去细致阅读 spring 源码。即便如此也只用了半天的时间便自己撸了一个基本满足标准依赖注入规范「 JSR-330 」的小框架 iockids。这个小框架只有一个主类 Injector,大约 200 行代码,它具备以下功能。

    1. 单例 /非单例注入
    2. 构造器注入
    3. 字段注入
    4. 循环依赖注入
    5. Qualifier 注入

    我们看一个稍微复杂一点的使用示例

    import javax.inject.Inject;
    import javax.inject.Named;
    import javax.inject.Singleton;
    
    import iockids.Injector;
    
    @Singleton
    class Root {
    
    	@Inject
    	@Named("a")
    	Node a;
    
    	@Inject
    	@Named("b")
    	Node b;
    
    	@Override
    	public String toString() {
    		return String.format("root(%s, %s)", a.name(), b.name());
    	}
    
    }
    
    interface Node {
    
    	String name();
    
    }
    
    @Singleton
    @Named("a")
    class NodeA implements Node {
    
    	@Inject
    	Leaf leaf;
    
    	@Inject
    	@Named("b")
    	Node b;
    
    	@Override
    	public String name() {
    		if (b == null)
    			return String.format("nodeA(%s)", leaf);
    		else
    			return String.format("nodeAWithB(%s)", leaf);
    	}
    
    }
    
    @Singleton
    @Named("b")
    class NodeB implements Node {
    
    	Leaf leaf;
    
    	@Inject
    	@Named("a")
    	Node a;
    
    	@Inject
    	public NodeB(Leaf leaf) {
    		this.leaf = leaf;
    	}
    
    	@Override
    	public String name() {
    		if (a == null)
    			return String.format("nodeB(%s)", leaf);
    		else
    			return String.format("nodeBWithA(%s)", leaf);
    	}
    
    }
    
    class Leaf {
    
    	@Inject
    	Root root;
    
    	int index;
    
    	static int sequence;
    
    	public Leaf() {
    		index = sequence++;
    	}
    
    	public String toString() {
    		if (root == null)
    			return "leaf" + index;
    		else
    			return "leafwithroot" + index;
    	}
    
    }
    
    public class Demo {
    
    	public static void main(String[] args) {
    		var injector = new Injector();
    		injector.registerQualifiedClass(Node.class, NodeA.class);
    		injector.registerQualifiedClass(Node.class, NodeB.class);
    		var root = injector.getInstance(Root.class);
    		System.out.println(root);
    	}
    
    }
    

    上面这份代码用到了 iockids 提供的所有功能。

    1. Root/NodeA/NodeB 类是单例类
    2. Leaf 类是非单例类
    3. 它们都使用了字段注入
    4. NodeB 使用了构造器注入
    5. NodeA 和 NodeB 还使用了 Qualifier 名称注入
    6. Leaf 类中有 Root 类型的字段,这便是循环依赖
    7. NodeA 中有 NodeB 字段,NodeB 中有 NodeA 字段,这也是循环依赖

    为了便于理解上述代码,我画了依赖图

    上面的代码输出如下

    root(nodeAWithB(leafwithroot0), nodeBWithA(leafwithroot1))
    

    从这个输出中,我们也可以大致想象出依赖结构。

    iockids 提供了丰富的注入错误异常报告,防止用户注入配置出错。

    比如我们将上面的 NodeA 和 NodeB 的名称都配置成一样的 a,就会曝出下面的错误堆栈

    iockids.InjectException: duplicated qualifier javax.inject.Named with the same class iockids.demo.Node
    	at iockids.Injector.registerQualifiedClass(Injector.java:87)
    	at iockids.Injector.registerQualifiedClass(Injector.java:70)
    	at iockids.demo.Demo.main(Demo.java:106)
    
    

    如果我们将 NodeB 的构造器随意加一个参数

    	@Inject
    	public NodeB(Leaf leaf, int k) {
    		this.leaf = leaf;
    	}
    

    运行时就会抛出下面的错误

    iockids.InjectException: no accessible constructor for injection class int
    	at iockids.Injector.createNew(Injector.java:120)
    	at iockids.Injector.createNew(Injector.java:94)
    	at iockids.Injector.createFromParameter(Injector.java:167)
    	at iockids.Injector.createFromConstructor(Injector.java:145)
    	at iockids.Injector.createNew(Injector.java:123)
    	at iockids.Injector.createFromQualified(Injector.java:216)
    	at iockids.Injector.createFromField(Injector.java:173)
    	at iockids.Injector.injectMembers(Injector.java:233)
    	at iockids.Injector.createNew(Injector.java:136)
    	at iockids.Injector.createFromQualified(Injector.java:216)
    	at iockids.Injector.createFromField(Injector.java:173)
    	at iockids.Injector.injectMembers(Injector.java:233)
    	at iockids.Injector.createNew(Injector.java:136)
    	at iockids.Injector.createNew(Injector.java:94)
    	at iockids.Injector.getInstance(Injector.java:245)
    	at iockids.demo.Demo.main(Demo.java:107)
    

    项目开源地址: https://github.com/pyloque/iockids

    11 条回复    2018-05-04 12:16:36 +08:00
    mjStudio
        1
    mjStudio  
       2018-05-02 09:26:07 +08:00
    Android 开发路过
    omengye
        2
    omengye  
       2018-05-02 09:26:41 +08:00
    大佬的 trySetAccessible 方法是只能用在 Java9 以上版本?
    shellquery
        3
    shellquery  
    OP
       2018-05-02 09:43:33 +08:00 via Android
    准确的讲,整个项目只能运行在 java10,注意 var 关键字
    shellquery
        4
    shellquery  
    OP
       2018-05-02 10:19:30 +08:00   ❤️ 1
    https://github.com/pyloque/httpkids 自己撸 web 框架
    https://github.com/pyloque/ormkids 自己撸 orm 框架
    https://github.com/pyloque/rpckids 自己撸 rpc 框架
    还在犹豫什么呢,关注公众号「码洞」吧
    des
        5
    des  
       2018-05-02 10:22:19 +08:00 via Android
    @kai 移动到推广节点
    exalex
        6
    exalex  
       2018-05-02 11:31:37 +08:00
    @shellquery 关注了
    arthas2234
        7
    arthas2234  
       2018-05-02 11:49:24 +08:00
    。。。Java10,我们公司 Java8 都不愿意上
    shellquery
        8
    shellquery  
    OP
       2018-05-02 12:59:51 +08:00
    @arthas2234 公司跟不上时代,但是我们自己可以跟得上
    THP301
        9
    THP301  
       2018-05-03 12:07:53 +08:00
    收藏了
    AllOfMe
        10
    AllOfMe  
       2018-05-04 09:43:04 +08:00 via iPhone
    你这个照片看的我有点慌
    shellquery
        11
    shellquery  
    OP
       2018-05-04 12:16:36 +08:00 via Android
    @AllOfMe 要的就是这效果
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5805 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 02:45 · PVG 10:45 · LAX 19:45 · JFK 22:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.