美文网首页Android进阶
注解实例 - 实现一个简单的@Autowired运行时注解

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

作者: littlefogcat | 来源:发表于2021-06-17 20:26 被阅读0次

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

    以练代学,通过自行实现一个注解学习注解知识。
    现在实现一个常见的字段注入注解@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<String> scanClasses(ClassLoader classLoader, String packageName) throws Exception {
            String packageNameForUrl = packageName.replace(".", "/");
            URL url = classLoader.getResource(packageNameForUrl);
            File root = new File(url.toURI());
            List<String> classList = new ArrayList<>();
            scanClasses(root, packageName, classList);
            return classList;
        }
    
        private static void scanClasses(File root, String packageName, List<String> 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<String> 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运行时注解

        本文链接:https://www.haomeiwen.com/subject/jprcyltx.html