【源码解析】多数据源 dynamic-datasource快速入门及源码解析

news2024/11/17 2:35:36

快速入门

依赖引入

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

配置文件

spring.datasource.druid.stat-view-servlet.enabled = true
spring.datasource.druid.stat-view-servlet.login-username = root
spring.datasource.druid.stat-view-servlet.login-password = root
spring.datasource.dynamic.druid.filters = stat,wall
spring.datasource.druid.aop-patterns = com.charles.mapper.*
spring.datasource.druid.web-stat-filter.enabled = true
spring.datasource.druid.filter.stat.enabled = true
spring.datasource.druid.filter.stat.log-slow-sql = true
spring.datasource.druid.filter.stat.slow-sql-millis = 1000

spring.datasource.dynamic.primary = db0
spring.datasource.dynamic.datasource.db1.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.db1.url = jdbc:mysql://127.0.0.1:3306/eb-crm?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.dynamic.datasource.db1.username = app
spring.datasource.dynamic.datasource.db1.password = Ys@123456
spring.datasource.dynamic.datasource.db1.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.dynamic.datasource.db0.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.db0.url = jdbc:mysql://127.0.0.1:3306/newcrm?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.dynamic.datasource.db0.username = app
spring.datasource.dynamic.datasource.db0.password = Ys@123456
spring.datasource.dynamic.datasource.db0.type = com.alibaba.druid.pool.DruidDataSource

使用注解拦截

@DS("db1")
public interface TbpExcelLogMapper extends BaseMapper<TbpExcelLog> {
}

源码解析

启动的时候,会加载在dynamic-datasource-spring-boot-starter的jar包中的spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration

数据源加载

DynamicDataSourceAutoConfiguration会注入DynamicRoutingDataSource

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setPrimary(properties.getPrimary());
        dataSource.setStrict(properties.getStrict());
        dataSource.setStrategy(properties.getStrategy());
        dataSource.setP6spy(properties.getP6spy());
        dataSource.setSeata(properties.getSeata());
        return dataSource;
    }

DynamicRoutingDataSource#afterPropertiesSet,系统启动的时候会加载所有的数据源

    @Override
    public void afterPropertiesSet() throws Exception {
        // 检查开启了配置但没有相关依赖
        checkEnv();
        // 添加并分组数据源
        Map<String, DataSource> dataSources = new HashMap<>(16);
        for (DynamicDataSourceProvider provider : providers) {
            dataSources.putAll(provider.loadDataSources());
        }
        for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
            addDataSource(dsItem.getKey(), dsItem.getValue());
        }
        // 检测默认数据源是否设置
        if (groupDataSources.containsKey(primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
        } else if (dataSourceMap.containsKey(primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
        } else {
            log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
        }
    }

DynamicDataSourceAutoConfiguration会注入DynamicDataSourceProvider

    @Bean
    public DynamicDataSourceProvider ymlDynamicDataSourceProvider() {
        return new YmlDynamicDataSourceProvider(properties.getDatasource());
    }

AbstractDataSourceProvider#createDataSourceMap,根据配置文件中的信息创建数据源。

    protected Map<String, DataSource> createDataSourceMap(
            Map<String, DataSourceProperty> dataSourcePropertiesMap) {
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            String dsName = item.getKey();
            DataSourceProperty dataSourceProperty = item.getValue();
            String poolName = dataSourceProperty.getPoolName();
            if (poolName == null || "".equals(poolName)) {
                poolName = dsName;
            }
            dataSourceProperty.setPoolName(poolName);
            dataSourceMap.put(dsName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
        }
        return dataSourceMap;
    }

AbstractDataSourceCreator#createDataSource,模板方法。该方法会调用dataSourceInitEvent.beforeCreate(dataSourceProperty);。而doCreateDataSource交由每个子类来创建。

    @Override
    public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
        String publicKey = dataSourceProperty.getPublicKey();
        if (StringUtils.isEmpty(publicKey)) {
            publicKey = properties.getPublicKey();
            dataSourceProperty.setPublicKey(publicKey);
        }
        Boolean lazy = dataSourceProperty.getLazy();
        if (lazy == null) {
            lazy = properties.getLazy();
            dataSourceProperty.setLazy(lazy);
        }
        dataSourceInitEvent.beforeCreate(dataSourceProperty);
        DataSource dataSource = doCreateDataSource(dataSourceProperty);
        dataSourceInitEvent.afterCreate(dataSource);
        this.runScrip(dataSource, dataSourceProperty);
        return wrapDataSource(dataSource, dataSourceProperty);
    }

DruidDataSourceCreator#doCreateDataSource,创建druid数据源。根据全局配置gConfig和定制化配置结合来生成DruidDataSourcespring.datasource.dynamic.datasource.xxx.type=com.alibaba.druid.pool.DruidDataSource可以进行定制化配置,也可以通过spring.datasource.dynamic.druid全局配置druid的属性

    private DruidConfig gConfig;

    @Override
    public DataSource doCreateDataSource(DataSourceProperty dataSourceProperty) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(dataSourceProperty.getUsername());
        dataSource.setPassword(dataSourceProperty.getPassword());
        dataSource.setUrl(dataSourceProperty.getUrl());
        dataSource.setName(dataSourceProperty.getPoolName());
        String driverClassName = dataSourceProperty.getDriverClassName();
        if (!StringUtils.isEmpty(driverClassName)) {
            dataSource.setDriverClassName(driverClassName);
        }
        DruidConfig config = dataSourceProperty.getDruid();
        Properties properties = config.toProperties(gConfig);

        List<Filter> proxyFilters = this.initFilters(dataSourceProperty, properties.getProperty("druid.filters"));
        dataSource.setProxyFilters(proxyFilters);

        dataSource.configFromPropety(properties);
        //连接参数单独设置
        dataSource.setConnectProperties(config.getConnectionProperties());
        //设置druid内置properties不支持的的参数
        this.setParam(dataSource, config);

        if (Boolean.FALSE.equals(dataSourceProperty.getLazy())) {
            try {
                dataSource.init();
            } catch (SQLException e) {
                throw new ErrorCreateDataSourceException("druid create error", e);
            }
        }
        return dataSource;
    }

切面修改数据源的key

DynamicDataSourceAutoConfiguration会注入DynamicDataSourceAnnotationAdvisor

    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    @Bean
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX + ".aop", name = "enabled", havingValue = "true", matchIfMissing = true)
    public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDatasourceAopProperties aopProperties = properties.getAop();
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(aopProperties.getAllowedPublicOnly(), dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, DS.class);
        advisor.setOrder(aopProperties.getOrder());
        return advisor;
    }

DynamicDataSourceAnnotationAdvisor#buildPointcut,构造切面,对方法和类都拦截。

    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(annotation, true);
        Pointcut mpc = new AnnotationMethodPoint(annotation);
        return new ComposablePointcut(cpc).union(mpc);
    }

如果使用@Aspect来切面的话,对注解进行切片,要注意@within@annotation的区别。

 1.  @within 对象级别
 2.  @annotation 方法级别

DynamicDataSourceAnnotationInterceptor#invoke,获取方法上或者类上的注解中的key

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String dsKey = determineDatasourceKey(invocation);
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

DynamicDataSourceAnnotationInterceptor#determineDatasourceKey,如果key以#开头,则返回dsProcessor.determineDatasource(invocation, key),否则直接返回key。

    private String determineDatasourceKey(MethodInvocation invocation) {
        String key = dataSourceClassResolver.findKey(invocation.getMethod(), invocation.getThis());
        return key.startsWith(DYNAMIC_PREFIX) ? dsProcessor.determineDatasource(invocation, key) : key;
    }

DsProcessor

DynamicDataSourceAutoConfiguration会注入DsProcessor,使用过滤器链的模式进行处理。

    @Bean
    @ConditionalOnMissingBean
    public DsProcessor dsProcessor(BeanFactory beanFactory) {
        DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
        DsSessionProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
        headerProcessor.setNextProcessor(sessionProcessor);
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
    }

DsHeaderProcessor#doDetermineDatasource,如果方法上的注解类似于@DS(#header=tenat_id),那么切换路由的key是由header中tenat_id的值决定的。

    @Override
    public String doDetermineDatasource(MethodInvocation invocation, String key) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        return request.getHeader(key.substring(8));
    }

spelExpressionProcessor是支持表达式的。

在注解中配置@DS("#header=tenantName"),则从header里面去寻找key;写了@DS("#session=tenantName"),就从session里面获取以tenantName为键的值。而@DS("#tenantName")@DS("#user.tenantName")根据参数来获取。

获取数据源

获取连接,AbstractRoutingDataSource#getConnection()。首先获取数据源,从数据源中获取Connection

    @Override
    public Connection getConnection() throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
            return determineDataSource().getConnection();
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ds = StringUtils.isEmpty(ds) ? "default" : ds;
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
        }
    }

DynamicRoutingDataSource#determineDataSource,获取线程变量的数据,根据线程变量获取数据源。

    @Override
    public DataSource determineDataSource() {
        String dsKey = DynamicDataSourceContextHolder.peek();
        return getDataSource(dsKey);
    }

DynamicRoutingDataSource#getDataSource,该key为空,获取默认数据源,优先从groupDataSources里面获取数据源,然后再从dataSourceMap里面获取。

    public DataSource getDataSource(String ds) {
        if (StringUtils.isEmpty(ds)) {
            return determinePrimaryDataSource();
        } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return groupDataSources.get(ds).determineDataSource();
        } else if (dataSourceMap.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return dataSourceMap.get(ds);
        }
        if (strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
        }
        return determinePrimaryDataSource();
    }

DynamicDataSourceStrategy,从群组里面获取数据源需要配置获取策略,从集合中决定是哪一个,有随机和轮询。

public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrategy {

    @Override
    public String determineKey(List<String> dsNames) {
        return dsNames.get(ThreadLocalRandom.current().nextInt(dsNames.size()));
    }
}

比对一下,spring-jdbc中的动态数据源切换。

AbstractRoutingDataSource#afterPropertiesSet,重写了InitializingBean#afterPropertiesSet的方法。

    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

AbstractRoutingDataSource#getConnection()。获取lookupKey的方式由子类决定。

    public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }

    @Nullable
    protected abstract Object determineCurrentLookupKey();

多数据源事务

DynamicDataSourceAutoConfiguration会注入dynamicTransactionAdvisor,主要是拦截处理有DSTransactional注解的方法。

    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    @Bean
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
    public Advisor dynamicTransactionAdvisor() {
        DynamicLocalTransactionInterceptor interceptor = new DynamicLocalTransactionInterceptor();
        return new DynamicDataSourceAnnotationAdvisor(interceptor, DSTransactional.class);
    }

DynamicLocalTransactionInterceptor,开启事务。

public class DynamicLocalTransactionInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        if (!StringUtils.isEmpty(TransactionContext.getXID())) {
            return methodInvocation.proceed();
        }
        boolean state = true;
        Object o;
        LocalTxUtil.startTransaction();
        try {
            o = methodInvocation.proceed();
        } catch (Exception e) {
            state = false;
            throw e;
        } finally {
            if (state) {
                LocalTxUtil.commit();
            } else {
                LocalTxUtil.rollback();
            }
        }
        return o;
    }
}

LocalTxUtil#startTransaction

    public static void startTransaction() {
        if (!StringUtils.isEmpty(TransactionContext.getXID())) {
            log.debug("dynamic-datasource exist local tx [{}]", TransactionContext.getXID());
        } else {
            String xid = UUID.randomUUID().toString();
            TransactionContext.bind(xid);
            log.debug("dynamic-datasource start local tx [{}]", xid);
        }
    }

AbstractRoutingDataSource#getConnection(),判断是否存在事务id,如果存在,则获取连接,并封装成ConnectionProxy

    @Override
    public Connection getConnection() throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
            return determineDataSource().getConnection();
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ds = StringUtils.isEmpty(ds) ? "default" : ds;
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
        }
    }

    private Connection getConnectionProxy(String ds, Connection connection) {
        ConnectionProxy connectionProxy = new ConnectionProxy(connection, ds);
        ConnectionFactory.putConnection(ds, connectionProxy);
        return connectionProxy;
    }

ConnectionFactory#putConnection,存储到线程变量中的concurrentHashMap

    public static void putConnection(String ds, ConnectionProxy connection) {
        Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();
        if (!concurrentHashMap.containsKey(ds)) {
            try {
                connection.setAutoCommit(false);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            concurrentHashMap.put(ds, connection);
        }
    }

提交事务,LocalTxUtil#commit

    public static void commit() {
        ConnectionFactory.notify(true);
        log.debug("dynamic-datasource commit local tx [{}]", TransactionContext.getXID());
        TransactionContext.remove();
    }

ConnectionFactory#notify,对于线程变量中的连接全部处理(回滚或者提交),并清空线程变量。

    public static void notify(Boolean state) {
        try {
            Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();
            for (ConnectionProxy connectionProxy : concurrentHashMap.values()) {
                connectionProxy.notify(state);
            }
        } finally {
            CONNECTION_HOLDER.remove();
        }
    }

配置文件加密

DynamicDataSourceAutoConfiguration会注入EncDataSourceInitEvent

    @Bean
    @ConditionalOnMissingBean
    public DataSourceInitEvent dataSourceInitEvent() {
        return new EncDataSourceInitEvent();
    }

EncDataSourceInitEvent#beforeCreate,对加密的字符串进行解密

    @Override
    public void beforeCreate(DataSourceProperty dataSourceProperty) {
        String publicKey = dataSourceProperty.getPublicKey();
        if (StringUtils.hasText(publicKey)) {
            dataSourceProperty.setUrl(decrypt(publicKey, dataSourceProperty.getUrl()));
            dataSourceProperty.setUsername(decrypt(publicKey, dataSourceProperty.getUsername()));
            dataSourceProperty.setPassword(decrypt(publicKey, dataSourceProperty.getPassword()));
        }
    }

EncDataSourceInitEvent#decrypt,匹配正则表达式则进行解密。 Pattern ENC_PATTERN = Pattern.compile("^ENC\\((.*)\\)$");

    private String decrypt(String publicKey, String cipherText) {
        if (StringUtils.hasText(cipherText)) {
            Matcher matcher = ENC_PATTERN.matcher(cipherText);
            if (matcher.find()) {
                try {
                    return CryptoUtils.decrypt(publicKey, matcher.group(1));
                } catch (Exception e) {
                    log.error("DynamicDataSourceProperties.decrypt error ", e);
                }
            }
        }
        return cipherText;
    }

脚本执行器

AbstractDataSourceCreator#runScrip,用对应的数据源执行脚本

    private void runScrip(DataSource dataSource, DataSourceProperty dataSourceProperty) {
        DatasourceInitProperties initProperty = dataSourceProperty.getInit();
        String schema = initProperty.getSchema();
        String data = initProperty.getData();
        if (StringUtils.hasText(schema) || StringUtils.hasText(data)) {
            ScriptRunner scriptRunner = new ScriptRunner(initProperty.isContinueOnError(), initProperty.getSeparator());
            if (StringUtils.hasText(schema)) {
                scriptRunner.runScript(dataSource, schema);
            }
            if (StringUtils.hasText(data)) {
                scriptRunner.runScript(dataSource, data);
            }
        }
    }

主从插件

MasterSlaveAutoRoutingPlugin。当进行查询的时候,切换数据源SLAVE;当进行更新的时候,切换数据源,MASTER

@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
@Slf4j
public class MasterSlaveAutoRoutingPlugin implements Interceptor {

    @Autowired
    protected DataSource dynamicDataSource;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        String pushedDataSource = null;
        try {
            String dataSource = SqlCommandType.SELECT == ms.getSqlCommandType() ? DdConstants.SLAVE : DdConstants.MASTER;
            pushedDataSource = DynamicDataSourceContextHolder.push(dataSource);
            return invocation.proceed();
        } finally {
            if (pushedDataSource != null) {
                DynamicDataSourceContextHolder.poll();
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

在这里插入图片描述

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

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

相关文章

在不确定性时代,亚马逊云科技让企业拥有持续增长力

2023年3月29日「哈佛商业评论-未来管理沙龙」活动盛大启幕&#xff0c;此次沙龙活动以穿越周期的力量为主题方向&#xff0c;以解码跨国企业持续增长源动力为主旨&#xff0c;希望为企业高层管理者们带来更多思考和启迪。 作为特邀嘉宾&#xff0c;亚马逊全球副总裁、亚马逊云…

【数据库】数据库的基础知识

目录 前言 1、 查看数据库 1.1、查看所有数据库&#xff08;show databases;&#xff09; 1.2、创建数据库之后&#xff0c;查看创建的数据库的基本信息。 2、 创建数据库 2.1、直接创建数据库&#xff08;create database [数据库名];&#xff09; 2.2、创建数据库的时…

如何将b站缓存的m4s视频转换成mp4格式

阅读前请看一下&#xff1a;我是一个热衷于记录的人&#xff0c;每次写博客会反复研读&#xff0c;尽量不断提升博客质量。文章设置为仅粉丝可见&#xff0c;是因为写博客确实花了不少精力。希望互相进步谢谢&#xff01;&#xff01; 文章目录 阅读前请看一下&#xff1a;我是…

【Unity VR开发】结合VRTK4.0:瞬移

语录&#xff1a; 到不了的地方都叫做远方&#xff0c;回不去的世界都叫做家乡&#xff0c;我一直向往的却是比远更远的地方。 前言&#xff1a; 在VR场景中的移动主要有&#xff1a;瞬移和平移。瞬移相当于在虚拟世界中标记出目标位置&#xff0c;并自动传输到该位置&#xff…

【C++】5. 引用

文章目录 前言一、引用1.1 理解引用1.2 引用的特性1.3 引用的权限1.4 引用的使用场景1.4.1 做参数1.4.2 做返回值 1.5 引用的本质 前言 C语言中什么最难学&#xff1f;那当然就是指针了。不但使用起来麻烦&#xff0c;时不时还会产生一些意料之外的错误。C提供了一种方式&…

Direct local .aar file dependencies are not supported when building an AAR.

前言 起因&#xff1a;项目中含有视频播放功能&#xff0c;使用的是GSYVideoPlayer&#xff0c;因为公司网络问题经常依赖添加不了&#xff0c;所以将关于它的aar包全部下载下来直接本地依赖。 因为多个业务都可能涉及视频播放功能&#xff0c;为了复用&#xff0c;就想着将视频…

XTDrone PX4 仿真平台|使用Docker快速部署仿真环境

XTDrone PX4 仿真平台|使用Docker快速部署仿真环境 Docker简介NVIDIA驱动安装NVIDIA-Docker安装Docker镜像下载与使用Docker与宿主机建立ROS通信宿主机安装 XTDrone 源码 宿主机系统环境Ubuntu20.04 Docker简介 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包…

TestNG 中使用 Guice 来进行依赖注入

Guice是Google开发的一个轻量级&#xff0c;基于Java5&#xff08;主要运用泛型与注释特性&#xff09;的依赖注入框架(IOC)。 Guice非常小而且快。Guice是类型安全的&#xff0c;它能够对构造函数&#xff0c;属性&#xff0c;方法&#xff08;包含任意个参数的任意方法&…

3.微服务项目实战---Nacos Discovery--服务治理

3.1 服务治理介绍 先来思考一个问题 通过上一章的操作&#xff0c;我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址 &#xff08; ip &#xff0c;端口&#xff09;等硬编码到了代码中&#xff0c;这种做法存在许多问题&#xff1a; 一旦服务提供者地址…

什么?Python一行命令快速搭建HTTP服务器并公网访问?

文章目录 1.前言2.本地http服务器搭建2.1.Python的安装和设置2.2.Python服务器设置和测试 3.cpolar的安装和注册3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 转载自远程内网穿透的文章&#xff1a;【Python】快速简单搭建HTTP服务器并公网访问「cpolar内网穿透…

【C++入门必备知识:内联函数+指针空值nullptr】

【C入门必备知识&#xff1a;内联函数指针空值nullptr】 ①.内联函数Ⅰ.概念Ⅱ.宏与内联Ⅲ.总结 ②.指针空值nullptr(C11)Ⅰ.C98中的指针空值Ⅱ.注意&#xff1a; ①.内联函数 Ⅰ.概念 用inline修饰的函数就叫做内联函数&#xff0c;编译时C编译器会在调用内联函数的地方将函数…

密歇根大学,一个被低估的美国公立常春藤名校

密歇根大学&#xff08;University of Michigan&#xff09;创建于1817年&#xff0c;是美国历史最悠久的公立大学之一&#xff0c;被誉为“公立常春藤”和“公立大学的典范”&#xff0c;与加州大学伯克利分校和威斯康星大学麦迪逊分校等大学一起代表了美国公立大学的最高水平…

Unity Camera -- (1)概览

Camera章节笔记所用的资源包在这里&#xff1a; https://connect-prd-cdn.unity.com/20230208/a0898204-bc36-4d6e-a3b2-d4b83ae67c1d/CreativeCore_Camera_2021.3LTS.ziphttps://connect-prd-cdn.unity.com/20230208/a0898204-bc36-4d6e-a3b2-d4b83ae67c1d/CreativeCore_Came…

ERTEC200P-2 PROFINET设备完全开发手册(9-1)

9. PROFIDRIVE AC1/AC4参考代码 PROFIdrive是西门子 Profibus 与 Profinet 两种通讯方式针对驱动的生产与自动化控制应用的一种协议框架&#xff0c;也可以称作“行规”&#xff0c; PROFIdrive使得用户更快捷方便实现对驱动的控制。PROFIdrive的最大特点是互操作性 – 不同厂…

低代码平台名声臭,用起来却真香——60%开发者不敢承认

群体盲从意识会淹没个体的理性&#xff0c;个体一旦将自己归入该群体&#xff0c;其原本独立的理性就会被群体的无知疯狂所淹没。——《乌合之众》 不知道从什么时候开始&#xff0c;“低代码不行”的论调充斥着整个互联网圈子&#xff0c;csdn、掘金、知乎、B站、脉脉……到处…

遗传算法求取函数最值问题

目录 1. 关于遗传算法 2. 遗传算法的步骤 3. 代码实现 3.1 工具函数 3.1.1 目标函数 3.1.2 解码 3.1.3 交叉 3.1.4 变异 3.2 主函数部分 3.3 代码 4. 其他 1. 关于遗传算法 遗传算法是根据生物进化论提出的计算最优解的一种算法&#xff0c;核心思想是物竞天择&…

九龙证券|光模块概念股封单资金超3亿元,传媒板块涨停潮来袭

今天A股三大股指低开低走。沪深两市收盘共37股涨停。剔除4只ST股&#xff0c;合计33股涨停。另外&#xff0c;10股封板未遂&#xff0c;整体封板率为78.72%。 涨停战场&#xff1a; 华工科技封单资金超3亿元 从收盘涨停板封单量来看&#xff0c;同方股份封单量最高&#xff0…

【UE】制作简单的山脉地形

在上一篇博客中&#xff08;【UE】使用Quixel Bridge下载免费贴图&#xff09;&#xff0c;介绍了如何下载免费贴图&#xff0c;本篇博客介绍如何使用这些贴图制作地形贴图。 1. 创建地形 2. 用雕刻工具绘制地形 3. 新建两个材质函数&#xff0c;分别命名为“GrassAuto”、“R…

UWERANSIM - OAI5GC分立部署教程

环境&#xff1a; Ubantu18.04OAI-5GCv1.5.0UERANSIMv3.2.6 网络&#xff1a; Host1&#xff1a;OAI-5GCens37&#xff1a;192.168.12.3Host2&#xff1a;UERANSIMens40&#xff1a;192.168.12.33 确保两台宿主机之间互通&#xff01; 网络配置 Host1 网络&#xff1a;OA…

掌握 Web3 游戏数据分析,详述 4 个开发者需追踪的关键指标

引入&#xff1a;需要关注的关键指标包括哪些 区块链游戏在开发运营过程中需要追踪的关键指标包括红馆加密市场数据&#xff0c;DAU、MAU 和用户留存相关的用户数据、社交媒体参与数据&#xff0c;以及游戏内资产等生态系统相关数据。 主要观点&#xff1a; GameFi 项目与传统…