使用springboot也有一段时间了,最近在学习java反射的时候突发奇想,觉得可以用反射来做一个简单的自动注入。
我们都知道,Spring在启动的时候会创建一个IOC容器,并将创建好的bean放到容器里进行托管,在需要使用的时候就从容器里面获取,不需要重新创建。而在springboot项目中,service层只是负责业务逻辑,真正的执行是在Impl层。而且接口是不能被实例化的。因此不难猜测,在使用@Autowired注入service时,spring创建的bean不可能是userService,而是它的实现类。下面,我们就将利用java的反射机制,简单实现一下如何使用注解来进行自动注入。话不多说,下面直接开干。
既然是要使用注解进行注入,参照springboot,我们需要用到两个注解,分别是@Autowired以及@Service,对这两个注解的定义如下:
其中,@Autowired主要作用在需要注入的字段上,所以@Target选择ElementType.FIELD,而@Service主要是模仿spring用来标记实现类,作用于实现类,所以@Target选择ElementType.TYPE。
下面我们来模拟一个简单的业务逻辑:
现在有以下三个角色:School、Teacher、Student。School里面有Teacher和Student,Teacher的任务是教学,Student的任务是学习。而我们的目的是在School里面去注入Teacher和Student,然后分别去调用它们的方法。
根据需求,Teacher和Student的接口定义如下:
public interface IStudent {
void learn();
}
public interface ITeacher {
void teach();
}
实现类如下:
@Service
public class TeacherImpl implements ITeacher {
public void teach(){
System.out.println("teacher teach");
}
}
@Service
public class StudentImpl implements IStudent {
public void learn(){
System.out.println("student learn");
}
}
对School的定义如下:
public class School {
@Autowired
private IStudent student;
@Autowired
private ITeacher teacher;
public void work(){
teacher.teach();
student.learn();
}
public static void main(String[] args) {
School school = new School();
school.work();
}
}
显然,上述的代码是不能正常运行的,此时的@Autowired并没有起到任何作用,直接运行将会抛出NPE异常,因为我们并没有注入IStudent以及ITeacher的实现类。下面,我们就试着来用反射对它们进行注入。
这里涉及到一个问题,对于接口,我们是没有办法直接获取到它们的实现类的。但是JDK提供了一个方法,可以用来判断类是否实现了某个接口,那就是Class类里的isAssignableFrom()方法。借助这个方法,提供注入的思路可归纳如下:
- 获取当前类所声明的字段;
- 通过getAnnotation判断字段是否使用了@Autowired注解;
- 若该字段使用了@Autowired注解,获得该字段的类型(即接口类型);
- 获取Impl层实现类(重点),类似spring的包扫描策略。
- 遍历这些实现类,若该类使用了@Service注解,且实现了使用@Autowired注入的接口,则使用field.set()方法注入该接口的实现类。
其中,步骤4是最难也是最重要的一步。如何获得指定包下的类呢?所幸的是,JDK同样为我们提供了工具,那就是:ClassLoader。借助它,我们可以轻松获得指定包下的类,确切来说是获取类的全路径名称,然后再通过Class.forName方法,一个个地添加到List集合中。下面贴出实现代码:
private static List<Class> getClassesForPackage(String packageName) {
List<Class> classes = new ArrayList<>();
String className = "";
try {
ClassLoader cld = ClassLoader.getSystemClassLoader();
if (cld == null) {
throw new ClassNotFoundException("Can't get class loader.");
}
String packagePathStr = packageName.replace('.', '/');
Enumeration<URL> resources = cld.getResources(packagePathStr);
while (resources.hasMoreElements()) {
URL res = resources.nextElement();
String type = res.getProtocol();
if (type.equalsIgnoreCase("file")) {
File impl = new File(res.getPath());
File[] files = impl.listFiles();
if (files != null) {
for (File file : files) {
String name = file.getName();
name = name.substring(0, name.lastIndexOf("."));
className = packageName + "." + name;
try {
classes.add(Class.forName(className));
} catch (NoClassDefFoundError error) {
error.printStackTrace();
}
}
}
}
}
} catch (Throwable te) {
te.printStackTrace();
}
return classes;
}
进行到这,最难的步骤4已经解决了,我们的工作也已经完成了大半,下面就是根据接口创建实现类并进行注入了。同样先贴出实现代码:
public static void inject(Object source) {
String packageName = "com.nx.reflect.service.impl";
List<Class> clas = getClassesForPackage(packageName);
Field[] fields = source.getClass().getDeclaredFields();
for (Field field : fields) {
Autowired autowired = field.getAnnotation(Autowired.class);
if (autowired != null) {
try {
Class clazz = field.getType();
for (Class cla : clas) {
Service queries = (Service) cla.getAnnotation(Service.class);
// 添加了Service注解 且 实现了该接口
if (queries != null && clazz.isAssignableFrom(cla)) {
Object target = cla.newInstance();
field.setAccessible(true);
field.set(source, target);
}
}
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
至于代码里面的步骤,和前面描述的一致,这里就不再解释。可以新建一个AutoWired类,将上述两个方法放入其中。结束后文件目录如下:
至于使用,就十分的简单了,只需要在School的构造方法中调用AutoWired.inject(this);即可完成注入。
public School() {
AutoWired.inject(this);
}
此时再运行School中的Main方法,发现实现类已经成功注入了。
创作不易,喜欢的话请点个赞吧~~~///(^v^)\\\~~~