欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《柒柒架构》DDD领域驱动设计--领域模型(二)

程序员文章站 2022-07-15 12:41:06
...

《柒柒架构》DDD领域驱动设计--领域模型(二)

前言

上篇文章已经讲到仓储模型的实现,本篇文章将继续详细介绍仓储实现的细节和应用。

仓储实现

上文最后我们完成了抽象类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接口了。

小结

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

相关标签: DDD JAVA