《柒柒架构》DDD领域驱动设计--领域模型(二)
- 前言
- 仓储实现
- 聚合差异化监测
- 实现类
- 小TIPS:如何消除Mapper定义
- 小结
前言
上篇文章已经讲到仓储模型的实现,本篇文章将继续详细介绍仓储实现的细节和应用。
仓储实现
上文最后我们完成了抽象类RepositorySupport 的设计,实现了Repository接口的find、save、remove方法,同时又定义了四个抽象方法onInsert、onSelect、onUpdate、onDelete。
这四个方法的具体实现,需要完成对持久化的具体操作。
在看具体的RepositorySupport的实现类前,我们先看下上篇文章没有讲的:如何监测当前聚合对象与改聚合之前状态的差异。
聚合差异化监测
首先我们引入maven包:
<dependency>
<groupId>de.danielbechler</groupId>
<artifactId>java-object-diff</artifactId>
<version>0.95</version>
</dependency>
下面是该包的一个应用示例:
public class DiffUtil {
public static void main(String[] args) {
CustomInfo customInfo1 = getCustomInfo1();
//--------------------------------------------
CustomInfo customInfo2 = getCustomInfo2();
//-------------------------------------------
DiffNode diff = ObjectDifferBuilder.buildDefault().compare(customInfo2, customInfo1);
//---------------------------------------------
System.out.println(diff.hasChanges());
System.out.println("diffState = " + diff.getState());
diff.visit((node, visit) -> System.out.println(node.getPath() + " => " + node.getState()));
//----------------------------------------------
BeanMap beanMap = BeanMap.create(customInfo1);
//Map<String, Object> aggregateMap = BeanUtils.beanToMap(customInfo1);
beanMap.forEach((key, value) -> System.out.println("key:" + key + ",value:" + value.getClass().getSimpleName()));
}
public static CustomInfo getCustomInfo1(){
User user1 = new User("1", "name1");
Position position1 = new Position("p1", "pname1");
List<Position> positions1=new ArrayList<>();
positions1.add(position1);
CustomInfo customInfo1 = new CustomInfo(user1, positions1);
return customInfo1;
}
private static CustomInfo getCustomInfo2(){
User user1 = new User("1", "name1");
Position position1 = new Position("p1", "pname2");
List<Position> positions1=new ArrayList<>();
positions1.add(position1);
CustomInfo customInfo1 = new CustomInfo(user1, positions1);
return customInfo1;
}
}
大家可以自行运行测试一下。
在此工具的基础上,我对该工具进行了一个包装:
public interface DiffUtil {
static AggregateDiff diff(Aggregate o1, Aggregate o2) {
AggregateDiff aggregateDiff = new AggregateDiff();
DiffNode diff = ObjectDifferBuilder.buildDefault().compare(o1, o2);
aggregateDiff.setState(diff.getState());
diff.visitChildren((diffNode, visit) -> {
if (diffNode.getParentNode().isRootNode() && !diffNode.getValueType().getSimpleName().contains("List")
|| diffNode.getParentNode().getValueType().getSimpleName().contains("List")) {
EntityDiff entityDiff = new EntityDiff();
entityDiff.setState(diffNode.getState());
entityDiff.setFromEntity((Entity) diffNode.canonicalGet(o2));
entityDiff.setToEntity((Entity) diffNode.canonicalGet(o1));
aggregateDiff.getEntityDiffs().add(entityDiff);
}
});
return aggregateDiff;
}
@Data
@NoArgsConstructor
class AggregateDiff {
DiffNode.State state;
List<EntityDiff> entityDiffs = new ArrayList<>();
public AggregateDiff(DiffNode.State state) {
this.state = state;
}
}
@Data
@NoArgsConstructor
class EntityDiff {
DiffNode.State state;
Entity fromEntity;
Entity toEntity;
public EntityDiff(DiffNode.State state) {
this.state = state;
}
}
}
tips:
一般聚合对象中的实体,从持久化对象以及业务视角的角度视角来看,只会有Entity或者List<Entity>两种,因此在做Diff时,仅需要考虑这两种情况。
因此我们就可以使用上述工具,对聚合中不同状态的实体进行不同的操作:
public void save(Aggregate aggregate, Class c) throws Exception {
String aggregateContextId = c.getSimpleName() + "_" + aggregate.idValue();
Aggregate aggregateCache = this.aggregateContext.find(aggregateContextId);
if (aggregateCache == null) {//如果缓存失效,需要先恢复缓存
find(aggregate.idValue(), c);
}
DiffUtil.AggregateDiff aggregateDiff = this.aggregateContext.detectChanges(aggregateContextId, aggregate);
switch (aggregateDiff.getState()) {
case UNTOUCHED:
case ADDED:
case REMOVED:
case CHANGED:
List<DiffUtil.EntityDiff> entityDiffsChanged = aggregateDiff.getEntityDiffs();
for (DiffUtil.EntityDiff entityDiff : entityDiffsChanged) {
switch (entityDiff.getState()) {
case ADDED:
this.onInsert(entityDiff.getToEntity(), entityDiff.getToEntity().getClass());
break;
case CHANGED:
this.onUpdate(entityDiff.getToEntity(), BeanMap.create(entityDiff.getFromEntity()), entityDiff.getToEntity().getClass());
break;
case REMOVED:
this.onDelete(BeanMap.create(entityDiff.getFromEntity()), entityDiff.getFromEntity().getClass());
break;
}
}
break;
default:
break;
}
this.aggregateContext.attach(aggregateContextId, aggregate);
}
实现类
作者开发的《柒柒架构》对持久化使用的是mybatis-plus框架,底层使用的是MySQL数据库,在此基础上,我们看下仓储实现类是怎么实现的。
首先我们先配置好SqlSessionFactory:
@Bean(name = "hikariConfig")
@ConfigurationProperties(prefix = "spring.datasource")
public HikariConfig hikariConfig() {
HikariConfig hikariConfig = new HikariConfig();
return hikariConfig;
}
@Bean(name = "dataSource")
public DataSource dataSource(@Qualifier("hikariConfig") HikariConfig hikariConfig) {
return new HikariDataSource(hikariConfig);
}
@Bean
@ConditionalOnBean(DataSource.class)
public MapperScannerConfigurer mapperScannerConfigurer() throws Exception {
MapperAutoCompile.initClass();
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
String poPath = (String) YmlPropertiesUtils.getCommonYml("actable.model.pack");
poPath = poPath.substring(0, poPath.lastIndexOf(".po")) + ".mapper";
String basePackage = poPath + ",com.gitee.sunchenbin.mybatis.actable.dao.*";
mapperScannerConfigurer.setBasePackage(basePackage);
mapperScannerConfigurer.setSqlSessionFactoryBeanName("dddMybatisPlusSqlSessionFactory");
return mapperScannerConfigurer;
}
@Bean("dddMybatisPlusSqlSessionFactory")
@ConditionalOnBean(DataSource.class)
public SqlSessionFactory mybatisPlusSqlSessionFactoryBean(@Autowired DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(dataSource);
sqlSessionFactory.setMapperLocations(mybatisPlusResolveMapperLocations());
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true); //下划线转驼峰
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory.getObject();
}
private Resource[] mybatisPlusResolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath*:com/gitee/sunchenbin/mybatis/actable/mapping/*/*.xml");
String poPath = (String) YmlPropertiesUtils.getCommonYml("actable.model.pack");
String mapperPath = "classpath*:" + poPath.substring(0, poPath.lastIndexOf(".po")) + ".mapper";
mapperLocations.add(mapperPath);
List<Resource> resources = new ArrayList();
if (!CollectionUtils.isEmpty(mapperLocations)) {
for (String mapperLocation : mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
log.error("Get myBatis resources happened exception", e);
}
}
}
Resource[] resourcesArray = resources.toArray(new Resource[resources.size()]);
return resourcesArray;
}
其中笔者使用了mybatis-plus的自动建表的功能,当然这不是重点。
我们定义Entity对应得Mapper对象,如下所示
@Mapper
public interface CustomInfoMapper extends BaseMapper<CustomInfoPo> {
}
然后我们看下具体的实现类:
public class RepositoryImpl extends RepositorySupport {
@Override
public void onInsert(Entity entity, Class c) {
//1.获取DO对应的Mapper对象
BaseMapper mapper = getMapper(c);
//2.转换成相应的DO
Po aPo = PoAssemblerFactory.toPo(entity);
//3.执行Mapper处理
mapper.insert(aPo);
}
@Override
public List<Entity> onSelect(Map<String, Object> searchMap, Class c) {
//1.获取DO对应的Mapper对象
BaseMapper mapper = getMapper(c);
//2.执行Mapper处理
List<Po> pos = (List<Po>) mapper.selectByMap(searchMap);
//3.转化成相应的Aggregate
if (pos == null || pos.size() == 0) {
return null;
} else {
List<Entity> entities = new ArrayList<>();
for (Po po : pos) {
entities.add(PoAssemblerFactory.toEntity(po, c));
}
return entities;
}
}
@Override
public void onUpdate(Entity entity, Map<String, Object> searchMap, Class c) {
//1.获取DO对应的Mapper对象
BaseMapper mapper = getMapper(c);
//2.执行更新
mapper.update(entity, new UpdateWrapper(searchMap));
}
@Override
public void onDelete(Map<String, Object> searchMap, Class c) {
//1.获取DO对应的Mapper对象
BaseMapper mapper = getMapper(c);
//2.执行删除
mapper.deleteByMap(searchMap);
}
private BaseMapper getMapper(Class c) {
String className = c.getSimpleName();
String mapperName = className.substring(0, 1).toLowerCase().concat(className.substring(1)) + "Mapper";
return (BaseMapper) SpringContextUtil.getBean(mapperName);
}
}
其中getMapper方法是从Spring容器中取出相应的Mapper,执行操作。这里的Mapper都继承自mybatis-plus的BaseMapper对象,其中已经实现了基本的sql操作。我们知道,DDD概念中对聚合的持久化操作就是分别对Entity实体的持久化操作,而且都是根据聚合ID进行索引的,所以基本不会涉及很复杂的SQL处理,因此使用基本的BaseMapper就基本可以满足要求。
如果涉及到批量处理,对DDD而言,是另外一种处理方式,在这里我们还是先只讨论对具体聚合的业务操作的问题。
在RepositorySupport 抽象类中,我们实现了监测变更实体,在RepositoryImpl 实现类中,我们完成了对差异实体的状态更新。
小TIPS:如何消除Mapper定义
我们知道,按照我们目前的设想,我们无需对Mapper对象进行额外实现,使用BaseMapper便可完成我们的需求,那么我们如何不再手动定义Mapper呢?
- 首先我们扫描所有的PO(持久化对象)的包,获取所有的class对象
public class ScanClassUtil {
public static Set<Class<?>> loadClassesByPackages(String packages) throws Exception {
Set<Class<?>> classes = new HashSet<>();
String path = ResourceUtils.getURL("classpath:").getPath();
if (!StringUtils.isEmpty(packages)) {
for (String s : packages.split(",")) {
classes.addAll(ScanClassUtil.loadClasses((path + s).replace('.', '/')));
}
} else {
log.error("packages加载class文件失败");
}
return classes;
}
private static Set<Class<?>> loadClasses(String rootClassPath) throws Exception {
Set<Class<?>> classSet = Sets.newHashSet();
// 设置class文件所在根路径
File clazzPath = new File(rootClassPath);
// 记录加载.class文件的数量
int clazzCount = 0;
if (clazzPath.exists() && clazzPath.isDirectory()) {
// 获取路径长度
int clazzPathLen = clazzPath.getAbsolutePath().length() + 1;
Stack<File> stack = new Stack<>();
stack.push(clazzPath);
// 遍历类路径
while (!stack.isEmpty()) {
File path = stack.pop();
File[] classFiles = path.listFiles(new FileFilter() {
public boolean accept(File pathname) {
//只加载class文件
return pathname.isDirectory() || pathname.getName().endsWith(".class");
}
});
if (classFiles == null) {
break;
}
for (File subFile : classFiles) {
if (subFile.isDirectory()) {
stack.push(subFile);
} else {
if (clazzCount++ == 0) {
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
boolean accessible = method.isAccessible();
try {
if (!accessible) {
method.setAccessible(true);
}
// 设置类加载器
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
// 将当前类路径加入到类加载器中
method.invoke(classLoader, clazzPath.toURI().toURL());
} catch (Exception e) {
e.printStackTrace();
} finally {
method.setAccessible(accessible);
}
}
// 文件名称
String className = subFile.getAbsolutePath();
int beginIndex = className.indexOf("target\\classes") + 15;
className = className.substring(beginIndex, className.length() - 6);
//将/替换成. 得到全路径类名
className = className.replace(File.separatorChar, '.');
// 加载Class类
Class<?> aClass = Class.forName(className);
classSet.add(aClass);
log.info("读取应用程序类文件[class={" + className + "}]");
}
}
}
}
return classSet;
}
}
- 然后在进行Spring上下文,持久化配置bean初始化前,动态编译Mapper类:
public class MapperAutoCompile {
public static boolean initClass() {
String poPath = (String) YmlPropertiesUtils.getCommonYml("actable.model.pack");
MapperAutoCompile.compiler(poPath);
return true;
}
@SneakyThrows
private static void compiler(String packages) {
Set<Class<?>> classesByPackages = ScanClassUtil.loadClassesByPackages(packages);
for (Class c : classesByPackages) {
if (Po.class.isAssignableFrom(c)) {
String className = c.getSimpleName();
String classFullName = c.getName();
if (className.indexOf("Po") == -1) {
continue;
}
className = className.substring(0, className.lastIndexOf("Po"));
String buildBaseMapperJavaString = buildBaseMapperJava(className, classFullName);
compilerClass(className, buildBaseMapperJavaString);
}
}
}
private static String buildBaseMapperJava(String className, String classFullName) {
String poPath = (String) YmlPropertiesUtils.getCommonYml("actable.model.pack");
poPath = poPath.substring(0, poPath.indexOf(".po")) + ".mapper";
StringBuffer javaString = new StringBuffer();
javaString.append("package " + poPath + ";\n");
javaString.append("import com.baomidou.mybatisplus.core.mapper.BaseMapper;\n");
javaString.append("import org.apache.ibatis.annotations.Mapper;\n");
javaString.append("import " + classFullName + ";\n");
javaString.append("@Mapper\n");
javaString.append("public interface " + className + "Mapper extends BaseMapper<" + className + "Po" + "> {}\n");
//log.info(javaString.toString());
return javaString.toString();
}
public static void compilerClass(String className, String buildBaseMapperJavaString) throws IOException, IllegalArgumentException, SecurityException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector diagnostics = new DiagnosticCollector();
// 定义一个StringWriter类,用于写Java程序
StringWriter writer = new StringWriter();
PrintWriter out = new PrintWriter(writer);
// 开始写Java程序
out.println(buildBaseMapperJavaString);
out.close();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
// 为这段代码取个名子:HelloWorld
SimpleJavaFileObject file = (new MapperAutoCompile()).new JavaSourceFromString(className + "Mapper", writer.toString());
Iterable compilationUnits = Arrays.asList(file);
// options命令行选项
String poPath = (String) YmlPropertiesUtils.getCommonYml("actable.model.pack");
poPath = poPath.substring(0, poPath.indexOf(".po")) + "/mapper";
String mapperPath = ResourceUtils.getURL("classpath:").getPath();//+ poPath.replace('.', '/')
Iterable<String> options = Arrays.asList("-d", mapperPath);// 指定的路径一定要存在,javac不会自己创建文件夹
File mapperPathFile = new File(mapperPath);
if (!mapperPathFile.exists()) {//如果文件夹不存在
mapperPathFile.mkdir();//创建文件夹
}
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null,
compilationUnits);
boolean success = task.call();
log.info((success) ? className + "Mapper编译成功" : className + "Mapper编译失败");
if (success == false) {
for (Object d : diagnostics.getDiagnostics()) {
log.error(((Diagnostic) d).getMessage(null));
}
}
}
// 用于传递源程序的JavaSourceFromString类
class JavaSourceFromString extends SimpleJavaFileObject {
final String code;
JavaSourceFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
}
这样在springboot启动后,就会在编译目录下,自动编译出Mapper的class对象,这样就无需对每个PO对象创建相应的Mapper接口了。
小结
这样,通用仓储对象就基本已经完成,那么该仓储对象将是如何运用到具体的业务代码中的呢?我将在下一篇文章中继续探讨。