《柒柒架构》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接口了。

小结

这样,通用仓储对象就基本已经完成,那么该仓储对象将是如何运用到具体的业务代码中的呢?我将在下一篇文章中继续探讨。