ByxContainerAnnotation -- an annotation based lightweight IOC container

Posted by brewmiser on Thu, 10 Feb 2022 10:27:34 +0100

ByxContainerAnnotation is a lightweight IOC container based on annotations that mimics Spring IOC. It supports constructor injection and field injection, circular dependency processing and detection, and has a highly extensible plug-in system.

Project address: https://github.com/byx2000/byx-container-annotation

Maven introduction

<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>

Use example

Use a simple example to quickly understand the use of 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 function:

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);
}

After executing the main function, the console outputs the following results:

hello!
hi

quick get start

Unless otherwise specified, the classes in the following examples are defined in byx Test package.

AnnotationContainerFactory

This class is the implementation class of ContainerFactory interface. ContainerFactory is a container factory, which is used to create IOC containers from specified places.

AnnotationContainerFactory creates IOC containers through package scanning. The usage is as follows:

Container container = new AnnotationContainerFactory(/*Package name or Class object of a Class*/).create();

When constructing AnnotationContainerFactory, you need to pass in a package name or a Class object of a Class. When the create method is called, all classes marked with @ Component under the package and its sub packages will be scanned, and a Container instance will be returned after the scanning is completed.

Container

This interface is the root interface of IOC container. You can use this interface to receive the return value of the create method of ContainerFactory. The included methods are as follows:

methoddescribe
void registerObject(String id, ObjectFactory factory)Register the object with IOC container. If the id already exists, IdDuplicatedException will be thrown
<T> T getObject(String id)Get the object with the specified id in the container. If the id does not exist, throw IdNotFoundException
<T> T getObject(Class<T> type)Gets the object of the specified type in the container. If the type does not exist, a TypeNotFoundException will be thrown. If there are more than one object of the specified type, a MultiTypeMatchException will be thrown
Set<String> getObjectIds()Gets the id collection of all objects in the container

The usage is as follows:

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

// Gets the object of type A in the container
A a = container.getObject(A.class);

// Get the object with id msg in the container
String msg = container.getObject("msg");

@Component annotation

@Component annotations can be added to classes to register objects with IOC containers. During package scanning, only classes marked with @ Component annotation will be scanned.

example:

@Component
public class A {}

public class B {}


// You can get the A object
A a = container.getObject(A.class);

// TypeNotFoundException will be thrown when this statement is executed
// Because class B is not annotated with @ Component annotation, it is not registered in the IOC container
B b = container.getObject(B.class);

@The Component annotation can also be added to the method to register an object created by an instance method with the IOC container. The registered id is the method name.

example:

@Component
public class A {
    // A String with id msg is registered
    @Component
    public String msg() {
        return "hello";
    }
}

// The value of msg is hello
String msg = container.getObject("msg");

Note that if a method is marked with @ Component, the class to which the method belongs must also be marked with @ Component, otherwise the method will not be scanned by the package scanner.

@Id annotation

@The id annotation can be added to the class and used with @ Component to specify the id used when registering the object.

example:

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

// Get A object with id
A a = container.getObject("a");

Note that if the class is not marked with @ id, the id when the class is registered is the fully qualified class name of the class.

@The id annotation can also be added to the method to specify the id of the object created by the instance method.

example:

@Component
public class A {
    // A String with id msg is registered
    @Component @Id("msg")
    public String f() {
        return "hello";
    }
}

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

@Id annotation can also be added to method parameters and fields. See Constructor Inject ,Method parameter injection and @Autowire auto assembly.

Constructor Inject

If a class has only one constructor (no or with parameters), the IOC container will call the constructor when instantiating the class and automatically inject the parameters of the constructor from the container.

example:

@Component
public class A {
    private final B b;

    // Inject field b through constructor
    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {}

// a is correctly constructed and its field b is correctly injected
A a = container.getObject(A.class);

The @ id annotation can be used on the parameters of the constructor to specify the injected object id. If there is no @ id annotation, it is injected by type by default.

example:

@Component
public class A {
    private final B b;

    // Inject the object with id b1 through the constructor
    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 {}

// At this time, b in a injects an instance of B1
A a = container.getObject(A.class);

For classes with multiple constructors, you need to use the @ Autowire annotation to mark the constructor used for instantiation.

example:

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

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

    // Use this constructor to create the A object
    @Autowire
    public A(String s) {
        this.s = s;
    }
}

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

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

// A instantiated using a constructor with a String parameter
A a = container.getObject(A.class);

Note that it is not allowed to annotate @ Autowire annotation on multiple constructors at the same time.

@Autowired auto assembly

@The annotation field of the object is directly injected on the annotation field of the object.

example:

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

@Component
public class B {}

// Field b in a was successfully injected
A a = container.getObject(A.class);

By default, @ Autowired is injected by type@ Autowired can also be used together with @ id to realize injection according to id.

example:

@Component
public class A {
    // Inject object with 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 {}

// Field b in a injects the object of B1
A a = container.getObject(A.class);

@Autowired can also be marked on the constructor. See Constructor Inject .

Method parameter injection

If the instance method marked with @ Component has a parameter list, these parameters will also be automatically injected from the container, and the injection rules are the same as those of the constructor.

example:

@Component
public class A {
    // All parameters of the method are obtained from the container
    @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";
    }
}

// The value of s is: hello hi
String s = container.getObject("s");

@Init annotation

@Init annotation is used to specify the initialization method of the object, which is created after the object attribute is filled and before the proxy object is created.

example:

@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";
    }
}

// Get a object
A a = container.getObject(A.class);

The output is as follows:

constructor
setter 1
setter 2
init

@Value annotation

@The Value annotation is used to register constant values into the container. This annotation is marked on a class marked by @ Component and can be marked repeatedly.

@Component
// Register an object of type String with id strVal and value hello
@Value(id = "strVal", value = "hello")
// Register an object of type int with id intVal and value 123
@Value(type = int.class, id = "intVal", value = "123")
// Register an object of type String whose id and value are hi
@Value(value = "hi")
// Register an object of type double with id and value of 6.28
@Value(type = double.class, value = "6.28")
public class A {
}

Users can register custom types by implementing 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;
    }

    // Omit getter s and setter s
}

@Component // Note that the converter needs to be registered in the container
public class UserConverter implements ValueConverter {
    @Override
    public Class<?> getType() {
        return User.class;
    }

    @Override
    public Object convert(String s) {
        // Convert string to User object
        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));
    }
}

// Register a User object
@Value(id = "user", type = User.class, value = "User(1001,'byx','123')")
public class A {
}

Cyclic dependence

ByxContainerAnnotation supports the processing and detection of various cyclic dependencies. The following are some examples.

Circular dependency of an object:

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

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

    // a was successfully created and initialized
    A a = container.getObject(A.class);
}

Circular dependency of two objects:

@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();

    // Both a and b were successfully created and initialized
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
}

Circular dependency of mixed constructor injection and field injection:

@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();

    // Both a and b were successfully created and initialized
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
}

Circular dependency of three objects:

@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. Both b and c are successfully created and initialized
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
    C c = container.getObject(C.class);
}

Unresolved circular dependencies:

@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();

    // Throw CircularDependencyException
    A a = container.getObject(A.class);
    B b = container.getObject(B.class);
}

extend

ByxContainer provides a flexible plug-in system. You can extend the functions of ByxContainer by introducing some dependencies named byx container extension - *. Of course, you can also write your own extensions.

Existing extensions

extendexplain
byx-container-extension-aopProvide aspect oriented programming (AOP) support, including pre enhancement (@ Before), post enhancement (@ After), surround enhancement (@ Around) and exception enhancement (@ AfterThrowing)
byx-container-extension-transactionProvides support for declarative transactions, including support for JdbcUtils and @ Transactional annotations

Write your own extensions

AnnotationContainerFactory provides two extension points:

  • ContainerCallback interface

    The interface is defined as follows:

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

    ContainerCallback is similar to Spring's beanfactorypost processor. The afterContainerInit method will call back after the package scanning. Users can dynamically register additional components in the container by creating the implementation class of the interface.

    When there are multiple containercallbacks, the order of their calls depends on the order value returned by getOrder. Those with small numbers are executed first.

  • ObjectCallback interface

    The interface is defined as follows:

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

    ObjectCallback is similar to Spring's BeanPostProcessor. afterObjectInit method will call back after object initialization (i.e. after attribute filling), and afterObjectWrap method will call back after proxy object creation.

    When there are multiple objectcallbacks, the order of their calls depends on the order value returned by getOrder. Those with small numbers are executed first.

To write a ByxContainer extension:

  1. Define one or more implementation classes of ContainerCallback and ObjectCallback. These implementation classes need to have an accessible default constructor

  2. In the resources directory, create a file named byx container extension Properties, which declares the components to be exported, and the key values are as follows:

    Key valuemeaning
    containerCallbackFully qualified class names of all ContainerCallback, separated by
    objectCallbackFully qualified class names of all ObjectCallback, separated by
  3. Package the project into Jar package or Maven dependency and introduce it into the main project (i.e. the project with byx container annotation) to enable custom callback components

Topics: Java Spring Container ioc