IOC is a great way to decouple in Spring, so I wrote one myself to better understand

Preliminary knowledge:

Annotations, reflection, collection classes, lambda expressions, streaming apis

IOC

How do I register a class? First we want the container to “find” it, so use annotations to declare that it should be added to the container

The value corresponds to the Bean name in Spring

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Part {
    String value(a) default "";
}

Copy the code

For factory beans

// Used to annotate the factory class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Part
public @interface FactoryBean {
}
// It is used to label the production function
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Produce {
    String value(a) default "";
}

Copy the code

Tools for scanning package generation classes

Of course, some people will say that Hutool’s ClassScaner works well, but I’m going to write one for you to understand

The idea is to use the filename to generate a reflection of the Class using class.forname ()

public static List<Object> find(String packName, ClassFilter classFilter) throws IOException {
        // Get the current path
        Enumeration<URL> entity = Thread.currentThread().getContextClassLoader().getResources(packName);
        HashSet<String> classPaths = new HashSet<>();
        ArrayList<Object> classes = new ArrayList<>();
        // Get the path after processing, before processing is /.... /target/classes
        // After processing, /.... /target/classes
        if (entity.hasMoreElements()) {
            String path = entity.nextElement().getPath().substring(1);
            classPaths.add(path);
        }
        // Jump to a method I wrote to generate a class name from a.class file in a path, which I'll describe later
        // Set elements are class names such as entity.student
        Set<String> set = loadClassName(classPaths);
        for (String s : set) {
            try{ Class<? > c = Class.forName(s);// Use the filter to determine whether an instance needs to be generated
                if (classFilter.test(c)){
                    // The no-argument constructor is used here for simplicityConstructor<? > constructor = c.getConstructor(); constructor.setAccessible(true);
                    // Add the generated instance to the returned listclasses.add(constructor.newInstance()); }}catch (ClassNotFoundException| InstantiationException | IllegalAccessException| InvocationTargetException e) {
                throw new RuntimeException(e);
            }catch(NoSuchMethodException e){ System.err.println(e.getMessage()); }}return classes;
    }
Copy the code

One of the core functions is loadClassName

/ * * *@paramClassPaths collection of path names *@returnCollection of class names */
    private static Set<String> loadClassName(HashSet<String> classPaths){
        Queue<File> queue = new LinkedList<>();
        HashSet<String> classNames = new HashSet<>();
        // Get all files ending in.class for each path
        classPaths.forEach(p -> {
            // The iterative method, the tree level traversal
            queue.offer(new File(p));
            while(! queue.isEmpty()){ File file = queue.poll();if (file.isDirectory()) {
                    File[] files = file.listFiles();
                    for(File file1 : files) { queue.offer(file1); }}else if(file.getName().endsWith(".class")) {// Handle the filename to get the class name
                    / /... /target/classes is handled as \.... \target\classes
                    String replace = p.replace("/"."\ \");
                    // For each.class file, the name is.... \target\classes, remove the beginning, remove the suffix is the class name
                   String className = file.getPath()
                            .replace(replace, "")
                            .replace(".class"."").replace("\ \"."."); classNames.add(className); }}});return classNames;
    }
Copy the code

Okay, now you’re ready to scan your bags

I mentioned that not all classes need to be in a container. Now let’s see what the ClassFilter is

@FunctionalInterface
public interface ClassFilter{
    boolean test(Class c);
}
Copy the code

Is a functional interface, which means lambda expressions can be handy

Using this interface we can easily construct a function that will help us generate all the classes with the @Part annotation

public static<T> List<Object> findByAnnotation(String packName, Class<T> annotation) throws IOException{
        if(! annotation.isAnnotation()) {throw new RuntimeException("it not an annotation"+annotation.getTypeName()); } ClassFilter classFilter =(c) -> c.getAnnotation(annotation) ! =null;
        return find(packName, classFilter);
    }
Copy the code

The IOC container

I think we’ve done all the prep work up here

It’s time to start writing IOC containers

Think about how easy it is to get Java beans by bean name in Spring, so using a Map<String,Object> can simulate this.

Here we add a variable to the IOCContainer

private Map<String,Object> context;
Copy the code

The constructor

public IOCContainer(String packName){
        try {
            initPart(packName);
            initFactory();
        } catch(IOException e) { e.printStackTrace(); }}public IOCContainer(a){
        this("");
    }
Copy the code

Initialize the @Part annotation class, similar to @Component

/** will all have@PartNotes (including by@PartNotes of notes) * for example@Part.@BeanFactory, will be read * {*@Retention(RetentionPolicy.RUNTIME)
     *      @Target(ElementType.TYPE)
     *      @Part
     *   public @interface FactoryBean {
     * }
     *  }
     * @paramPackName The path name used in the ClassScannerUtil function is *@throws IOException
     * @author dreamlike_ocean
     */
public void initPart(String packName) throws IOException {
        // Do a bean name mapping. If the value in the @Part annotation is not empty, the bean name is used with the value of value
        // If empty, use the class name of the Java bean as the bean nameFunction<Object,String> keyMapper = (o) -> { Class<? > aClass = o.getClass(); Part part = aClass.getAnnotation(Part.class);if (part == null ||part.value().isBlank()) {
                return o.getClass().getTypeName();
            }
            return part.value();
        };
        context = new HashMap<String,Object>();
        // Get all class instances with @Part annotations
        List<Object> objectList = ClassScannerUtil.find(packName,(c) -> isContainPart(c,Part.class) );
        //List<Object> objectList = ClassScannerUtil.findByAnnotation(packName, Part.class);
        // Inject yourself first
        context.put("IOCContainer".this);
        for (Object o : objectList) {
            // Get the bean name using the mapping function interface written above
            String beanName = keyMapper.apply(o);
            // The bean name conflicts
            if (context.containsKey(beanName)) {
                String msg = new StringBuilder().append("duplicate bean name: ")
                        .append(beanName)
                        .append("in")
                        .append(o.getClass())
                        .append(" and ")
                        .append(context.get(beanName).getClass()).toString();
                throw new RuntimeException(msg);
            }
            // Add to container
            context.put(beanName, o);
        }

        // Help garbage collection, this complexity is O(n), theoretically objectList = null can also help garbage collection
        objectList.clear();
    }
Copy the code

So one of the core functions to do this is isContainPart()

 /** * Iterates to see if there are target annotations on the class, including annotations within annotations (similar to@FactoryBeanContained in the@Part) *@paramC To determine whether it contains@paramTarget annotated class *@param target
     * @returnContains */
    private boolean isContainPart(Class c,Class<? extends Annotation> target){
        Queue<Annotation> queue = new LinkedList<>(Arrays.asList(c.getDeclaredAnnotations()));
        while(! queue.isEmpty()) { Annotation poll = queue.poll();// Check if it is the same
            if (poll.annotationType().isAssignableFrom(target)){
                return true;
            }
            Annotation[] annotations = poll.annotationType().getDeclaredAnnotations();
            for (Annotation annotation : annotations) {
                // Documented causes an infinite loop and the queue size becomes larger and larger
                // So you need to check whether it is repeated
                if(! isRepeat(annotation.annotationType())){ queue.offer(annotation); }}}return false;
    }

    /** * Determine if a comment is annotated by yourself * for example * {*@Documented
     *     @Retention(RetentionPolicy.RUNTIME)
     *     @Target(ElementType.ANNOTATION_TYPE)
     * public @interface Documented {
     * }
     * }
     * 此时isRepeat就会返回ture
     * @paramAClass requires reflection of the annotations to be judged *@return* /
    private boolean isRepeat(Class<? extends Annotation> aClass){
        // The basic idea is that if you annotate yourself, you must repeat yourself
        Annotation[] declaredAnnotations = aClass.getDeclaredAnnotations();
        for (Annotation declaredAnnotation : declaredAnnotations) {
            if (declaredAnnotation.annotationType().isAssignableFrom(aClass))
                return true;
        }
        return false;
    }
Copy the code

The initialization contains the @BeanFactory annotation class, similar to @Configuration

 @SneakyThrows
    public void initFactory(a){
        / / the use of a new map to avoid side traversal, remove causes ConcurrentModificationException
        HashMap<String, Object> map = new HashMap<>();
        Collection<Object> beans = getBeans();
        for (Object o : beans) {
            // Filter out the unmarked ones first
            if (o.getClass().getAnnotation(FactoryBean.class) == null) {
                continue;
            }
            Method[] methods = o.getClass().getDeclaredMethods();
            for (Method method : methods) {
                Produce produce = method.getAnnotation(Produce.class);
                // Filter out unlabeled methods
                if(produce ! =null) {
                    method.setAccessible(true);
                    // As a rule, if a method name is provided, use the method name
                    Object result = invokeMethod(o, method);
                    if (produce.value().isBlank()){
                    map.put(method.getName(), result);
                    }else{ map.put(produce.value(),result); }}}}// It is not recommended that a bean may not be added to the container if the same name occurs
        //context.putAll(map);
        Set<Map.Entry<String, Object>> entries = map.entrySet();
        // Use set to avoid duplicate names
        for (Map.Entry<String, Object> entry : entries) {
            String beanName = entry.getKey();
            Object value = entry.getValue();
            if (context.containsKey(beanName)) {
                String msg = new StringBuilder().append("duplicate bean name: ")
                        .append(beanName)
                        .append("in")
                        .append(value.getClass())
                        .append(" and ")
                        .append(context.get(beanName).getClass()).toString();
                throw newRuntimeException(msg); } context.put(beanName, value); }}/** * by taking arguments of the corresponding type from the container, similar to setter injection *@paramO Instance of the method to be called *@paramMethod Specifies the method to be called@returnMethod returns the value *@throws IllegalAccessException
     * @throws InvocationTargetException
     * @author dreamlike_ocean
     */
    private Object invokeMethod(Object o, Method method) throws IllegalAccessException, InvocationTargetException {
        // Get the parameter listClass<? >[] parameterTypes = method.getParameterTypes(); method.setAccessible(true);
        int i = method.getParameterCount();
        // Prepare to store the arguments
        Object[] param = new Object[i];
        // Variable reuse, now it represents the current subscript
        i = 0;
        for(Class<? > parameterType : parameterTypes) { List<? > list = getBeanByType(parameterType);if (list.size() == 0) {
                throw new RuntimeException("not find " + parameterType + ". method :" + method + "class:" + o.getClass());
            }
            if(list.size() ! =1) {
                throw new RuntimeException("too many");
            }
            // Store the arguments temporarily
            param[i++] = list.get(0);
        }
        // Call the corresponding instance function
        return method.invoke(o, param);
    }

Copy the code

The exposed API for getting beans

    / * * * *@param beanName
     * @returnRemember to judge the null pointer *@author dreamlike_ocean
     */
    public Optional<Object> getBean(String beanName){
        return Optional.ofNullable(context.get(beanName));
    }

    / * * * *@param beanName
     * @param aclass
     * @param<T> The type to return, strong *@exceptionA strong ClassCastException may result in an unconverted exception *@return @author dreamlike_ocean
     */
    public<T> Optional<T> getBean(String beanName,Class<T> aclass){
        return Optional.ofNullable((T)context.get(beanName));
    }

    / * * * *@param interfaceType
     * @param <T>
     * @returnAll collections that inherit this interface *@author dreamlike_ocean
     */
    public<T> List<T> getBeanByInterfaceType(Class<T> interfaceType){
        if(! interfaceType.isInterface()) {throw new RuntimeException("it is not an interface type:"+interfaceType.getTypeName());
        }
        return context.values().stream()
                .filter(o -> interfaceType.isAssignableFrom(o.getClass()))
                .map(o -> (T)o)
                .collect(Collectors.toList());
    }

    / * * * *@param type
     * @param <T>
     * @returnAll sets of this type *@author dreamlike_ocean
     */
    
    public<T> List<T> getBeanByType(Class<T> type){
        return context.values().stream()
                .filter(o -> type.isAssignableFrom(o.getClass()))
                .map(o -> (T)o)
                .collect(Collectors.toList());
    }

    / * * * *@returnGet all values *@author dreamlike_ocean 
     */
    public Collection<Object> getBeans(a){
        return context.values();
    }

    / * * * *@returnGet container@author dreamlike_ocean
     */
    public Map<String,Object> getContext(a){
        return context;
    }

Copy the code

IOC constructor

Ok, now that the basic load bean functionality is complete, remember the original constructor above?

It doesn’t matter if you don’t remember. Let me write it again

So why do I scan the @Part annotation classes first

Because @BeanFactory needs @Part as raw material to produce a new @Part annotation class

public IOCContainer(String packName){
        try {
            initPart(packName);
            initFactory();
        } catch(IOException e) { e.printStackTrace(); }}public IOCContainer(a){
        this("");
    }
Copy the code

DI

All of the above are Java beans with no arguments. This is far from what I expected. I wanted a painting and he gave me a blank sheet of paper. This is not gonna work! On the DI module, give him the whole thing!

To distinguish between injection by type and injection by name, I write two annotations to distinguish

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectByName {
    String value(a);
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
public @interface InjectByType {

}

Copy the code

DI must first know which container to inject into, so it passes one through the constructor

 private IOCContainer iocContainer;
    public DI(IOCContainer iocContainer) {
        Objects.requireNonNull(iocContainer);
        this.iocContainer = iocContainer;
    }
Copy the code

First, the fields are injected by type

/ * * * *@paramO Classes that need to be injected *@author dreamlike_ocean          
     */

    private void InjectFieldByType(Object o){
        try {
            // Get all the internal fields
            Field[] declaredFields = o.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
                // Determine whether the current field has an annotation identifier
                if(field.getAnnotation(InjectByType.class) ! =null) {
                    // Prevent exceptions from being thrown because of private
                    field.setAccessible(true);
                    List list = iocContainer.getBeanByType(field.getType());
                    // If it cannot be found, the injection fails
                    // Here I choose to throw an exception or assign it to null
                    if(list.size() == 0) {throw new RuntimeException("not find "+field.getType());
                    }
                    // More than one injection fails, as with Spring
                    if(list.size()! =1) {throw new RuntimeException("too many");
                    }
                    // Normal injection
                    field.set(o, list.get(0)); }}}catch(IllegalAccessException e) { e.printStackTrace(); }}Copy the code

Inject fields by name

 / * * * *@paramO Classes that need to be injected *@author dreamlike_ocean
     */
    private void InjectFieldByName(Object o){
        try {
            Field[] declaredFields = o.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
                InjectByName annotation = field.getAnnotation(InjectByName.class);
                if(annotation ! =null) {
                    field.setAccessible(true);
                    // Look for the injected value through the bean name in the annotation
                    // Here the optional class fails to take advantage of its own functionality because I don't think it would be nice to write exception handling inside lambda expressions
                    // To borrow a phrase from Stack Overflow, Oracle is messing with lambda with checked exceptions
                    Object v = iocContainer.getBean(annotation.value()).get();
                    if(v ! =null) {
                        field.set(o, v);
                    }else{
                        // If no exception is found
                        throw new RuntimeException("not find "+field.getType()); }}}}catch(IllegalAccessException e) { e.printStackTrace(); }}Copy the code

Inject functions by type

 /** * this function must be a setter function *@paramO Class to be injected *@author dreamlike_ocean
     */
    private void InjectMethod(Object o){
        Method[] declaredMethods = o.getClass().getDeclaredMethods();
        try {
            for (Method method : declaredMethods) {
                // Get the function to annotate
                if(method.getAnnotation(InjectByType.class) ! =null) {
                    // Get the parameter listClass<? >[] parameterTypes = method.getParameterTypes(); method.setAccessible(true);
                    int i = method.getParameterCount();
                    // Prepare to store the arguments
                    Object[] param = new Object[i];
                    // Variable reuse, now it represents the current subscript
                    i=0;
                    for(Class<? > parameterType : parameterTypes) { List<? > list = iocContainer.getBeanByType(parameterType);if(list.size() == 0) {throw new RuntimeException("not find "+parameterType+". method :"+method+"class:"+o.getClass());
                        }
                        if(list.size()! =1) {throw new RuntimeException("too many");
                        }
                        // Store the arguments temporarily
                        param[i++] = list.get(0);
                    }
                    // Call the corresponding instance functionmethod.invoke(o, param); }}}catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch(InvocationTargetException e) { e.printStackTrace(); }}Copy the code

You’ll notice that these are all private methods, because I want to expose a neat API

  /** * fields are injected by type, then injected by name, and then injected by setter methods@author dreamlike_ocean
     */
    public void inject(a){
        iocContainer.getBeans().forEach(o -> {
            InjectFieldByType(o);
            InjectFieldByName(o);
            InjectMethod(o);
        });
    }
Copy the code

test

Here we go. Let’s test it out

@FactoryBean
class BeanFactory{
    public BeanFactory(a){}
    @Produce
    public LoginUser register(A a,B b,C c){
        return newLoginUser(); }}@Part("testA")
class A{
   // @InjectByType
    private B b;
    public A(a){}public B getB(a) {
        return b;
    }
    @InjectByType
    public void setB(B b) {
        this.b = b; }}@Part
class B{
    private UUID uuid;
public B(a){
    uuid = UUID.randomUUID();
}

    public UUID getUuid(a) {
        returnuuid; }}@Part
class C{
    @InjectByType
    private A a;
    public C(a){}public A getA(a) {
        returna; }}Copy the code

Test Method 1

@Test
public void test(a){
 IOCContainer container = new IOCContainer();
       DI di = new DI(container);
       di.inject();
       System.out.println(container.getBeanByType(A.class).get(0).getB().getUuid());
       System.out.println(container.getBeanByType(B.class).get(0).getUuid());
}
Copy the code

Test Method 2

   @Test
public void test(a){
       IOCContainer container = new IOCContainer();
       DI di = new DI(container);
       di.inject();
       container.getContext().forEach((k,v)-> System.out.println(k));


}

Copy the code

All right, that’s it

Attached engineering structure