• Springboot多数据源配置--数据源动态切换


    在上一篇我们介绍了多数据源,但是我们会发现在实际中我们很少直接获取数据源对象进行操作,我们常用的是jdbcTemplate或者是jpa进行操作数据库。那么这一节我们将要介绍怎么进行多数据源动态切换。添加本文实现的代码之后,只需要配置要数据源就可以直接通过注解使用,在实际使用的时候特别的简单。那么本章主要分以下几个步骤进行实战。

    本章大纲 写道
    (1)新建maven java project;
    (2)在pom.xml添加依赖包;
    (3)编写启动类App.java
    (4)编写配置文件application.properties;
    (5)动态数据源路由类;
    (6)注册多数据源;
    (7)测试类测试;

           接下来我们看看每一步具体的实现吧:

    (1)新建maven java project;

           新建一个maven project,取名为:spring-boot-multi-ds

    (2)在pom.xml添加依赖包;

           在pom.xml文件中加入依赖的库文件,主要是spring boot基本的,数据库驱动,spring-jpa支持即可,具体pom.xml文件如下:

    <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

      <modelVersion>4.0.0</modelVersion>

      <groupId>com.kfit</groupId>

      <artifactId>spring-boot-multids</artifactId>

      <version>0.0.1-SNAPSHOT</version>

      <packaging>jar</packaging>

      <name>spring-boot-multids</name>

      <url>http://maven.apache.org</url>

      <properties>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- jdk版本号,这里需要你本地进行的jdk进行修改,这里angel使用的是1.8的版本. -->

        <java.version>1.8</java.version>

      </properties>

       <!--

           spring boot 父节点依赖,

           引入这个之后相关的引入就不需要添加version配置,

           spring boot会自动选择最合适的版本进行添加。

           在这里使用的1.3.3版本,可能目前官方有最新的版本了,大家可以

           使用最新的版本。

         -->

        <parent>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-parent</artifactId>

           <version>1.3.3.RELEASE</version>

        </parent>

      <dependencies>

        <!-- 单元测试包,在这里没有使用到. -->

        <dependency>

          <groupId>junit</groupId>

          <artifactId>junit</artifactId>

          <scope>test</scope>

        </dependency>

       

        <!-- spring boot web支持:mvc,aop...

            这个是最基本的,基本每一个基本的demo都是需要引入的。

        -->

        <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

       

        <!-- mysql驱动.

           我们的demo是多数据源,在这里使用Mysql数据库.

        -->

        <dependency>

           <groupId>mysql</groupId>

           <artifactId>mysql-connector-java</artifactId>

        </dependency>

       

       

        <!-- spring jpa

           spring jpa中带有自带的tomcat数据连接池;

           在代码中我们也需要用到.

         -->

        <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-data-jpa</artifactId>

        </dependency>

       

      </dependencies>

    </project>

           在上面的配置文件中都有相应的解释,大家可以自己解读下。

    (3)编写启动类App.java

           编写spring boot的启动类:

    com.kfit.App:

    package com.kfit;

    import org.springframework.boot.SpringApplication;

    import org.springframework.boot.autoconfigure.SpringBootApplication;

    /**

     *

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    @SpringBootApplication

    public class App {

        public static void main(String[] args) {

           SpringApplication.run(App.class, args);

        }

    }

    (4)编写配置文件application.properties;

           在这里主要是多数据源和jpa的配置:

    src/main/resources/application.properties:

    ########################################################

    ###配置文件包括1个主数据源和多个数据源,

    ###其中主数据源在Spring中的beanName默认为dataSource,

    ###另外几个数据源的beanName分包为:ds1、ds2、ds3

    ###其中datasource的type属性可以具体指定到我们需要的数据源上面,

    ###不指定情况下默认为:org.apache.tomcat.jdbc.pool.DataSource

    ###当然你也可以把这些数据源配置到主dataSource数据库中,然后读取数据库生成多数据源。当然这样做的必要性并不大,难不成数据源还会经常变吗。

    ########################################################

    # 主数据源,默认的

    #spring.datasource.type=com.zaxxer.hikari.HikariDataSource

    spring.datasource.driverClassName=com.mysql.jdbc.Driver

    spring.datasource.url=jdbc:mysql://localhost:3306/test

    spring.datasource.username=root

    spring.datasource.password=root

    # 更多数据源

    custom.datasource.names=ds1,ds2,ds3

    #custom.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource

    custom.datasource.ds1.driverClassName =com.mysql.jdbc.Driver

    custom.datasource.ds1.url=jdbc:mysql://localhost:3306/test1

    custom.datasource.ds1.username=root

    custom.datasource.ds1.password=root

    #custom.datasource.ds2.type=com.zaxxer.hikari.HikariDataSource

    custom.datasource.ds2.driverClassName =com.mysql.jdbc.Driver

    custom.datasource.ds2.url=jdbc:mysql://localhost:3306/test

    custom.datasource.ds2.username=root

    custom.datasource.ds2.password=root

    #custom.datasource.ds3.type=com.zaxxer.hikari.HikariDataSource

    custom.datasource.ds3.driverClassName =com.mysql.jdbc.Driver

    custom.datasource.ds3.url=jdbc:mysql://localhost:3306/test

    custom.datasource.ds3.username=root

    custom.datasource.ds3.password=root

    # 下面为连接池的补充设置,应用到上面所有数据源中

    spring.datasource.maximum-pool-size=100

    spring.datasource.max-idle=10

    spring.datasource.max-wait=10000

    spring.datasource.min-idle=5

    spring.datasource.initial-size=5

    spring.datasource.validation-query=SELECT 1

    spring.datasource.test-on-borrow=false

    spring.datasource.test-while-idle=true

    spring.datasource.time-between-eviction-runs-millis=18800

    ########################################################

    ### Java Persistence Api

    ########################################################

    # Specify the DBMS

    spring.jpa.database = MYSQL

    # Show or not log for each sql query

    spring.jpa.show-sql = true

    # Hibernate ddl auto (create, create-drop, update)

    spring.jpa.hibernate.ddl-auto = update

    # Naming strategy

    #[org.hibernate.cfg.ImprovedNamingStrategy  #org.hibernate.cfg.DefaultNamingStrategy]

    spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.DefaultNamingStrategy

    # stripped before adding them to the entity manager)

    spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

    (5)动态数据源路由类;

           动态数据源能进行自动切换的核心就是spring底层提供了AbstractRoutingDataSource类进行数据源的路由的,我们主要继承这个类,实现里面的方法即可实现我们想要的,这里主要是实现方法:determineCurrentLookupKey(),而此方法只需要返回一个数据库的名称即可,所以我们核心的是有一个类来管理数据源的线程池,这个类才是动态数据源的核心处理类。还有另外就是我们使用aop技术在执行事务方法前进行数据源的切换。所以这里有几个需要编码的类,具体如下:

    动态数据源上下文>com.kfit.config.datasource.DynamicDataSourceContextHolder:

    package com.kfit.config.datasource.dynamic;

    import java.util.ArrayList;

    import java.util.List;

    /**

     * 动态数据源上下文.

     *

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    public class DynamicDataSourceContextHolder {

       

        /*

         * 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,

         * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

         */

        private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

        /*

         * 管理所有的数据源id;

         * 主要是为了判断数据源是否存在;

         */

        public static List<String> dataSourceIds = new ArrayList<String>();

        /**

         * 使用setDataSourceType设置当前的

         * @param dataSourceType

         */

        public static void setDataSourceType(String dataSourceType) {

           contextHolder.set(dataSourceType);

        }

        public static String getDataSourceType() {

           return contextHolder.get();

        }

        public static void clearDataSourceType() {

           contextHolder.remove();

        }

       

        /**

         * 判断指定DataSrouce当前是否存在

         *

         * @param dataSourceId

         * @return

         * @author SHANHY

         * @create  2016年1月24日

         */

        public static boolean containsDataSource(String dataSourceId){

            return dataSourceIds.contains(dataSourceId);

        }

    }

    数据源路由类>com.kfit.config.datasource.dynamic.DynamicDataSource:

    package com.kfit.config.datasource.dynamic;

    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

    /**

     * 动态数据源.

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    public class DynamicDataSource extends AbstractRoutingDataSource {

       

        /*

         * 代码中的determineCurrentLookupKey方法取得一个字符串,

         * 该字符串将与配置文件中的相应字符串进行匹配以定位数据源,配置文件,即applicationContext.xml文件中需要要如下代码:(non-Javadoc)

         * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()

         */

        @Override

        protected Object determineCurrentLookupKey() {

           /*

            * DynamicDataSourceContextHolder代码中使用setDataSourceType

            * 设置当前的数据源,在路由类中使用getDataSourceType进行获取,

            *  交给AbstractRoutingDataSource进行注入使用。

            */

           return DynamicDataSourceContextHolder.getDataSourceType();

        }

    }

    指定数据源注解类>com.kfit.config.datasource.dynamic.TargetDataSource:

    package com.kfit.config.datasource.dynamic;

    import java.lang.annotation.Documented;

    import java.lang.annotation.ElementType;

    import java.lang.annotation.Retention;

    import java.lang.annotation.RetentionPolicy;

    import java.lang.annotation.Target;

    /**

     * 在方法上使用,用于指定使用哪个数据源

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    @Target({ ElementType.METHOD, ElementType.TYPE })

    @Retention(RetentionPolicy.RUNTIME)

    @Documented

    public @interface TargetDataSource {

        String value();

    }

    切换数据源Advice>com.kfit.config.datasource.dynamic.DynamicDataSourceAspect:

    package com.kfit.config.datasource.dynamic;

    import org.aspectj.lang.JoinPoint;

    import org.aspectj.lang.annotation.After;

    import org.aspectj.lang.annotation.Aspect;

    import org.aspectj.lang.annotation.Before;

    import org.springframework.core.annotation.Order;

    import org.springframework.stereotype.Component;

    /**

     * 切换数据源Advice

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    @Aspect

    @Order(-10)//保证该AOP在@Transactional之前执行

    @Component

    public class DynamicDataSourceAspect {

       

        /*

         * @Before("@annotation(ds)")

         * 的意思是:

         *

         * @Before:在方法执行之前进行执行:

         * @annotation(targetDataSource):

         * 会拦截注解targetDataSource的方法,否则不拦截;

         */

        @Before("@annotation(targetDataSource)")

        public void changeDataSource(JoinPoint point, TargetDataSourcetargetDataSource) throws Throwable {

           //获取当前的指定的数据源;

            String dsId = targetDataSource.value();

            //如果不在我们注入的所有的数据源范围之内,那么输出警告信息,系统自动使用默认的数据源。

            if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {

                System.err.println("数据源[{}]不存在,使用默认数据源 > {}"+targetDataSource.value()+point.getSignature());

            } else {

                System.out.println("Use DataSource : {} > {}"+targetDataSource.value()+point.getSignature());

                //找到的话,那么设置到动态数据源上下文中。

                DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.value());

            }

        }

       

        @After("@annotation(targetDataSource)")

        public void restoreDataSource(JoinPoint point, TargetDataSourcetargetDataSource) {

           System.out.println("Revert DataSource : {} > {}"+targetDataSource.value()+point.getSignature());

           //方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。

            DynamicDataSourceContextHolder.clearDataSourceType();

        }

       

    }

    (6)注册多数据源;

           以上都是动态数据源在注入的时候使用的代码,其实很重要的一部分代码就是注册我们在application.properties配置的多数据源,这才是重点,这里我们使用

    ImportBeanDefinitionRegistrar进行注册,具体的代码如下:

    package com.kfit.config.datasource.dynamic;

    import java.util.HashMap;

    import java.util.Map;

    import javax.sql.DataSource;

    import org.springframework.beans.MutablePropertyValues;

    import org.springframework.beans.PropertyValues;

    import org.springframework.beans.factory.support.BeanDefinitionRegistry;

    import org.springframework.beans.factory.support.GenericBeanDefinition;

    import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;

    import org.springframework.boot.bind.RelaxedDataBinder;

    import org.springframework.boot.bind.RelaxedPropertyResolver;

    import org.springframework.context.EnvironmentAware;

    import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;

    import org.springframework.core.convert.ConversionService;

    import org.springframework.core.convert.support.DefaultConversionService;

    import org.springframework.core.env.Environment;

    import org.springframework.core.type.AnnotationMetadata;

    /**

     * 动态数据源注册;

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    public class DynamicDataSourceRegister  implements ImportBeanDefinitionRegistrar, EnvironmentAware {

       

        //如配置文件中未指定数据源类型,使用该默认值

        private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";

        private ConversionService conversionService = new DefaultConversionService();

        private PropertyValues dataSourcePropertyValues;

       

        // 默认数据源

        private DataSource defaultDataSource;

       

        private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();

       

        /**

         * 加载多数据源配置

         */

        @Override

        public void setEnvironment(Environment environment) {

           System.out.println("DynamicDataSourceRegister.setEnvironment()");

           initDefaultDataSource(environment);

            initCustomDataSources(environment);

        }

       

        /**

         * 加载主数据源配置.

         * @param env

         */

        private void initDefaultDataSource(Environment env){

           // 读取主数据源

           RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "spring.datasource.");

           Map<String, Object> dsMap = new HashMap<String, Object>();

           dsMap.put("type", propertyResolver.getProperty("type"));

            dsMap.put("driverClassName", propertyResolver.getProperty("driverClassName"));

            dsMap.put("url", propertyResolver.getProperty("url"));

            dsMap.put("username", propertyResolver.getProperty("username"));

            dsMap.put("password", propertyResolver.getProperty("password"));

           

            //创建数据源;

            defaultDataSource = buildDataSource(dsMap);

            dataBinder(defaultDataSource, env);

        }

       

        /**

         * 初始化更多数据源

         *

         * @author SHANHY

         * @create 2016年1月24日

         */

        private void initCustomDataSources(Environment env) {

            // 读取配置文件获取更多数据源,也可以通过defaultDataSource读取数据库获取更多数据源

            RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "custom.datasource.");

            String dsPrefixs = propertyResolver.getProperty("names");

            for (String dsPrefix : dsPrefixs.split(",")) {// 多个数据源

                Map<String, Object> dsMap = propertyResolver.getSubProperties(dsPrefix + ".");

                DataSource ds = buildDataSource(dsMap);

                customDataSources.put(dsPrefix, ds);

                dataBinder(ds, env);

            }

        }

       

        /**

         * 创建datasource.

         * @param dsMap

         * @return

         */

        @SuppressWarnings("unchecked")

        public DataSource buildDataSource(Map<String, Object> dsMap) {

           Object type = dsMap.get("type");

            if (type == null){

                type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource

            }

            Class<? extends DataSource> dataSourceType;

           

           try {

               dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);

               String driverClassName = dsMap.get("driverClassName").toString();

                String url = dsMap.get("url").toString();

                String username = dsMap.get("username").toString();

                String password = dsMap.get("password").toString();

                DataSourceBuilder factory =   DataSourceBuilder.create().driverClassName(driverClassName).url(url).username(username).password(password).type(dataSourceType);

                returnfactory.build();

           } catch (ClassNotFoundException e) {

               e.printStackTrace();

           }

           returnnull;

        }

       

        /**

         * 为DataSource绑定更多数据

         * @param dataSource

         * @param env

         */

        private void dataBinder(DataSource dataSource, Environment env){

           RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource);

           dataBinder.setConversionService(conversionService);

           dataBinder.setIgnoreNestedProperties(false);//false

            dataBinder.setIgnoreInvalidFields(false);//false

            dataBinder.setIgnoreUnknownFields(true);//true

           

            if(dataSourcePropertyValues == null){

                Map<String, Object> rpr = new RelaxedPropertyResolver(env, "spring.datasource").getSubProperties(".");

                Map<String, Object> values = new HashMap<>(rpr);

                // 排除已经设置的属性

                values.remove("type");

                values.remove("driverClassName");

                values.remove("url");

                values.remove("username");

                values.remove("password");

                dataSourcePropertyValues = new MutablePropertyValues(values);

            }

            dataBinder.bind(dataSourcePropertyValues);

           

        }

       

        @Override

        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

           System.out.println("DynamicDataSourceRegister.registerBeanDefinitions()");

           Map<Object, Object> targetDataSources = new HashMap<Object, Object>();

           // 将主数据源添加到更多数据源中

            targetDataSources.put("dataSource", defaultDataSource);

            DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");

            // 添加更多数据源

            targetDataSources.putAll(customDataSources);

            for (String key : customDataSources.keySet()) {

                DynamicDataSourceContextHolder.dataSourceIds.add(key);

            }

           

            // 创建DynamicDataSource

            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();

            beanDefinition.setBeanClass(DynamicDataSource.class);

           

            beanDefinition.setSynthetic(true);

            MutablePropertyValues mpv = beanDefinition.getPropertyValues();

            //添加属性:AbstractRoutingDataSource.defaultTargetDataSource

            mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);

            mpv.addPropertyValue("targetDataSources", targetDataSources);

            registry.registerBeanDefinition("dataSource", beanDefinition);

        }

    }

    这里还有一个步骤很重要,由于我们是使用的ImportBeanDefinitionRegistrar的方式进行注册的,所以我们需要在App.java类中使用@Import进行注册,具体改造之后的App.java代码如下:

    package com.kfit;

    import org.springframework.boot.SpringApplication;

    import org.springframework.boot.autoconfigure.SpringBootApplication;

    import org.springframework.context.annotation.Import;

    import com.kfit.config.datasource.dynamic.DynamicDataSourceRegister;

    /**

     *

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    @SpringBootApplication

    //注册动态多数据源

    @Import({DynamicDataSourceRegister.class})

    public class App {

        public static void main(String[] args) {

           SpringApplication.run(App.class, args);

        }

    }

    (7)测试类测试;

           核心的代码都编写完毕了,当然我们肯定是需要编写代码进行测试下,我们才放心了。

    com.kfit.demo.bean.Demo:

    package com.kfit.demo.bean;

    import javax.persistence.Entity;

    import javax.persistence.GeneratedValue;

    import javax.persistence.Id;

    /**

     * 测试demo类.

     * @author Angel(QQ:412887952)

     * @version v.0.1

     */

    @Entity

    public class Demo {

        @Id @GeneratedValue

        private longid;

       

        private String name;

        public long getId() {

           return id;

        }

        public void setId(longid) {

           this.id = id;

        }

        public String getName() {

           return name;

        }

        publicvoid setName(String name) {

           this.name = name;

        }

        @Override

        public String toString() {

           return"Demo [id=" + id + ", name=" + name + "]";

        }

    }

    com.kfit.demo.dao.TestDao:

    package com.kfit.demo.dao;

    import java.sql.ResultSet;

    import java.sql.SQLException;

    import java.util.List;

    import org.springframework.beans.factory.annotation.Autowired;

    import org.springframework.jdbc.core.JdbcTemplate;

    import org.springframework.jdbc.core.RowMapper;

    import org.springframework.stereotype.Service;

    import com.kfit.demo.bean.Demo;

    @Service

    public class TestDao {

       

        @Autowired

        private JdbcTemplate jdbcTemplate;

       

        /**

         * 不指定数据源使用默认数据源

         * @return

         */

        public List<Demo> getList(){

           String sql = "select *from Demo";

            return (List<Demo>) jdbcTemplate.query(sql, new RowMapper<Demo>(){

                @Override

                public Demo mapRow(ResultSet rs, introwNum) throws SQLException {

                    Demo demo = new Demo();

                    demo.setId(rs.getLong("id"));

                    demo.setName(rs.getString("name"));;

                    returndemo;

                }

            });

        }

       

         /**

         * 指定数据源

         * 在对应的service进行指定;

         * @return

         * @author SHANHY

         * @create  2016年1月24日

         */

        public List<Demo> getListByDs1(){

            /*

             * 这张表示复制的主库到ds1的,在ds中并没有此表.

             * 需要自己自己进行复制,不然会报错:Table 'test1.demo1' doesn't exist

             */

           String sql = "select *from Demo1";

            return (List<Demo>) jdbcTemplate.query(sql, new RowMapper<Demo>(){

                @Override

                public Demo mapRow(ResultSet rs, introwNum) throws SQLException {

                    Demo demo = new Demo();

                    demo.setId(rs.getLong("id"));

                    demo.setName(rs.getString("name"));;

                    returndemo;

                }

            });

        }

    }

    com.kfit.demo.service.TestService :

    package com.kfit.demo.service;

    import java.util.List;

    import javax.annotation.Resource;

    import org.springframework.stereotype.Service;

    import com.kfit.config.datasource.dynamic.TargetDataSource;

    import com.kfit.demo.bean.Demo;

    import com.kfit.demo.dao.TestDao;

    @Service

    public class TestService {

       

        @Resource

        private TestDao testDao;

       

        /**

         * 不指定数据源使用默认数据源

         * @return

         */

        public List<Demo> getList(){

           returntestDao.getList();

        }

       

        /**

         * 指定数据源

         * @return

         */

        @TargetDataSource("ds1")

        public List<Demo> getListByDs1(){

            returntestDao.getListByDs1();

        }

    }

    com.kfit.demo.controller.TestController:

    package com.kfit.demo.controller;

    import javax.annotation.Resource;

    import org.springframework.web.bind.annotation.RequestMapping;

    import org.springframework.web.bind.annotation.RestController;

    import com.kfit.demo.bean.Demo;

    import com.kfit.demo.service.TestService;

    @RestController

    public class TestController {

       

        @Resource

        private TestService testService;

       

        @RequestMapping("/test1")

        public String test(){

    //     for(Demo d:testService.getList()){

    //         System.out.println(d);

    //     }

           for(Demo d:testService.getListByDs1()){

               System.out.println(d);

           }

           return"ok";

        }

       

    }

    好了,测试代码就这么多了,运行App.java进行测试把,访问:

    http://127.0.0.1:8080/test1 查看控制台的打印。

    这里需要提醒下,这种方式spring-jpa的方式好像不能自动路由,博主打算在之后的一篇文章介绍spring-jpa多数据源的问题。

    https://blog.csdn.net/QQQQQQ654/article/details/78025559

  • 相关阅读:
    node之时间
    node之querystring
    学图像处理应该学Matlab还是C++
    柯西—施瓦茨不等式
    矿区 GPS 变形监测及其伪卫星增强技术 GPS and its Pseudolite Augmented Technique for Deformation Monitoring in the Mining Area
    伪卫星
    The Principles of Selection. The Cartographic Journal, 3, 10-16. http://dx.doi.org/10.1179/caj.1966.3.1.10 Topfer, F. and Pillewizer, W. (1966)
    基于采样的网络地图的多尺度曲线绘制表达
    Coursera上Python课程(公开课)汇总
    三江源区高寒草地地上生物量遥感反演模型研究 Modeling Aboveground Biomass of Alpine Grassland in the Three-River Headwaters Region Based on Remote Sensing Data
  • 原文地址:https://www.cnblogs.com/sjqq/p/10524955.html
Copyright © 2020-2023  润新知