ByxContainerAnnotation——基于注解的轻量级IOC容器

ByxContainerAnnotation是一个模仿Spring IOC容器基于注解的轻量级IOC容器,支持构造函数注入和字段注入,支持循环依赖处理和检测,具有高可扩展的插件系统。

项目地址:https://github.com/byx2000/byx-container-annotation

Maven引入

<repositories>
    <repository>
        <id>byx-maven-repo</id>
        <name>byx-maven-repo</name>
        <url>https://gitee.com/byx2000/maven-repo/raw/master/</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>byx.ioc</groupId>
        <artifactId>byx-container-annotation</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

使用示例

通过一个简单的例子来快速了解ByxContainerAnnotation的使用。

A.java:

package byx.test;

import byx.ioc.annotation.Autowired;
import byx.ioc.annotation.Autowired;
import byx.ioc.annotation.Component;

@Component
public class A {
    @Autowired
    private B b;

    public void f() {
        b.f();
    }
}

B.java:

package byx.test;

import byx.ioc.annotation.Component;

@Component
public class B {
    public void f() {
        System.out.println("hello!");
    }

    @Component
    public String info() {
        return "hi";
    }
}

main函数:

public static void main(String[] args) {
    Container container = new AnnotationContainerFactory("byx.test").create();

    A a = container.getObject(A.class);
    a.f();

    String info = container.getObject("info");
    System.out.println(info);
}

执行main函数后,控制台输出结果:

hello!
hi

快速入门

如无特殊说明,以下示例中的类均定义在byx.test包下。

AnnotationContainerFactory

该类是ContainerFactory接口的实现类,ContainerFactory是容器工厂,用于从指定的地方创建IOC容器。

AnnotationContainerFactory通过包扫描的方式创建IOC容器,用法如下:

Container container = new AnnotationContainerFactory(/*包名或某个类的Class对象*/).create();

在构造AnnotationContainerFactory时,需要传入一个包名或者某个类的Class对象。调用create方法时,该包及其子包下所有标注了@Component的类都会被扫描,扫描完成后返回一个Container实例。

Container

该接口是IOC容器的根接口,可以用该接口接收ContainerFactorycreate方法的返回值,内含方法如下:

方法 描述
void registerObject(String id, ObjectFactory factory) 向IOC容器注册对象,如果id已存在,则抛出IdDuplicatedException
<T> T getObject(String id) 获取容器中指定id的对象,如果id不存在则抛出IdNotFoundException
<T> T getObject(Class<T> type) 获取容器中指定类型的对象,如果类型不存在则抛出TypeNotFoundException,如果存在多于一个指定类型的对象则抛出MultiTypeMatchException
Set<String> getObjectIds() 获取容器中所有对象的id集合

用法如下:

Container container = new AnnotationContainerFactory(...).create();

// 获取容器中类型为A的对象
A a = container.getObject(A.class);

// 获取容器中id为msg的对象
String msg = container.getObject("msg");

@Component注解

@Component注解可以加在类上,用于向IOC容器注册对象。在包扫描的过程中,只有标注了@Component注解的类才会被扫描。

例子:

@Component
public class A {}

public class B {}


// 可以获取到A对象
A a = container.getObject(A.class);

// 这条语句执行会抛出TypeNotFoundException
// 因为class B没有标注@Component注解,所以没有被注册到IOC容器中
B b = container.getObject(B.class);

@Component注解还可以加在方法上,用于向IOC容器注册一个实例方法创建的对象,注册的id为方法名。

例子:

@Component
public class A {
    // 注册了一个id为msg的String
    @Component
    public String msg() {
        return "hello";
    }
}

// msg的值为hello
String msg = container.getObject("msg");

注意,如果某个方法被标注了@Component,则该方法所属的类也必须标注@Component,否则该方法不会被包扫描器扫描。

@Id注解

@Id注解可以加在类上,与@Component配合使用,用于指定注册对象时所用的id。

例子:

@Component @Id("a")
public class A {}

// 使用id获取A对象
A a = container.getObject("a");

注意,如果类上没有标注@Id,则该类注册时的id为该类的全限定类名。

@Id注解也可以加在方法上,用于指定实例方法创建的对象的id。

例子:

@Component
public class A {
    // 注册了一个id为msg的String
    @Component @Id("msg")
    public String f() {
        return "hello";
    }
}

// hello
String msg = container.getObject("msg");

@Id注解还可以加在方法参数和字段上,请看构造函数注入方法参数注入@Autowire自动装配

构造函数注入

如果某类只有一个构造函数(无参或有参),则IOC容器在实例化该类的时候会调用该构造函数,并自动从容器中注入构造函数的参数。

例子:

@Component
public class A {
    private final B b;

    // 通过构造函数注入字段b
    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {}

// a被正确地构造,其字段b也被正确地注入
A a = container.getObject(A.class);

在构造函数的参数上可以使用@Id注解来指定注入的对象id。如果没有标注@Id注解,则默认是按照类型注入。

例子:

@Component
public class A {
    private final B b;

    // 通过构造函数注入id为b1的对象
    public A(@Id("b1") B b) {
        this.b = b;
    }
}

public class B {}

@Component @Id("b1")
public class B1 extends B {}

@Component @Id("b2")
public class B2 extends B {}

// 此时a中的b注入的是B1的实例
A a = container.getObject(A.class);

对于有多个构造函数的类,需要使用@Autowire注解标记实例化所用的构造函数。

例子:

@Component
public class A {
    private Integer i;
    private String s;

    public A(Integer i) {
        this.i = i;
    }

    // 使用这个构造函数来创建A对象
    @Autowire
    public A(String s) {
        this.s = s;
    }
}

@Component
public class B {
    @Component
    public Integer f() {
        return 123;
    }

    @Component
    public String g() {
        return "hello";
    }
}

// 使用带String参数的构造函数实例化的a
A a = container.getObject(A.class);

注意,不允许同时在多个构造函数上标注@Autowire注解。

@Autowired自动装配

@Autowired注解标注在对象中的字段上,用于直接注入对象的字段。

例子:

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {}

// a中的字段b被成功注入
A a = container.getObject(A.class);

默认情况下,@Autowired按照类型注入。@Autowired也可以配合@Id一起使用,实现按照id注入。

例子:

@Component
public class A {
    // 注入id为b1的对象
    @Autowired @Id("b1")
    private B b;
}

public class B {}

@Component @Id("b1")
public class B1 extends B {}

@Component @Id("b2")
public class B2 extends B {}

// a中的字段b注入的是B1的对象
A a = container.getObject(A.class);

@Autowired还可标注在构造函数上,请看构造函数注入

方法参数注入

如果标注了@Component的实例方法带有参数列表,则这些参数也会从容器自动注入,注入规则与构造函数的参数注入相同。

例子:

@Component
public class A {
    // 该方法的所有参数都从容器中获得
    @Component
    public String s(@Id("s1") String s1, @Id("s2") String s2) {
        return s1 + " " + s2;
    }
}

@Component
public class B {
    @Component
    public String s1() {
        return "hello";
    }

    @Component
    public String s2() {
        return "hi";
    }
}

// s的值为:hello hi
String s = container.getObject("s");

@Init注解

@Init注解用于指定对象的初始化方法,该方法在对象属性填充后、创建代理对象前创建。

例子:

@Component
public class A {
    public A() {
        System.out.println("constructor");
        State.state += "c";
    }

    @Autowired
    public void set1(String s) {
        System.out.println("setter 1");
        State.state += "s";
    }

    @Init
    public void init() {
        System.out.println("init");
        State.state += "i";
    }

    @Autowired
    public void set2(Integer i) {
        System.out.println("setter 2");
        State.state += "s";
    }
}

// 获取a对象
A a = container.getObject(A.class);

输出如下:

constructor
setter 1
setter 2
init

@Value注解

@Value注解用于向容器中注册常量值。该注解标注在某个被@Component标注的类上,可重复标注。

@Component
// 注册一个id为strVal、值为hello的String类型的对象
@Value(id = "strVal", value = "hello")
// 注册一个id为intVal、值为123的int类型的对象
@Value(type = int.class, id = "intVal", value = "123")
// 注册一个id和值都为hi的String类型的对象
@Value(value = "hi")
// 注册一个id和值都为6.28的double类型的对象
@Value(type = double.class, value = "6.28")
public class A {
}

用户可通过实现一个ValueConverter来注册自定义类型:

public class User {
    private final Integer id;
    private final String username;
    private final String password;

    public User(Integer id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    // 省略getter和setter ...
}

@Component // 注意,该转换器要在容器中注册
public class UserConverter implements ValueConverter {
    @Override
    public Class<?> getType() {
        return User.class;
    }

    @Override
    public Object convert(String s) {
        // 将字符串转换为User对象
        s = s.substring(5, s.length() - 1);
        System.out.println(s);
        String[] ps = s.split(",");
        System.out.println(Arrays.toString(ps));
        return new User(Integer.valueOf(ps[0]), ps[1].substring(1, ps[1].length() - 1), ps[2].substring(1, ps[2].length() - 1));
    }
}

// 注册一个User对象
@Value(id = "user", type = User.class, value = "User(1001,'byx','123')")
public class A {
}

循环依赖

ByxContainerAnnotation支持各种循环依赖的处理和检测,以下是一些例子。

一个对象的循环依赖:

@Component
public class A {
    @Autowired
    private A a;
}

public static void main(String[] args) {
    Container container = new AnnotationContainerFactory("byx.test").create();

    // a被成功创建并初始化
    A a = container.getObject(A.class);
}

两个对象的循环依赖:

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

public static void main(String[] args) {
    Container container = new AnnotationContainerFactory("byx.test").create();

    // a和b都被成功创建并初始化
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
}

构造函数注入与字段注入混合的循环依赖:

@Component
public class A {
    private final B b;

    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {
    @Autowired
    private A a;
}

public static void main(String[] args) {
    Container container = new AnnotationContainerFactory("byx.test").create();

    // a和b都被成功创建并初始化
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
}

三个对象的循环依赖:

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private C c;
}

@Component
public class C {
    @Autowired
    private A a;
}

public static void main(String[] args) {
    Container container = new AnnotationContainerFactory("byx.test").create();

    // a、b、c都被成功创建并初始化
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
    C c = container.getObject(C.class);
}

无法解决的循环依赖:

@Component
public class A {
    private final B b;

    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {
    private final A a;

    public B(A a) {
        this.a = a;
    }
}

public static void main(String[] args) {
    Container container = new AnnotationContainerFactory("byx.test").create();

    // 抛出CircularDependencyException异常
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
}

扩展

ByxContainer提供了一个灵活的插件系统,你可以通过引入一些名称为byx-container-extension-*的依赖来扩展ByxContainer的功能。当然,你也可以编写自己的扩展。

当前已有的扩展

扩展 说明
byx-container-extension-aop 提供面向切面编程(AOP)的支持,包括前置增强(@Before)、后置增强(@After)、环绕增强(@Around)、异常增强(@AfterThrowing)四种增强类型
byx-container-extension-transaction 提供声明式事务的支持,包括对JdbcUtils@Transactional注解的支持

自己编写扩展

AnnotationContainerFactory对外提供两个扩展点:

  • ContainerCallback接口

    该接口定义如下:

    public interface ContainerCallback {
        void afterContainerInit(Container container);
    
        default int getOrder() {
            return 1;
        }
    }
    

    ContainerCallback类似于Spring的BeanFactoryPostProcessorafterContainerInit方法会在包扫描结束后回调,用户可通过创建该接口的实现类来动态地向容器中注册额外的组件。

    当存在多个ContainerCallback时,它们调用的先后顺序取决于getOrder返回的顺序值,数字小的先执行。

  • ObjectCallback接口

    该接口定义如下:

    public interface ObjectCallback{
        default void afterObjectInit(ObjectCallbackContext ctx) {
    
        }
    
        default Object afterObjectWrap(ObjectCallbackContext ctx) {
            return ctx.getObject();
        }
    
        default int getOrder() {
            return 1;
        }
    }
    

    ObjectCallback类似于Spring的BeanPostProcessorafterObjectInit方法会在对象初始化后(即属性填充后)回调,afterObjectWrap方法会在代理对象创建后回调。

    当存在多个ObjectCallback时,它们调用的先后顺序取决于getOrder返回的顺序值,数字小的先执行。

编写ByxContainer扩展的步骤:

  1. 定义一个或多个ContainerCallbackObjectCallback的实现类,这些实现类需要有可访问的默认构造函数

  2. resources目录下创建一个名为byx-container-extension.properties的文件,该文件声明了需要导出的组件,包含的键值如下:

    键值 含义
    containerCallback 所有ContainerCallback的全限定类名,用,分隔
    objectCallback 所有ObjectCallback的全限定类名,用,分隔
  3. 将该项目打包成Jar包或Maven依赖,在主项目(即引入了byx-container-annotation的项目)中引入,即可启用自定义的回调组件

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

推荐阅读更多精彩内容