【Mybatis】MybatisPlus轻松实现数据动态可配置热脱敏

news2024/10/7 16:27:32

文章目录

  • 前言
  • 一、数据
    • 1.1、mybatis-plus 统一字段管理
    • 1.2、Map 映射如何实现字段自动填充
    • 1.3、数据权限
    • 1.4、TenantLineInnerInterceptor 的升级改造
    • 1.5、TenantLineInnerInterceptor 改造 2
    • 1.6、数据权限改造 3
  • 二、数据脱敏
  • 三、TypeHander
  • 总结

前言

网站开发功能是否全面是衡量的一个重要指标,往往我们还需要针对用户进行功能的拼接或者切割,这部分我愿称之为用户画像。在用户画像下我们还需要考虑的是如何做到精准控制。基于 Spring 我们可以轻松实现传统的 三元 管理用于精准控制用户的菜单、按钮、及资源。
再仔细思考想,难道控制住资源就是精准控制了吗?我决定将精准下方我想细化到数据层面。

一、数据

  • 上面说的其实总结一句话就是我想将数据控制住,不同的角色拥有不同的权限操作不同的数据。

在这里插入图片描述

  • 想要实现数据的权限控制,势必需要在数据中额外存储他的所属信息。我们可以将数据分成两部分: 主体信息+所属信息
  • 这对于 Spring 来说非常容易,我们只需要在每张表上新增部分字段即可完成。虽然理论上是没有问题的但是实现上我们需要将现存的项目所有的表都添加字段,而且对应的实体上需要映射出来。这个工作量非常大而且重复度极高。这里注意下我的用词 重复度极高,就个人而言我非常不喜欢重复度极高的工作,因为这无疑是枯燥乏味的。

1.1、mybatis-plus 统一字段管理

  • 为了规避掉单独处理每张表的问题,我们引入 mybatis-plus 的通用字段处理功能。你要问我你的项目中没有 mybatis-plus 或者你的 sql 没有遵循 mybatis 的基本要求映射该怎么办?那我只能说你必须改到 mybatis 上,或者就单独处理吧。我们这里默认是项目使用 mybatis 的。
  • mybatis-plus 中提供 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler 这个类专门用来处理字段填充。他的使用场景就是公共字段的额外处理。

在这里插入图片描述

  • 点进去会发现需要我们实现的就是上面截图的两个方法。通过方法名我们也能够理解分别是控制插入是公共字段填充和更新时的字段填充的逻辑。

在这里插入图片描述

  • 上面是实现的 MetaObjectHandler 的实现类。因为 mybatis-plus 主要是针对实现了实体映射的处理,mybatis 除了实体映射以外还提供了 Map 映射,为了能够兼容 Map 映射,所以这里我将公共字段的映射做了一层抽象,主要是为了后面实现 Map 字段管理的时候能够通用。

在这里插入图片描述

1.2、Map 映射如何实现字段自动填充

  • 既然是组件开发,Mybatis-Plus 提供的功能已经很强大了。但是保不齐项目中就存在这些硬骨头,好在经过一番研究通过拦截器是能够实现 Map 映射的字段填充的。

在这里插入图片描述

  • 其中使用的 Handler 实际上就是通用上面我们抽象的逻辑。具体代码见底部 github 链接。通过简单的封装我们就统一字段处理了。逻辑处理事没有问题这个时候我们需要将统一字段抽离成一个单独的实体类方便其他类继承。
    在这里插入图片描述

  • 有了这些之后,作为完美主义者我们还缺一个脚本用来更改数据库的变更。

ALTER TABLE {{table_name}} ADD create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间';
ALTER TABLE {{table_name}} ADD update_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间';
ALTER TABLE {{table_name}} ADD create_by VARCHAR(100) NULL COMMENT '创建人';
ALTER TABLE {{table_name}} ADD update_by VARCHAR(100) NULL COMMENT '更新人';
ALTER TABLE {{table_name}} ADD delete_flag INT NULL COMMENT '删除标记';
ALTER TABLE {{table_name}} ADD version INT NULL COMMENT '版本记录';
  • 然后通过脚本将 table_name 变量替换掉。因为实现上的脚本涉及到其他内容,所以这里具体脚本就不提供了。我大体上是通过 linux 的 sed 进行模式匹配后替换内容。这块读者可以根据自己擅长的领域进行脚本实现,推荐使用 python 或者 go 吧。linux 太老了,也请原谅我对 linux 的偏爱。

1.3、数据权限

  • 上面说了重点仅仅介绍了如何将数据所属的用户信息维护起来。本文的主题是如何进行数据权限控制,接下来我们将重点拉回本文。
  • 数据权限最常见的场景就是 SAAS 化,SAAS 的产生背景就是为了实现数据隔离,这点 mybatis-plus 也帮我们实现了 com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor 实现这个类我们就可以将数据进行隔离管理。当前它的实现基础也是每张被管理的表需要有一个公共统一字段。
  /**
     * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        List<String> ignoreTableList = new ArrayList<String>() {
            {
                add("t_tenant");
            }
        };
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new CustomTenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                try {
                    SaasPlusControlHandler saasPlusControlHandler = DynicApplicationUtils.getApplicationContext().getBean(SaasPlusControlHandler.class);
                    String customTenantId = saasPlusControlHandler.getCustomTenantId();
                    if (StringUtils.isNotEmpty(customTenantId)) {
                        return new StringValue(customTenantId);
                    }
                } catch (Exception e) {
                //    初始化启动忽略报错
                }
                //return new StringValue("0000-0000-0000-0000-0000-0000-0000-0001");
                String tenantId = YpUserUtil.getOriginTenantId();
                if (StringUtils.isEmpty(tenantId)) {
                    return null;
                }
                return new StringValue(tenantId);
            }

            // 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
            @Override
            public boolean ignoreTable(String tableName) {
                if (ignoreTableList.contains(tableName)) {
                    return true;
                }
                //return YpUserUtil.ignoreTable(tableName);
                return false;
            }
        }));
        // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
        // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
  • 我们只需要实现一个拦截器并指明其中的表名和字段名,mybatis-plus 就会自动帮我们实现统一的权限控制。使用 mybatisplus 提供的就比较受限制,因为它的理念是通过单一字段进行过滤,简单理解为下图

在这里插入图片描述

  • 但是往往我们的数据权限是跟角色相关,所以上述的 TenantLineInnerInterceptor 虽然可以实现数据权限功能,但是他的主要使用场景 saas 的多租户。

1.4、TenantLineInnerInterceptor 的升级改造

  • TenantLineInnerInterceptor 的主要场景是多租户,但是我们的数据权限控制并不是租户级别,而是更加细粒度的用户级别,该如何改造呢?其实很简单我们仍然使用 TenantLineInnerInterceptor 将 tenantId 完全理解成 userId 来识别即可以完成用户级别的所属。
  • 这样改的好处是完全不需要改动任何代码就能实现。如果我们不仅仅想实现用户级别数据权限还想事项与用户相关的比如部门权限,那么我们只需要将部门下的用户列表查出来,在 tenantId 比较的时候扩展他的 Expression 就可以了。比如默认实现的大于比较

在这里插入图片描述

  • 这里我没有细化去实现 In 比较,既然源码中已经实现了抽象我们去扩展那就是很简单的事情了。篇幅有限这里不做介绍了。

1.5、TenantLineInnerInterceptor 改造 2

  • mybatisplus 实现的原理无非就是接住拦截器进行 sql 拼接,我们完全可以自己实现一个拦截器去拼接我们的规则,基于此我们完全掌握了 sql 的主动权,这样就可以更加灵活的实现数据的权限控制。
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Component
@ConditionalOnProperty(prefix = "spring.tenant",name="enable",havingValue  = "true")
public class PermissionInterceptor implements Interceptor {
    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();

    @Autowired
    SaasHolderHandler saasHolderHandler;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取sql信息
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        System.out.println("原sql为: " + sql);
        // 获取元数据
        StatementHandler statementHandler2 = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler2);
        MetaObject metaResultSetHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // 获取调用方法
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        System.out.println("调用方法为: " + id);
        if (saasHolderHandler.isAuthSaas()) {
            return invocation.proceed();
        }
        Class<?> type = mappedStatement.getParameterMap().getType();
        // 注解查询
        Class clazz = Class.forName(className);
        Method method = clazz.getDeclaredMethod(methodName,type);
        if (method.isAnnotationPresent(TemporaryDisable.class)) {
            return invocation.proceed();
        }
        SimpleMetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
        MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        if (annotationMetadata.hasAnnotation(TemporaryDisable.class.getName())) {
            return invocation.proceed();
        }
        HashMap<String, Object> jwtMap = SecurityUtils.getJwtMap();
        /*Map<String, Object> jwtMap = new HashMap<>();
        jwtMap.put("startTenant", 0);
        jwtMap.put("endTenant", 1);*/
        String newSql = String.format("select * from (%s) `range` where tenant_id>=%s and tenant_id < %s", sql, jwtMap.getOrDefault("startTenant",0),jwtMap.getOrDefault("endTenant",1));
        // String newSql = "select * from account where permission in (\"advertise\")";
        System.out.println("修改后的sql为: " + newSql);
        Class boundClass = boundSql.getClass();
        Field field = boundClass.getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, newSql);
        return invocation.proceed();
    }
}
  • 我们拦截 sql 后将他包装一层之后就可以利用查询出来的 sql 进行各种的条件判断了。用户权限、部门权限、角色权限、角色组权限等等各种稀奇古怪的权限控制我们都可以实现了。比如之前项目遇到的实现数据的委托处理,我可以将自己的数据委托给某人或者某个角色代为处理,这个时候我们就需要有一个委托字段并且在 sql 中通过这个字段进行扩展过滤,这个复杂的逻辑我们完全可以在拼接的方式中实现。

在这里插入图片描述

  • 对比能够发现,出发的角度不同,我们控制的程度也不同,基于这种方式条件 sql 完全是我们开发者自主控制。
  • 但是他也存在缺点。自定义拦截器在处理查询是非常有效,但是想自定义拦截器去管理数据的填充这还是比较麻烦的,我们需要监听增加、更新的操作并识别出扩展字段,这点比较麻烦,我还是建议使用 TenantLineInnerInterceptor 的方式来管理,查询的时候使用我们自定义的拦截器来实现权限控制。这是其中一个缺点
  • 还有一个比较致命的缺点就是我们在 sql 的基础上进行条件判断,如果我们的 sql 压根没有查出来我们条件中的字段就会出现问题。
  • antlr 可以解析 sql 的语法树,从而我们能够得知道数据查询的字段。基于此技术我们完全可以由针对的原生 sql 进行单表的过滤,这样实现就完全没有问题,但是笔者测试过 antlr 存在性能问题,所以该改造方案也仅仅是理论方案。但是这条思路是可以走通下去。我这里就算抛砖引玉或者寻找志同道合的人有机会可以一起按照这个思路开发下去。

1.6、数据权限改造 3

  • 总结下上面两种的方法:第一种太死板;第二种存在局限性;为了能够解决上面两种缺陷,我们半定制化实现一套权限控制。
    在这里插入图片描述
@DataFilter(deptColumnName="dept_id",deptAlias = "a",userAlias = "a",userColumnName = "user_id",mapperMethodName = {"queryPage"})

上面的注解用于在一个查询的方法上,而该方法最终的 sql 中 dept_id 表示部门 id,user_id 表示用户 Id 信息。queryPage 就是 mapper 层上真正的查询方法。

二、数据脱敏

  • 上面我们从数据源管理以及数据权限两个方向分析了如何实现及相关的改造方案。在数据权限的衍生中还存在一个数据脱敏这个功能。

在这里插入图片描述

  • 我们上面实现的数据权限是针对数据行,而数据脱敏可以说是针对数据列来操作的。比如现在有个角色可以看到所有的用户数据,这点我们条件匹配即可完成,但是这个用户属于第三方用户,我不想讲用户的关键信息暴露给他,比如说用户的手机号、邮箱、生日、密码等机密信息。这个时候我们可以接住数据脱敏功能。
  • 在 mybatis-plus (mate) 中提供了 FieldEncrypt 注解用来加密我们指定的字段数据,也可以通过 SensitiveWordsProcessor 进行数据敏感检测替换。下来配置来源于网络
@Bean
    public IParamsProcessor paramsProcessor() {
        return new SensitiveWordsProcessor() {
 
            /**
             // 可以指定你需要拦截处理的请求地址,默认 /* 所有请求
             @Override public Collection<String> getUrlPatterns() {
             return super.getUrlPatterns();
             }
             */
 
            @Override
            public List<String> loadSensitiveWords() {
                // 这里的敏感词可以从数据库中读取,也可以本文方式获取,加载只会执行一次
                return sensitiveWordsMapper.selectList(Wrappers.<SensitiveWords>lambdaQuery().select(SensitiveWords::getWord))
                        .stream().map(t -> t.getWord()).collect(Collectors.toList());
            }
 
            @Override
            public String handle(String fieldName, String fieldValue, Collection<Emit> emits) {
                if (CollectionUtils.isNotEmpty(emits)) {
                    try {
                        // 这里可以过滤直接删除敏感词,也可以返回错误,提示界面删除敏感词
                        System.err.println("发现敏感词(" + fieldName + " = " + fieldValue + ")" +
                                "存在敏感词:" + toJson(emits));
                        String fv = fieldValue;
                        for (Emit emit : emits) {
                            fv = fv.replaceAll(emit.getKeyword(), "");
                        }
                        return fv;
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                return fieldValue;
            }
        };
    }
  • 两种都可实现我们所谓的数据脱敏。加密本身也是一种数据脱敏。但是上面的方式都过于死板,比如我想新增一个字段的加密就会需要重新开发,虽然开发量很少,但是重大项目发布周期本身就很繁琐,所以我就思考能不能实现一个热数据脱敏,可以实时生效的数据脱敏法。

三、TypeHander

  • 自定义数据脱敏主要是在数据看和 java 代码映射的时候进行拦截处理,这点 TypeHandler 恰好符合。TypeHandler 主要是类型处理器,将 sql 的数据类型映射成 java 数据类型的中专站,那么我们完全可以在转换的时候将我们指定的数据进行加密即脱敏。
    在这里插入图片描述

  • 上面是 JDBC 的处理流程,我们需要拦截的就是第五步 处理运行结果。对应到 mybatis 上就是 TypeHandler。

  • 那么我们只需要注册一个 TypeHandler 就可以实现映射时进行数据脱敏

@SneakyThrows
    @Override
    public String getNullableResult(ResultSet resultSet, String s) throws SQLException {
        String result = resultSet.getString(s);
        if (StringUtils.isEmpty(result)) {
            return result;
        }
        if (ControEncrypThreadLocal.ACCESS.equals(ControEncrypThreadLocal.getAccess())) {
            return result;
        }
        if (null == globalConfig) {
            globalConfig = SpringUtils.getBean(GlobalConfig.class);
        }
        if (null != globalConfig && "false".equals(globalConfig.getEncryp())) {
            return result;
        }
        EncrypService encrypService = SpringUtils.getBean(EncrypService.class);
        List<Encryp> encrypList = encrypService.selectMoreEncryp(s);
        if (CollectionUtils.isEmpty(encrypList)) {
            return result;
        }
        ResultSetMetaData metaData = resultSet.getMetaData();
        //((ResultSetMetaData) ((ResultSetMetaDataProxyImpl) metaData).raw).fields
        ResultSetMetaDataProxyImpl metaData1 = (ResultSetMetaDataProxyImpl) metaData;
        ResultSetMetaData resultSetMetaDataRaw = metaData1.getResultSetMetaDataRaw();
        Class<? extends ResultSetMetaData> aClass = resultSetMetaDataRaw.getClass();
        for (Field declaredField : aClass.getDeclaredFields()) {
            System.out.println("declaredField.getName() = " + declaredField.getName());
        }
        Field fields = aClass.getDeclaredField("fields");
        fields.setAccessible(true);
        Object o = fields.get(resultSetMetaDataRaw);
        com.mysql.cj.result.Field[] fieldArray = (com.mysql.cj.result.Field[]) o;
        List<com.mysql.cj.result.Field> fieldList = Arrays.asList(fieldArray);
        Map<String, com.mysql.cj.result.Field> collect = fieldList.stream().collect(Collectors.toMap(com.mysql.cj.result.Field::getOriginalName, a -> a, (k1, k2) -> k1));
        if (collect.containsKey(s)) {
            com.mysql.cj.result.Field field = collect.get(s);
            String originalTableName = field.getOriginalTableName();
            String databaseName = field.getDatabaseName();
            Map<String, Encryp> encrypCollect = encrypList.stream().collect(Collectors.toMap(item -> {
                return String.format("%s-%s-%s", item.getDatabaseName(), item.getTableName(), item.getColumnName());
            }, a -> a, (k1, k2) -> k1));
            if (encrypCollect.containsKey(String.format("%s-%s-%s",databaseName, originalTableName, s))) {
                return encrypService.encryp(s);
            }
        }
        return result;
    }
  • 上面哪些字段需要加密我是通过读取配置的方式进行开发的,这样就能够达成热部署的方式进行字段控制了。这段代码虽然逻辑简单但是如何获取字段却花费了好长时间,值得你收藏哦。
    TypeHandler 开发好还需要指定数据库类型才能生效。

在这里插入图片描述

  • 到此我们在 mybatis 映射时 String 字段类型的就可以通过配置进行管理是否数据脱敏了。这样对于我们的运营人员来说就方便很多了。比如每年特殊时期我们需要对某些数据进行脱敏,过了特殊时期后放开。

总结

  • 本文主要是思路的剖析,并没有将每个案例都完成的代码呈现出来,因为在项目中也不可能同时使用上述所有的场景。我仅讲核心代码提供,部分辅助型的如有需要可以下方留言给你提供思路。
  • mybatis-plus 功能已经很强大了。基本上我们主流的需求都存在,但是保不齐我们项目需要定制化开发。上述的功能基本上都是在 mybatis 的基础上进行额外的扩展,技术无好坏,主要是符合项目最重要。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/939226.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java 中级】一文精通 Spring MVC - 标签库 (八)

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

【JAVA基础——static关键字】

JAVA基础 static关键字 文章目录 JAVA基础概述静态成员与加载顺序 概述 static 变量&#xff1a;static变量由该类的所有对象共享&#xff0c;不需要创建对象也可使用。static 方法&#xff1a;允许直接访问&#xff0c;不需要创建对象也可被调用。如 main 方法。static 初始…

华为OD机试 - 求字符串中所有整数的最小和 - 逻辑分析(Java 2023 B卷 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#…

一个免费好用的全域数据集成平台

文章目录 全域数据集成平台RestCloud一、产品架构一、数据源管理二、离线数据集成三、实时数据集成四、监控中心五、对比总结六、离线数据集成实战1.新建mysql数据源2.离线数据集成3.执行同步数据 大家好&#xff0c;我是脚丫先生 (o^^o) 小伙伴们都知道&#xff0c;在之前数据…

无涯教程-机器学习 - 矩阵图函数

相关性是有关两个变量之间变化的指示&#xff0c;在前面的章节中&#xff0c;无涯教程讨论了Pearson的相关系数以及相关的重要性&#xff0c;可以绘制相关矩阵以显示哪个变量相对于另一个变量具有较高或较低的相关性。 在以下示例中&#xff0c;Python脚本将为Pima印度糖尿病数…

VMware虚拟机的安装以及安装CentOS系统

VMware虚拟机是一款非常受欢迎的虚拟化软件&#xff0c;它可以模拟一台计算机运行在另一台计算机上&#xff0c;从而实现在一台物理机器上运行多个虚拟机的目的。在本文中&#xff0c;我们将会介绍如何安装VMware虚拟机并在其中安装CentOS操作系统。 文章目录 &#x1f4c0;VMw…

Linux用户与组管理(03)(八)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、组管理 1、概述 2、用户信息查看 总结 前言 今天是学习用户与组管理的最后一节课&#xff0c;这节课主要是组管理的内容&#xff0c;希望能一起学习&#xff…

跨足多领域:人脸美颜SDK在医疗、娱乐和安全中的应用案例

随着科技的不断发展&#xff0c;人脸美颜技术不再局限于满足用户的审美需求&#xff0c;而是在医疗、娱乐和安全领域展现出了广泛的应用前景。本文将深入探讨人脸美颜SDK 在这三个领域中的创新应用案例&#xff0c;展示其在不同场景中的独特价值和潜力。 一、医疗领域 1、皮…

Leetcode86. 分隔链表

给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台…

2.3 【MySQL】命令行和配置文件中启动选项的区别

在命令行上指定的绝大部分启动选项都可以放到配置文件中&#xff0c;但是有一些选项是专门为命令行设计的&#xff0c;比方说defaults-extra-file 、 defaults-file 这样的选项本身就是为了指定配置文件路径的&#xff0c;再放在配置文件中使用就没啥意义了。 如果同一个启动选…

winpe还原windows系统备份

准备工作 用大白菜制作一个启动u盘&#xff0c;里面可以镜系统备份文件 插入电脑&#xff0c;启动&#xff0c;按f11&#xff08;这个快捷键因电脑而异&#xff09;&#xff0c;选择启动u盘&#xff0c;进入winpe 硬盘格式化 选择分区助手软件 选择硬盘&#xff0c;右键选择【…

Spring Bean对象生命周期

文章目录 前言基础通俗理解bean作用域 前言 最近学习spring的一些基础概念&#xff0c;所以就先了解了bean对象的概念&#xff0c;而且发现这个里面涉及到很多的内容&#xff0c;比如在spring中一个bean对象是如何创建以及销毁的这些概念&#xff0c;所以就打算总结一些spring…

Spring MVC 学习总结

学习目标 了解 Spring MVC 是什么&#xff0c;为什么要使用它或者说它能解决什么问题&#xff0c;其与 Spring 是什么关系。理解为什么配置 Spring MVC 的前端控制器的映射路径为 “/” 会导致静态资源访问不了&#xff0c;掌握怎么处理这个问题。掌握基于注解方式使用 Spring…

分享一个vue-slot插槽使用场景

需求再现 <el-table-column align"center" label"状态" prop"mitStatus" show-overflow-tooltip />在这里&#xff0c;我想对于状态进行一个三目判断&#xff0c;如果为0那就是进行中&#xff0c;否则就是已完成&#xff0c;期初我是这样写…

九大常见数据结构

常用的数据结构可根据数据访问的特点分为线性结构和非线性结构。线性结构包括常见的链表、栈、队列等&#xff0c;非线性结构包括树、图等。 1 数组 数组可以说是最基本最常见的数据结构。数组一般用来存储相同类型的数据&#xff0c;可通过数组名和下标进行数据的访问和更新。…

self instruct 技术

《SELF-INSTRUCT: Aligning Language Model with Self Generated Instructions》 github: self-instruct 背景 大模型表现惊人&#xff0c;但是严重依赖于人工编写的指令数据。本文中提出self-instruct框架&#xff1a;一种基于大模型自动生成指令数据的方法。 主要步骤 1. …

[ DPU / SmartNIC/ 网卡 ]系统级的测试验证

开局一张图&#xff0c;其他慢慢来编 信雅纳DPU测试解决方案荣获第三届DPU峰会的“匠芯技术奖” 看完这张&#xff0c;小编再送一张&#xff1a;&#xff1a;&#xff1a; 网卡进化 更智能的加速卡SmartNIC/DPU 例如&#xff0c;将部分网络协议处理的功能卸载到 DPU 网卡&am…

Flutter(九)Flutter动画简介

1.动画简介 Animation、Curve、Controller、Tween这四个角色&#xff0c;它们一起配合来完成一个完整动画 Animation Animation是抽象类&#xff0c;和UI渲染没有关系&#xff0c;功能是保存动画的插值和状态&#xff1b;比较常用的是Animation addListener&#xff1a;帧监听…

EXCEL中点击单元格,所在行和列都改变颜色

1、打开VBA编辑环境。 2、选中需要添加程序的Sheet页面。 3、粘贴如下代码在编辑区域并保存后关闭。 Private Sub Worksheet_SelectionChange(ByVal Target As Excel.Range) On Error Resume Next Cells.FormatConditions.Delete // 如下代码是行变&#xff0c;在粘贴到VBA中时…

【项目经理】如何说话有条理

如何说话有条理 1. PREP法则2. SCRTV模型3. FFC赞美法则4. RIDE 说服法则 1. PREP法则 2. SCRTV模型 3. FFC赞美法则 4. RIDE 说服法则