一、一个简单注解案例实现步骤
以练代学,通过自行实现一个注解学习注解知识。
现在实现一个常见的字段注入注解@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
(运行时保留,可以通过反射获取到)。
因为要注入的是字段,所以Target
为FIELD
;由于需要运行时注入,所以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
为一个接口,有Duck
和Panda
两个实现类。通过@Autowired
注解传入对应的名称,实现将具体类型对象注入到一个Animal类型的变量。
也即是说,对于一个Animal类型的变量,当注解为@Autowired("animal_duck")
时,注入对应的Duck
对象,当注解为@Autowired("animal_panda")
时,注入对应的Panda
对象。
1. 思路
- 新增
InjectName
注解,标记Animal
实现类的名称; - 在
Injector
中扫描并保存所有标注了InjectName
的类,在调用Injector.inject
方法的时候进行匹配,将名称匹配成功的对象赋值给相应的变量。
2. 修改Animal类
更改Animal类为接口,并创建Duck
和Panda
实现类:
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容器