关键词:依赖注入(DI)、自定义注解、反射机制、控制反转(IoC)、Java元编程
摘要:本文将从0到1带您实现一个简单的自定义依赖注入框架,通过讲解注解定义、反射扫描、依赖解析等核心步骤,揭开Spring等框架中@Autowired注解的底层原理。即使您没接触过框架源码,也能通过生活类比和代码实战轻松理解。
很多开发者每天用Spring的@Autowired注解注入依赖,但很少有人真正理解“注解如何驱动对象创建”“依赖如何自动注入”的底层逻辑。本文将用纯Java代码实现一个迷你DI框架,覆盖:
本文将按照“概念铺垫→实现步骤→实战验证→原理扩展”的顺序展开,重点通过代码示例和生活类比降低理解门槛。
术语 | 通俗解释 |
---|---|
依赖注入(DI) | 就像点外卖时,外卖员(框架)主动把餐(依赖对象)送到你(当前对象)手中,不用自己去买 |
控制反转(IoC) | 对象的创建和管理权限从“自己new”交给“框架管家”,就像把钥匙交给物业统一管理房间 |
反射(Reflection) | Java的“透视镜”,能在运行时查看类的字段、方法,甚至修改私有变量 |
元注解 | 给注解打标签的注解(如@Retention定义注解存活周期),类似“标签的标签” |
假设你开了一家奶茶店(类),需要用到吸管(依赖对象)。以前你得自己去超市买(自己new),现在有个“外卖平台”(DI框架):
这个过程就是依赖注入的核心:用注解标记需求,框架自动完成对象的查找和注入。
注解就像“便签纸”,可以贴在类、字段、方法上做标记。比如:
类比:你在课本上贴便利贴(注解),写着“重点复习”(标记作用),老师(框架)看到后会特别关注。
反射是Java的“透视镜”,能在运行时:
类比:就像你有一个“万能钥匙”,能打开任何房间(类),看到里面的家具(字段),甚至调整家具位置(修改字段值)。
IoC容器是“对象的管家”,负责:
类比:小区的快递柜(容器),所有快递(对象)都存这里,你报取件码(类名)就能拿到快递(对象实例)。
这三个概念就像“快递三件套”:
用户类(如UserService)
↓(类上贴@MyComponent标签)
IoC容器(扫描包路径,收集所有@MyComponent类)
↓(解析类中的@MyInject字段)
反射机制(获取字段类型,从容器中查找对应实例)
↓(将实例注入字段)
完成依赖注入(UserService的userRepository字段有值了)
graph TD
A[定义@MyComponent注解] --> B[定义@MyInject注解]
B --> C[扫描指定包下所有类]
C --> D[筛选带@MyComponent的类]
D --> E[创建这些类的实例(单例)]
E --> F[将实例存入IoC容器(Map)]
F --> G[遍历所有实例的字段]
G --> H[筛选带@MyInject的字段]
H --> I[从容器中获取字段类型对应的实例]
I --> J[通过反射将实例注入字段]
J --> K[完成依赖注入]
要实现自定义依赖注入,需要完成5个关键步骤:
需要2个注解:
@MyComponent
:标记需要被容器管理的类(类似Spring的@Component)@MyInject
:标记需要注入依赖的字段(类似Spring的@Autowired)// 元注解说明:@MyComponent只能贴在类上,存活到运行时
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
// 可选:指定Bean名称,默认用类名首字母小写
String value() default "";
}
// @MyInject只能贴在字段上,存活到运行时
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInject {
}
需要扫描指定包下的所有类,找到带@MyComponent
的类。这里可以用Java的ClassPathScanningCandidateComponentProvider
(Spring提供的工具),但为了纯Java实现,我们手动实现一个简单的扫描器。
原理:通过类加载器获取包下的所有文件,筛选出.class文件,再加载为Class对象。
public class PackageScanner {
public static List<Class<?>> scan(String basePackage) throws Exception {
List<Class<?>> classes = new ArrayList<>();
// 将包名转为文件路径(如"com.example" → "com/example")
String path = basePackage.replace('.', '/');
// 获取类加载器,用于加载资源
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
File directory = new File(resource.getFile());
if (directory.exists()) {
// 递归遍历目录下的所有.class文件
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(".class")) {
// 提取类名(如UserService.class → UserService)
String className = basePackage + '.' + file.getName().substring(0, file.getName().length() - 6);
classes.add(Class.forName(className));
}
}
}
}
}
return classes;
}
}
筛选出带@MyComponent
的类后,需要创建它们的实例(默认单例),并存入容器(用Map保存,键是Bean名称,值是实例)。
public class MyApplicationContext {
// 单例Bean池:键是Bean名称(如"userService"),值是实例
private final Map<String, Object> singletonBeans = new HashMap<>();
public MyApplicationContext(String basePackage) throws Exception {
// 步骤1:扫描包,获取所有类
List<Class<?>> classes = PackageScanner.scan(basePackage);
// 步骤2:筛选带@MyComponent的类
List<Class<?>> componentClasses = classes.stream()
.filter(clazz -> clazz.isAnnotationPresent(MyComponent.class))
.collect(Collectors.toList());
// 步骤3:创建实例并注册到容器
registerBeans(componentClasses);
// 步骤4:注入依赖(关键!)
injectDependencies();
}
private void registerBeans(List<Class<?>> componentClasses) throws Exception {
for (Class<?> clazz : componentClasses) {
// 获取@MyComponent的value,默认类名首字母小写
MyComponent annotation = clazz.getAnnotation(MyComponent.class);
String beanName = annotation.value().isEmpty()
? toLowerCamelCase(clazz.getSimpleName())
: annotation.value();
// 创建实例(这里简化为无参构造)
Object instance = clazz.getDeclaredConstructor().newInstance();
singletonBeans.put(beanName, instance);
}
}
// 辅助方法:类名首字母小写(如UserService → userService)
private String toLowerCamelCase(String className) {
if (className == null || className.isEmpty()) {
return "";
}
return className.substring(0, 1).toLowerCase() + className.substring(1);
}
}
遍历所有已注册的Bean,检查每个字段是否有@MyInject
注解。如果有,就从容器中获取对应类型的Bean实例,通过反射注入。
public class MyApplicationContext {
// ...(前面的代码)
private void injectDependencies() throws Exception {
for (Map.Entry<String, Object> entry : singletonBeans.entrySet()) {
String beanName = entry.getKey();
Object beanInstance = entry.getValue();
// 获取Bean的所有字段(包括私有字段)
Field[] fields = beanInstance.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(MyInject.class)) {
// 获取字段类型(如UserRepository.class)
Class<?> fieldType = field.getType();
// 从容器中查找对应类型的Bean(简化处理:假设类型唯一)
Object dependency = findBeanByType(fieldType);
if (dependency != null) {
// 允许访问私有字段(重要!)
field.setAccessible(true);
// 注入值(将dependency赋值给beanInstance的这个字段)
field.set(beanInstance, dependency);
}
}
}
}
}
// 根据类型查找Bean(简化版:假设每个类型只有一个Bean)
private Object findBeanByType(Class<?> type) {
return singletonBeans.values().stream()
.filter(bean -> type.isInstance(bean))
.findFirst()
.orElseThrow(() -> new RuntimeException("未找到类型为" + type.getName() + "的Bean"));
}
}
现在我们有了一个迷你DI框架!接下来用实际案例验证。
依赖注入的本质是对象图的自动构建。假设我们有3个类:
UserController
(需要UserService
)UserService
(需要UserRepository
)UserRepository
(无依赖)它们的依赖关系可以表示为:
U s e r C o n t r o l l e r → U s e r S e r v i c e → U s e r R e p o s i t o r y UserController \rightarrow UserService \rightarrow UserRepository UserController→UserService→UserRepository
通过自定义注解标记后,框架需要构建这张对象图:
U s e r C o n t r o l l e r 实例 ← U s e r S e r v i c e 实例 ← U s e r R e p o s i t o r y 实例 UserController实例 \leftarrow UserService实例 \leftarrow UserRepository实例 UserController实例←UserService实例←UserRepository实例
// 1. 仓库层:操作数据库(无依赖)
@MyComponent // 标记为需要容器管理的Bean
public class UserRepository {
public void saveUser(String name) {
System.out.println("保存用户:" + name);
}
}
// 2. 服务层:调用仓库(依赖UserRepository)
@MyComponent
public class UserService {
@MyInject // 标记需要注入的依赖
private UserRepository userRepository;
public void createUser(String name) {
userRepository.saveUser(name); // 使用注入的依赖
}
}
// 3. 控制层:调用服务(依赖UserService)
@MyComponent
public class UserController {
@MyInject
private UserService userService;
public void handleCreateUser(String name) {
userService.createUser(name); // 使用注入的依赖
}
}
public class Main {
public static void main(String[] args) throws Exception {
// 创建容器,扫描"com.example.di"包(根据实际包名调整)
MyApplicationContext context = new MyApplicationContext("com.example.di");
// 从容器中获取UserController实例
UserController userController = (UserController) context.getBean("userController");
// 调用方法,验证依赖是否注入成功
userController.handleCreateUser("张三");
// 预期输出:保存用户:张三
}
}
@MyComponent
标记的类会被容器扫描并创建实例@MyInject
标记的字段会被自动注入对应类型的Bean虽然我们的迷你框架功能简单,但能说明以下场景:
AutowiredAnnotationBeanPostProcessor
(处理@Autowired的核心类)by lazy
等特性可能与DI结合,提供更简洁的语法field.setAccessible(true)
有什么潜在风险?(提示:与JVM安全策略有关)Q:为什么@MyInject的@Retention要设为RUNTIME?
A:因为我们需要在运行时通过反射获取字段上的注解。如果设为CLASS(编译期保留),运行时无法获取注解信息。
Q:如果Bean的构造方法有参数,如何处理?
A:当前示例只处理了无参构造。要支持有参构造,需要解析构造方法的参数类型,从容器中获取对应实例(类似Spring的构造注入)。
Q:循环依赖会导致什么问题?如何解决?
A:比如A依赖B,B依赖A,容器在创建A时需要先创建B,创建B时又需要A,导致死循环。简单解决方案是“提前暴露未初始化的实例”(Spring的三级缓存机制)。