注解实例 - 实现一个简单的@Autowired运行时注解

一、一个简单注解案例实现步骤

以练代学,通过自行实现一个注解学习注解知识。
现在实现一个常见的字段注入注解@Autowired,希望通过这个注解可以实现依赖注入,自动给字段赋值。

    @Autowired
    public MyObject obj; // 实现自动注入

1. 声明一个注解

在Java中,声明一个注解和声明一个class类似,把class换成@interface即可(kotlin则是annotation class)。现在声明一个Autowired注解:

public @interface Autowired {
}

2. 添加Target和Retention

Target接收一个数组,表明这个注解可以标记在哪些元素上方,比如类注解、方法注解、字段注解等等。常见的有TYPE(类/接口)、FIELD(字段)、METHOD(方法)、PARAMETER(参数)、CONSTRUCTOR(构造方法)等等。

Retention接收一个RetentionPolicy枚举类型的变量,表明当前注解的保留时间。一共有三种:SOURCE(只在源码中保留)、CLASS(在字节码中保留,但在运行时被丢弃,默认项)、RUNTIME(运行时保留,可以通过反射获取到)。

因为要注入的是字段,所以TargetFIELD;由于需要运行时注入,所以Retention选择RUNTIME

此时Autowired注解如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}

这样注解的声明就完成了。

3. 定义注射器(Injector)类

现在需要一个类来定义注解的具体行为。

因为需要实现字段注入,所以命名为Injector
Injector类中声明inject方法,表明当前对象中有字段需要实现注入:

public class Injector {
    public static void inject(Object obj) {
    }
}

inject具体实现的方式:通过反射,扫描这个对象的所有字段,对标记了@Autowired的进行赋值。因为没有进行相关配置,所以这里简单的通过反射调用对应类的无参构造方法对象:

    public static void inject(Object obj) throws Exception {
        Class cls = obj.getClass();
        Field[] fields = cls.getFields();
        for (Field field : fields) {
            if (field.getAnnotation(Autowired.class) != null) {
                // 这个字段标记了@Autowired
                inject(obj, field);
            }
        }
    }

    private static void inject(Object target, Field field) throws Exception {
        Class fieldClass = field.getType();
        Object fieldObj = fieldClass.getConstructor().newInstance();
        field.setAccessible(true);
        field.set(target, fieldObj);
    }

4. 测试

新建Animal类,作为待注入字段的类型:

public class Animal {
    public void say() {
        System.out.println("I am an animal");
    }
}

新建测试类AnnotationTest,其中animal变量标注了@Autowired

public class AnnotationTest {
    @Autowired()
    public Animal animal;

    AnnotationTest() {
        Injector.inject(this);
    }
}

main方法中测试:

    public static void main(String[] args) {
        AnnotationTest test = new AnnotationTest();
        test.animal.say();
    }

输出:

I am an animal

可以看到,字段注入成功。

二、通过注解参数注入不同对象

增加需求

现在需求Animal为一个接口,有DuckPanda两个实现类。通过@Autowired注解传入对应的名称,实现将具体类型对象注入到一个Animal类型的变量。
也即是说,对于一个Animal类型的变量,当注解为@Autowired("animal_duck")时,注入对应的Duck对象,当注解为@Autowired("animal_panda")时,注入对应的Panda对象。

1. 思路

  • 新增InjectName注解,标记Animal实现类的名称;
  • Injector中扫描并保存所有标注了InjectName的类,在调用Injector.inject方法的时候进行匹配,将名称匹配成功的对象赋值给相应的变量。

2. 修改Animal类

更改Animal类为接口,并创建DuckPanda实现类:

public interface Animal {
    void say();
}
public class Duck implements Animal {
    @Override
    public void say() {
        System.out.println("I am a duck");
    }
}
public class Panda implements Animal {
    @Override
    public void say() {
        System.out.println("I am a panda");
    }
}

3. 修改Autowired注解

由于多了一个参数,所以需要在Autowired注解中加入一个String类型的参数,具体的方式是增加一个返回值为String的方法(kotlin直接把参数写在构造方法里面):

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
    String value();
}

4. 增加@InjectName注解

因为需要根据字符串判断注入对象,所以被注入的类也需要增加一个注解标记对应的名称。即:

@InjectName("animal_duck")
public class Duck implements Animal {……}

@InjectName("animal_panda")
public class Panda implements Animal {……}

InjectName定义如下:

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectName {
    String value();
}

因为名称为一个字符串,所以同样需要定义一个返回值为String的方法。

5. 修改Injector类

5.1 扫描保存所有标注了@InjectName的类

由于需要找到对应名称的类,所以需要在程序运行之后,扫描包下所有标注了@InjectName的类,并获取其名称,保存在一个Map中。
具体的扫描方式,Java和Android实现不同,百度一下即可(见扫描Java包下所有类)。这里给出Java程序的实现方式:

// 扫描包下所有类
public class ClassUtil {
    public static List scanClasses(ClassLoader classLoader, String packageName) throws Exception {
        String packageNameForUrl = packageName.replace(".", "/");
        URL url = classLoader.getResource(packageNameForUrl);
        File root = new File(url.toURI());
        List classList = new ArrayList<>();
        scanClasses(root, packageName, classList);
        return classList;
    }

    private static void scanClasses(File root, String packageName, List result) {
        for (File child : root.listFiles()) {
            String name = child.getName();
            if (child.isDirectory()) {
                scanClasses(child, packageName + "." + name, result);
            } else if (name.endsWith(".class")) {
                String className = packageName + "." + name.replace(".class", "");
                result.add(className);
            }
        }
    }
}

在Injector类中添加init方法,在其中扫描并保存所有标注了@InjectName的类:

    public static void init(ClassLoader classLoader, String pkgName) throws Exception {
        List classList = ClassUtil.scanClasses(classLoader, pkgName);
        for (String className : classList) {
            Class clazz = Class.forName(className);
            InjectName annotation = clazz.getAnnotation(InjectName.class);
            if (annotation != null) {
                String name = annotation.value();
                injectNameMap.put(name, clazz);
            }
        }
    }

5.2 修改inject方法,查找标注了@Autowired的字段,匹配对应的对象并注入

    public static void inject(Object obj) throws Exception {
        Class cls = obj.getClass();
        Field[] fields = cls.getFields();
        for (Field field : fields) {
            if (field.getAnnotation(Autowired.class) != null) {
                // 这个字段标记了@Autowired
                Autowired a = field.getAnnotation(Autowired.class);
                String name = a.value(); // 获取要注入的name
                Class clazz = injectNameMap.get(name); // 匹配要注入的类
                Object injectObj = clazz.newInstance(); // 要注入的对象
                field.setAccessible(true);
                field.set(obj, injectObj); // 注入
            }
        }
    }

6. 测试

先创建一个常量类,防止硬编码:

public class Animals {
    public static final String NAME_PANDA = "animal_panda";
    public static final String NAME_DUCK = "animal_duck";
}

编写测试类。在main方法中初始化Injector,并使用@Autowired注释标记变量animal

public class AnnotationTest {
    @Autowired(Animals.PANDA)
    public Animal panda;
    @Autowired(Animals.DUCK)
    public Animal duck;

    AnnotationTest() throws Exception {
        Injector.inject(this);
    }

    public void say() {
        panda.say();
        duck.say();
    }
}

输出:

I am a panda

三、进一步优化*

如果需要实现注入,则需要在每个需要注入的类构造方法中调用Injector.inject(this),这样略显繁琐。
如果需要实现类似于Spring那样的注解,需要实现一个外部容器,在外部容器中通过反射构造所有的对象并实现注入。因为原理都是一样的,并且需要改动和增加一些额外代码,所以这里就不展开了,具体源码放在了github中:链接https://github.com/LittleFogCat/JavaDemo/tree/master/src/top/littlefogcat/demolist/learn/annotation

效果如下:

// 组件类,通过@Component注解标记
@Component
class MyComponent {
    // 通过@Autowired注解注入字段
    @Autowired("my_name")
    MyBean bean;
}

// 注入类,通过@Bean注解标记
@Bean("my_name")
class MyBean {
    public void say() {
        System.out.println("I am MyBean.");
    }
}

class Main {
    public static void main(String[] args) {
        // 通过包名初始化容器,并执行注入
        ObjectsContainer container = ObjectsContainer.get("top.littlefogcat.demo");
        // 根据Class获取组件对象
        container.getComponent(MyComponent.class);
        // 获取注入的bean
        MyBean bean = container.bean;
        bean.say();
    }
}

三、参考资料

掘金:编译时注解Kapt 实现基础版butterKnife
CNBlogs:Java 扫描包下所有类(包括jar包)
Github/ARouter:Android扫描包中所有类
Java:实现一个简单的依赖注入/IoC容器

你可能感兴趣的:(注解实例 - 实现一个简单的@Autowired运行时注解)