如何在运行时注入并切换数据库/数据源?
咱先理清楚核心思路:你需要把两种数据访问方式都实现成FooDao接口的具体类,然后用一个中间层来根据运行时的选择,动态调用对应的实现。Spring的依赖注入可以帮我们轻松管理这些实现类,再结合ThreadLocal来存储当前要使用的数据源类型,就能实现无感知的切换了。下面是具体的实现步骤和代码示例:
具体实现方案
1. 定义数据源类型枚举
先搞一个枚举来标识不同的数据源类型,方便后续切换时指定:
public enum DataStoreType { DATABASE, // 数据库存储 FILE // 文件存储 }
2. 实现两种数据访问的FooDao接口
基于JPA的数据库实现
把现有的FooRepository包装成FooDao的实现类,适配接口方法:
@Repository public class DatabaseFooDao implements FooDao { private final FooRepository fooRepository; // 构造函数注入Spring自动生成的FooRepository实现 public DatabaseFooDao(FooRepository fooRepository) { this.fooRepository = fooRepository; } @Override public Collection<Foo> getAll() { return fooRepository.findAll(); } @Override public Foo getById(Long id) { return fooRepository.findById(id).orElse(null); } @Override public void removeById(Long id) { fooRepository.deleteById(id); } @Override public void update(Foo foo) { fooRepository.save(foo); } @Override public void insert(Foo foo) { fooRepository.save(foo); } }
文件存储的实现
自己写文件读写逻辑,这里用JSON文件做示例(需要引入Jackson依赖):
@Repository public class FileFooDao implements FooDao { // 自定义存储文件路径,实际项目可以放配置文件里 private final String storagePath = "foos_data.json"; private final ObjectMapper objectMapper = new ObjectMapper(); @Override public Collection<Foo> getAll() { try (FileReader reader = new FileReader(storagePath)) { return objectMapper.readValue(reader, new TypeReference<List<Foo>>() {}); } catch (IOException e) { // 文件不存在时返回空集合 return new ArrayList<>(); } } @Override public Foo getById(Long id) { return getAll().stream() .filter(foo -> foo.getId().equals(id)) .findFirst() .orElse(null); } @Override public void removeById(Long id) { List<Foo> foos = new ArrayList<>(getAll()); foos.removeIf(foo -> foo.getId().equals(id)); saveToFile(foos); } @Override public void update(Foo foo) { List<Foo> foos = new ArrayList<>(getAll()); foos.replaceAll(f -> f.getId().equals(foo.getId()) ? foo : f); saveToFile(foos); } @Override public void insert(Foo foo) { List<Foo> foos = new ArrayList<>(getAll()); foos.add(foo); saveToFile(foos); } // 私有方法:把Foo集合写入文件 private void saveToFile(Collection<Foo> foos) { try (FileWriter writer = new FileWriter(storagePath)) { objectMapper.writeValue(writer, foos); } catch (IOException e) { throw new RuntimeException("写入文件失败", e); } } }
3. 创建上下文持有者管理当前数据源
用ThreadLocal来存储当前线程要使用的数据源类型,避免多线程冲突:
public class DataStoreContextHolder { private static final ThreadLocal<DataStoreType> CONTEXT = new ThreadLocal<>(); // 设置当前数据源类型 public static void setDataStoreType(DataStoreType type) { CONTEXT.set(type); } // 获取当前数据源类型 public static DataStoreType getDataStoreType() { return CONTEXT.get(); } // 清理上下文,必须在使用完后调用 public static void clear() { CONTEXT.remove(); } }
4. 封装动态切换的服务类
创建一个服务类,把两种FooDao实现注入进来,根据上下文的数据源类型动态选择调用:
@Service public class FooDataService implements FooDao { private final Map<DataStoreType, FooDao> daoMap; // Spring会自动把所有FooDao的实现类注入到List中 public FooDataService(List<FooDao> daoList) { this.daoMap = daoList.stream() .collect(Collectors.toMap( dao -> { if (dao instanceof DatabaseFooDao) { return DataStoreType.DATABASE; } else if (dao instanceof FileFooDao) { return DataStoreType.FILE; } else { throw new IllegalArgumentException("未知的FooDao实现类:" + dao.getClass()); } }, Function.identity() )); } // 核心:获取当前要使用的Dao实现 private FooDao getCurrentDao() { DataStoreType type = DataStoreContextHolder.getDataStoreType(); // 如果没指定,默认用数据库 return daoMap.get(type != null ? type : DataStoreType.DATABASE); } // 下面都是委托给当前Dao实现的方法 @Override public Collection<Foo> getAll() { return getCurrentDao().getAll(); } @Override public Foo getById(Long id) { return getCurrentDao().getById(id); } @Override public void removeById(Long id) { getCurrentDao().removeById(id); } @Override public void update(Foo foo) { getCurrentDao().update(foo); } @Override public void insert(Foo foo) { getCurrentDao().insert(foo); } }
5. 业务代码中使用切换逻辑
在业务层,只需要先设置上下文的数据源类型,再调用FooDataService的方法即可:
@Service public class FooBusinessService { private final FooDataService fooDataService; public FooBusinessService(FooDataService fooDataService) { this.fooDataService = fooDataService; } // 使用文件存储处理业务 public void processWithFileStore() { try { // 设置当前使用文件数据源 DataStoreContextHolder.setDataStoreType(DataStoreType.FILE); // 调用数据操作方法,底层会自动用文件存储实现 fooDataService.insert(new Foo(1L, "文件存储的Foo")); Collection<Foo> fileFoos = fooDataService.getAll(); // ... 业务逻辑处理 } finally { // 必须清理上下文,避免线程复用导致的数据源残留 DataStoreContextHolder.clear(); } } // 使用数据库存储处理业务(默认,无需设置上下文) public void processWithDatabase() { fooDataService.insert(new Foo(2L, "数据库存储的Foo")); Foo dbFoo = fooDataService.getById(2L); // ... 业务逻辑处理 } }
注意事项
- ThreadLocal清理:一定要在
finally块中调用DataStoreContextHolder.clear(),不然线程池中的线程复用会导致数据源类型混乱。 - 事务处理:如果数据库实现需要事务,可以在
DatabaseFooDao的方法上添加@Transactional注解。 - 扩展性:以后要加新的数据源,只需要实现
FooDao接口,然后在FooDataService的映射逻辑中添加对应的枚举类型即可。 - 异常处理:文件存储的IO异常建议封装成自定义业务异常,不要直接抛出RuntimeException,方便上层统一处理。
内容的提问来源于stack exchange,提问作者wokadakow
相关产品推荐
相关产品推荐

