springboot配置注入增强(三)自定义数据源/自定义解析方法

news2024/11/27 5:24:57

我们回忆下上一篇文章的内容,属性注入的关键节点是PropertySourcesPlaceholderConfigurer的BeanFactory后置处理器org.springframework.context.support.PropertySourcesPlaceholderConfigurer#postProcessBeanFactory,只有在执行这个方法前设置到Environment的数据源,才能被应用到springboot启动中所有属性注入的阶段,这样才能优雅的将自己的代码加入扩展点中,否则还得自己手动处理@value注解,手动绑定@ConfigurationProperties等

一 自定义数据源

上一篇文章说过任何地方都能加入数据源,但是我们通常需要在application.yml中读取一些属性来确定怎么获取数据源,比如获取数据源的地址、账号、密码什么的,那就需要我们在加载application.yml后再进行我们数据源的加载,下面我就介绍几个常用的场景

1 自定义EnvironmentPostProcessor接口

application.yml就是用的这个接口实现加载的,他是在ConfigDataEnvironmentPostProcessor#postProcessEnvironment中加载的,而这个方法的调用是EnvironmentPostProcessorApplicationListener在springboot启动时创建完Environment对象后收到的ApplicationEnvironmentPreparedEvent消息后,执行中spring.factories中定义的所有EnvironmentPostProcessor的接口,可以看到一些Environment中默认的数据源(random等)也用的这个接口来实现

所以我们也可以自定一EnvironmentPostProcessor接口来加入到springboot的启动流程中

EnvironmentPostProcessorApplicationListener在执行EnvironmentPostProcessor接口时是先进行排序根据order的顺序来依次执行,如果我们想在application.yml后执行,那么只需要比ConfigDataEnvironmentPostProcessor后执行即可,ConfigDataEnvironmentPostProcessor的order是Ordered.HIGHEST_PRECEDENCE + 10(这个+10就是springboot框架作为一个可扩展框架留给我们的扩展点,我们可以在+9或者+11来确定在他的前还是后执行我们的代码)

那我们只需要继承Ordered或用@Order注解将order设置为Ordered.HIGHEST_PRECEDENCE + 11即可,不能用PriorityOrdered,因为PriorityOrdered的类会优先于order类执行

public class EnhanceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        String userId = environment.getProperty("user.id");
        Map<String, Object> enhanceSource = new HashMap<>();
        enhanceSource.put("user." + userId + ".name", "EnhanceEnvironmentPostProcessor设置的名称");

        MutablePropertySources propertySources = environment.getPropertySources();
        MapPropertySource enhancePropertySource = new MapPropertySource("enhance", enhanceSource);
        propertySources.addFirst(enhancePropertySource);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 11;
    }
}

2 自定义ApplicationContextInitializer接口

springboot在SpringApplication构造器时会从spring.factories中获取所有EnvironmentPostProcessor的接口保存起来,并且在prepareContext阶段的applyInitializers()方法中以此去执行(也是根据order排好序的),因为这个阶段是在EnvironmentPostProcessor阶段后执行的,所以也可以获取到application.yml的属性

public class EnhanceApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        String userId = environment.getProperty("user.id");
        Map<String, Object> enhanceSource = new HashMap<>();
        enhanceSource.put("user." + userId + ".name", "EnhanceApplicationContextInitializer设置的名称");

        MutablePropertySources propertySources = environment.getPropertySources();
        MapPropertySource enhancePropertySource = new MapPropertySource("enhance", enhanceSource);
        propertySources.addFirst(enhancePropertySource);
    }
}

3 自定义BeanFactoryPostProcessor

BeanFactoryPostProcessor就是更靠后的一个阶段了,这种方式注入的时候不仅能获取到application.yml中的属性,还可以获取到分布式配置中心的属性。因为springboot第一次用到配置也就是Environment对象,是在PropertySourcesPlaceholderConfigurer中解析BeanDefinition的propertyValues(上一篇讲过),那么分布式也要在这之前配置进去,如

  • disconf就是自定义了一个PropertyPlaceholderConfigurer并在初始化这个bean的时候加载的配置
  • nacos则是用NacosConfigApplicationContextInitializer使用上述自定义ApplicationContextInitializer接口的方式加载的

而PropertySourcesPlaceholderConfigurer也是一个BeanFactoryPostProcessor,那我们只要在他之前加载即可,它使用了父类的PriorityOrdered设置的order

那我们只要比他优先一点就可以了,也继承PriorityOrdered,设置order为Ordered.LOWEST_PRECEDENCE-1

二 自定义属性解析

我们还是针对那四种获取属性的方式来执行自定义解析,BeanDefinition、@Value、@ConfigurationProperties、environment.getProperty。但environment.getProperty是在我们代码中调用的,我们完全可以自由控制对结果再执行自定义的解析方法,所以这个就没必要在讨论了,我们主要对另外三种springboot自动注入的属性(用户无感知)来做解析,比如我要写一个自定义解密的方法decode()

1 BeanDefinition

之前已经说过BeanDefinition是在org.springframework.beans.factory.config.PlaceholderConfigurerSupport#doProcessProperties中解析bean属性的

可以看到他是用的valueResolver来进行解析的,而springboot并没有在这留扩展点,所以我们需要,自己写一个StringValueResolver,并用这个解析器重新解析下BeanDefinition,而且我们要在PropertySourcesPlaceholderConfigurer之后执行,这样我们就能对springboot解析后的属性再进行一次解析,比如${user.123.name},springboot先会解析为decode(abc),然后我们这个再会将decode(abc)解析为123

   @Override
    public void postProcessBeanFactory(@Nullable ConfigurableListableBeanFactory beanFactory) throws BeansException {
        PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
        StringValueResolver valueResolver = strVal -> helper.replacePlaceholders(strVal, this::decodeValue);

        BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);

        String[] beanNames = beanFactory.getBeanDefinitionNames();
        for (String curName : beanNames) {
            if (!(curName.equals(this.beanName) && beanFactory.equals(this.beanFactory))) {
                BeanDefinition bd = beanFactory.getBeanDefinition(curName);
                try {
                    visitor.visitBeanDefinition(bd);
                } catch (Exception ex) {
                    throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
                }
            }
        }
    }
    private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
        Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
        return new PropertyPlaceholderHelper("decode(", ")",
                PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
    }

    public String decodeValue(String strVal) {
        if (strVal != null && strVal.equals("abc")) {
            return "123";
        }
        return strVal;
    }

可以看到,我用了PropertyPlaceholderHelper来帮助解析属性,并且设置了前缀"decode("和后缀")",replacePlaceholders方法有两个参数,一个是要解析的值,第二个是PlaceholderResolver对象,当调用replacePlaceholders方法时会先将字符串中所有的decode(xxx)的子串,依次递归先去掉前缀和后缀,然后再调用PlaceholderResolver对象的resolvePlaceholder方法(我们自定义的)进行解析,解析完成后的子串如果还有decode(xxx)的子串会接续递归执行上面的步骤直到没有前缀。springboot默认的${xxx}这种类型的解析也是这里处理的只不过它的PlaceholderResolver对象的resolvePlaceholder方法,是从数据源集合里面调用获取属性对应的值。

2 @Value

对于@value,之前也说过,他也是在执行PropertySourcesPlaceholderConfigurer时,将StringValueResolver添加到beanFactory中,等后面解析的时候从这里面获取,也就是和上面的BeanDefinition解析用的同一个StringValueResolver。幸运的是springboot对于解析@value留了扩展点(因为beanFactory中保存了StringValueResolver的集合,解析@value的时候是从这个集合中遍历,用每一个解析器来对上一个解析的结果再做解析)

所以只要把我们自定义的那个StringValueResolver也加到beanFactory中即可,即对刚才的代码加上一行beanFactory.addEmbeddedValueResolver(valueResolver);

3 @ConfigurationProperties

@ConfigurationProperties是由org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization来绑定的,之前也说过本质是是用的ConfigurationPropertiesBinder类的binder成员变量,而这个binder也是写死的生成方式,没有给扩展点。所以如果实现@ConfigurationProperties的自定义解析,那我们只能自定义binder,然后用binder来对@ConfigurationProperties的类来进行解析,或者利用反射来手动为ConfigurationPropertiesBinder的binder变量赋值

Binder中有数据源和解析方法,调用其bind方法时只要传入属性的前缀(prefix = "user.123")和要绑定的对象,即可对该目标对象进行属性绑定

ConfigurationPropertiesBindingPostProcessor在InitializingBean阶段中,从bean工厂中获取ConfigurationPropertiesBinder对象

并且注册这个bean时会判断是否已经有这个bean了,如果有的话就就不创建了,直接用已有的bean

所以针对这个@ConfigurationProperties的解析有以下几种方案

3.1 手动设置ConfigurationPropertiesBinder#binder(有侵入性,但是进行了一次绑定)

因为ConfigurationPropertiesBinder的作用域是friendly的,所以只能同一个包里能访问,我们只能反射来使用或者自己创建个org.springframework.boot.context.properties包,然后在里面写我们的替换逻辑,ConfigurationPropertiesBinder中的binder是私有的,所以要想设置上,只能用反射了

public class BinderPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {

    private Environment environment;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String beanName = "org.springframework.boot.context.internalConfigurationPropertiesBinder";
        Object configurationPropertiesBinder = beanFactory.getBean(beanName);
        try {
            //获取configurationPropertiesBinder的 binder 对象
            Field binderField = configurationPropertiesBinder.getClass().getDeclaredField("binder");
            binderField.setAccessible(true);
            Binder binder = (Binder) binderField.get(configurationPropertiesBinder);
            if (binder == null) {
                //如果binder为空,要先获取springboot定义的binder
                Method getBinder = configurationPropertiesBinder.getClass().getDeclaredMethod("getBinder");
                getBinder.setAccessible(true);
                binder = (Binder) getBinder.invoke(configurationPropertiesBinder);
            }
            //获取springboot原生binder的解析方法,解析${xxx}的
            Field placeholdersResolverField = binder.getClass().getDeclaredField("placeholdersResolver");
            placeholdersResolverField.setAccessible(true);
            PlaceholdersResolver springbootResolver = (PlaceholdersResolver) placeholdersResolverField.get(binder);
            //自定义的解析方法,解析decode(xxx)的
            PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
            PlaceholdersResolver myResolver = val -> helper.replacePlaceholders(String.valueOf(val), this::decodeValue);
            //将这两个解析方法组合到一起,先执行springboot的解析,对解析的结果在进行自定义的解析
            MutablePlaceholdersResolver mutablePlaceholdersResolver = new MutablePlaceholdersResolver(springbootResolver, myResolver);
            //将新的解析器设置回binder中
            placeholdersResolverField.set(binder, mutablePlaceholdersResolver);
        } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
        Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
        return new PropertyPlaceholderHelper("decode(", ")",
                PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
    }

    public String decodeValue(String strVal) {
        if (strVal != null && strVal.equals("abc")) {
            return "123";
        }
        return strVal;
    }

    @Override
    public void setEnvironment(@Nullable Environment environment) {
        this.environment = environment;
    }
}
public class MutablePlaceholdersResolver implements PlaceholdersResolver {

    private final PlaceholdersResolver[] placeholdersResolvers;

    public MutablePlaceholdersResolver(PlaceholdersResolver... placeholdersResolvers) {
        if (placeholdersResolvers == null) {
            throw new IllegalArgumentException("placeholdersResolvers is null");
        }
        this.placeholdersResolvers = placeholdersResolvers;
    }

    @Override
    public Object resolvePlaceholders(Object value) {
        for (PlaceholdersResolver placeholdersResolver : placeholdersResolvers) {
            value = placeholdersResolver.resolvePlaceholders(value);
        }
        return value;
    }
}

可以看到只是对原有的binder对象里面的placeholdersResolver解析器进行了修改,而且之前的placeholdersResolver也没去掉,是组合到一起使用。里面的那个MutablePlaceholdersResolver是自定义的一个解析器,他的作用是使用多个解析器对一个属性进行解析,我将binder对象原本的解析器和我自定义的decode解析器一同放到MutablePlaceholdersResolver里面,用这个MutablePlaceholdersResolver替换了原本的解析器,解析的时候先通过原本的解析器(解析${xxx})进行解析,然后再用decode解析器对解析结果进行解析。

3.2 自定义BeanPostProcessor(无侵入性,但是进行了两次绑定)

自定义一个BeanPostProcessor对ConfigurationPropertiesBindingPostProcessor已经绑定好的@ConfigurationProperties类进行二次解析,已知ConfigurationPropertiesBindingPostProcessor是实现的PriorityOrdered,如果想在他后面执行就比他优先级低可以,所以干脆就不实现order即可

@Slf4j
@Component
public class EnhanceConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, EnvironmentAware, InitializingBean {

    private Environment environment;

    private PlaceholdersResolver myResolver;

    @Override
    public void afterPropertiesSet() throws Exception {
        PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
        myResolver = val -> helper.replacePlaceholders(String.valueOf(val), this::decodeValue);
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        ConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);
        if (annotation != null) {
            try {
                String beanJson = JSON.toJSONString(bean, SerializerFeature.IgnoreNonFieldGetter);
                JSONObject source = JSON.parseObject(beanJson);

                Map<String, Object> target = new LinkedHashMap<>();
                EnhanceUtils.buildFlattenedMap(target, source, annotation.prefix());
                List<ConfigurationPropertySource> mapPropertySources = Collections.singletonList(new MapConfigurationPropertySource(target));

                Binder binder = new Binder(mapPropertySources, myResolver);
                binder.bind(annotation.prefix(), Bindable.ofInstance(bean));
            } catch (Exception e) {
                log.error("EnhanceConfigurationPropertiesBindingPostProcessor bind fail,beanName:{}", beanName, e);
            }
        }
        return bean;
    }

    private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
        Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
        return new PropertyPlaceholderHelper("decode(", ")",
                PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
    }

    public String decodeValue(String strVal) {
        if (strVal != null && strVal.equals("abc")) {
            return "123";
        }
        return strVal;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

}

可以看到这个Binder的sources我用的是这个bean的json转的,这样做的目的是,对这个bean的值再执行自定义的值解析,不重新从数据源里找值了,当然也可以用environment作为sources覆盖之前springboot赋的值,不过如果之前绑定的数据源没在environment里就没法自定义解析了,而且解析的方式还有加上springboot的那个解析方式,不然值是${}这样的属性就没办法解析了,如下修改上面的那个binder

Binder binder = new Binder(ConfigurationPropertySources.get(environment), myResolver);

可以看到${my.decode}并没有得到解析,所以还有修改Binder的第二个参数,也就是解析器要加上springboot的默认解析方式,参考3.1的MutablePlaceholdersResolver

PlaceholdersResolver springbootResolver = new PropertySourcesPlaceholdersResolver(environment);
MutablePlaceholdersResolver mutablePlaceholdersResolver = new MutablePlaceholdersResolver(springbootResolver, myResolver);
Binder binder = new Binder(ConfigurationPropertySources.get(environment), mutablePlaceholdersResolver);

三 简化开发

如果觉得一个个模块的设置比较麻烦,我在下一篇会写出一个框架,可以直接使用框架来自定义相关的数据源和解析

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

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

相关文章

竞赛 基于深度学习的中文情感分类 - 卷积神经网络 情感分类 情感分析 情感识别 评论情感分类

文章目录 1 前言2 情感文本分类2.1 参考论文2.2 输入层2.3 第一层卷积层&#xff1a;2.4 池化层&#xff1a;2.5 全连接softmax层&#xff1a;2.6 训练方案 3 实现3.1 sentence部分3.2 filters部分3.3 featuremaps部分3.4 1max部分3.5 concat1max部分3.6 关键代码 4 实现效果4.…

平均精度(AP)

什么是平均精度(AP) 平均精度 (AP)并不是精度 (P)的平均值。 平均精度 (AP) 是按类别计算的。 mAP&#xff08;mean average precision&#xff09;是一个平均值&#xff0c;常用作目标检测中的检测精度指标mAP 指标通过对于一个平均目标来检测任务中多个目标所对应不同 AP&a…

9.19号作业

2> 完成文本编辑器的保存工作 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QFontDialog> #include <QFont> #include <QMessageBox> #include <QDebug> #include <QColorDialog> #include <QColor&g…

Centos安装显卡

1、安装基础环境 yum -y install epel-release yum -y install gcc kernel-devel kernel-headers 2.对应内核版本 yum info kernel-devel kernel-headers Cat /proc/version 3、yum安装版本不对应。则去官网手动下载 离线安装对应的rpm&#xff1a; https://pkgs.org/dow…

电脑桌面的复选框如何取消

电脑桌面图标的复选框如何取消 1. 概述2. 去掉图标的复选框方法结束语 1. 概述 当你拿到新的电脑开机后&#xff0c;发现桌面上软件应用的图标左上角有个小框&#xff0c;每次点击图标都会显示&#xff0c;并且点击图标时&#xff0c;小框还会打上√&#xff1b; 这个小框的…

移动端APP测试-如何指定测试策略、测试标准?

制定项目的测试策略是一个重要的步骤&#xff0c;可以帮助测试团队明确测试目标、测试范围、测试方法、测试资源、测试风险等&#xff0c;从而提高测试效率和质量。本篇是一些经验总结&#xff0c;理论分享。并不是绝对正确的&#xff0c;也欢迎大家一起讨论。 文章目录 一、测…

activiti7的数据表和字段的解释

activiti7的数据表和字段的解释 activiti7版本有25张表&#xff0c;而activiti6有28张表&#xff0c;activiti5有27张表&#xff0c;绝大部分的表和字段的含义都是一样的&#xff0c;所以本次整理的activiti7数据表和字段的解释&#xff0c;也同样适用于activiti6和5。 1、总览…

higher-order function in functional programming (JS)

1 functional programming该怎么理解&#xff1f; functions就是values&#xff0c;就像String or Numbers那样&#xff0c;可以构造匿名函数&#xff0c;并把函数赋给某个变量 或者 传递给其他函数&#xff08;higher-order function&#xff09; 2 higher-order function有…

B树的定义和特点

1.多叉查找树的效率 策略1:m叉查找树中&#xff0c;规定除了根节点外&#xff0c;任何结点至少有[m/2]个分叉&#xff0c;即至少含有[m/2]-1个关键字。策略2:m叉查找树中&#xff0c;规定对于任何一个结点&#xff0c;其所有子树的高度都要相同。 而满足以上两种策略的树被称…

新手怎样快速上手接口测试?掌握这几个知识点直接起飞!

接口测试是测试系统组件间接口的一种方式&#xff0c;接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是检查数据的增删改查操作&#xff0c;以及系统之间的逻辑关系等。 接口的几种类型 接口的类型包括&#xff1a;post &#xff0c;get&…

Postman应用——控制台调试

当你在测试脚本中遇到错误或意外行为时&#xff0c;Postman控制台可以帮助你识别&#xff0c;通过将console.log调试语句与你的测试断言相结合&#xff0c;你可以检查http请求和响应的内容&#xff0c;以及变量之类的。 通常可以使用控制台日志来标记代码执行&#xff0c;有时…

Golang gorm 一对一关系

一对一关系 一对一关系比较少&#xff0c;一般用于表的扩展例如一张用户表&#xff0c;有很多字段那么就可以把它拆分为两张表&#xff0c;常用的字段放主表&#xff0c;不常用的字段放详情表。 针对用户表来说可以通过user去点出userinfo。 创建表和插入数据 package mainimp…

一款超强的 Python 分析工具!

这是一个star暴增的项目&#xff1a;PyGWalker&#xff0c;也是目前看来更加轻量级的分析工具&#xff01; 之前我们的数据分析思路是&#xff1a; 1、利用 R 或者 Python 进行数据分析‍ 2、利用沉跌跌的工具进行数据分析&#xff0c;比如&#xff1a;tableau 今天介绍的*…

Hive 数据仓库介绍

目录 ​编辑 一、Hive 概述 1.1 Hive产生的原因 1.2 Hive是什么&#xff1f; 1.3 Hive 特点 1.4 Hive生态链关系 二、Hive架构 2.1 架构图 2.2 架构组件说明 2.2.1 Interface 2.2.1.1 CLI 2.2.1.2 JDBC/ODBC 2.2.1.3 WebUI 2.2.2 MetaData 2.2.3 MetaStore 2.2…

tokenizers总结

简介 tokenize的目标是把输入的文本流&#xff0c;切分成一个个子串&#xff0c;每个子串相对有完整的语义&#xff0c;便于学习embedding表达和后续模型的使用。 tokenize有三种粒度&#xff1a;word/subword/char word词&#xff0c;是最自然的语言单元。对于英文等自然语…

通过数据导入导出功能批量重命名文件名称更简单

在日常工作中&#xff0c;我们经常需要对大量的文件进行重命名&#xff0c;以方便管理和查找。然而&#xff0c;手动一个一个修改文件名称的方式不仅费时费力&#xff0c;而且容易出错。为了提高工作效率&#xff0c;我们可以使用一款名为“固乔文件管家”的软件来实现文件的批…

【MySQL多表查询以及事务、索引】

1. 多表查询 1.1 概述 1.1.1 数据准备 #建议&#xff1a;创建新的数据库 create database db04; use db04;-- 部门表 create table tb_dept (id int unsigned primary key auto_increment comment 主键ID,name varchar(10) not null unique comment 部门名称…

基于自编译的onlyoffice镜像,关于修改字体的问题

基于自编译的onlyoffice镜像&#xff0c;关于修改字体的问题 自编译onlyoffice镜像来自于 https://blog.csdn.net/Gemini1995/article/details/132427908 该镜像里面没有documentserver-generate-allfonts.sh文件&#xff0c;所以需要自己创建一个&#xff08;建议放在/usr/b…

C++笔记之引用折叠规则

C笔记之引用折叠规则 文章目录 C笔记之引用折叠规则1. 当两个左值引用结合在一起时&#xff0c;它们会折叠成一个左值引用。2. 当一个左值引用和一个右值引用结合在一起时&#xff0c;它们会折叠成一个左值引用。3. 当两个右值引用结合在一起时&#xff0c;它们也会折叠成一个右…

Mybatis学习笔记9 动态SQL

Mybatis学习笔记8 查询返回专题_biubiubiu0706的博客-CSDN博客 动态SQL的业务场景&#xff1a; 例如 批量删除 get请求 uri?id18&id19&id20 或者post id18&id19&id20 String[] idsrequest.getParameterValues("id") 那么这句SQL是需要动态的 还…