506. Java 反射 - 在 BeanFactory 中实现单例模式
1. 为什么要用单例 (Singleton)?
在企业级应用中,有些对象是全局唯一的,例如:
-
数据库连接服务 (
DBService) - 远程调用客户端 (REST Client、MQ 客户端)
- 配置中心服务
👉 如果每次都 new 一个,会浪费资源、产生性能问题,甚至引发错误。
👉 使用单例,确保只会创建一个实例,所有地方复用它。
2. 自定义 @Singleton 注解
我们通过注解来标记哪些类应该是单例的:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Singleton {}
用法示例:
@Singleton
public class DBService {
}
这样,BeanFactory 知道:DBService 需要单例管理。
3. 改造后的 BeanFactory
核心思想:
- 使用 ConcurrentMap 注册表 保存单例对象。
- 第一次请求时创建对象,并放入注册表。
- 以后再请求时,直接返回已存在的对象。
- 利用
ConcurrentMap.putIfAbsent()保证线程安全。
public enum BeanFactory {
INSTANCE;
private final ConcurrentMap<Class<?>, Object> registry = new ConcurrentHashMap<>();
public <T> T getInstanceOf(Class<T> beanClass, Object... arguments) {
try {
if (beanClass.isAnnotationPresent(Singleton.class)) {
// 已经有实例 → 直接返回
if (registry.containsKey(beanClass)) {
return (T) registry.get(beanClass);
}
// 没有 → 创建实例
T bean = instantiateBeanClass(beanClass, arguments);
// 放入注册表(原子操作)
registry.putIfAbsent(beanClass, bean);
// 返回最终存入的实例(可能不是自己创建的)
return (T) registry.get(beanClass);
} else {
// 普通类 → 每次创建新对象
return instantiateBeanClass(beanClass, arguments);
}
} catch (NoSuchMethodException | InvocationTargetException |
InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
throws NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
Class<?>[] argumentClasses =
Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
Constructor<T> beanConstructor = beanClass.getConstructor(argumentClasses);
return beanConstructor.newInstance(arguments);
}
}
4. 使用示例
void main() {
BeanFactory beanFactory = BeanFactory.INSTANCE;
// 普通类 → 每次创建新对象
Message message = beanFactory.getInstanceOf(Message.class, "Hello");
System.out.println("Message = " + message);
// 单例类 → 所有调用返回同一个实例
DBService dbService1 = beanFactory.getInstanceOf(DBService.class);
DBService dbService2 = beanFactory.getInstanceOf(DBService.class);
System.out.println("Instances of DBService are the same? " + (dbService1 == dbService2));
}
输出结果:
Message = Message[message=Hello]
Instances of DBService are the same? true
5.要点 🎓
-
线程安全:为什么要用
ConcurrentMap+putIfAbsent()?
👉 防止并发情况下创建多个实例。 -
为什么最后用
registry.get()返回?
👉 因为可能有别的线程赢得了竞争,它的实例才是最终被放进去的。 -
对比 Spring:Spring 的
@Scope("singleton")本质上就是类似的机制,只是功能更丰富。 -
延伸思考:
- 如果想要 “懒加载单例”,可以结合
Supplier<T>或者computeIfAbsent()。 - 如果 Bean 构造参数不同,是否还应该共享同一个实例?(讨论点)
- 如果想要 “懒加载单例”,可以结合