zoukankan      html  css  js  c++  java
  • Springboot笔记<12>数据访问

    springboot数据访问

    springboot整合JDBC

    JdbcTemplate是Spring对JDBC的封装,目的是使JDBC更加易于使用。JdbcTemplate是Spring的一部分。JdbcTemplate处理了资源的建立和释放。他帮助我们避免一些常见的错误,比如忘了总要关闭连接。他运行核心的JDBC工作流,如Statement的建立和执行,而我们只需要提供SQL语句和提取结果。

    • execute:可以执行所有SQL语句,一般用于执行DDL语句。
    • update:用于执行INSERTUPDATEDELETE等DML语句。
    • queryXxx:用于DQL数据查询语句。

    pom引入依赖

    pom.xml 配置maven依赖,mysql-connector驱动和jdbc-starter,数据库版本要和驱动版本对应。导入JDBC场景,不导入驱动因为官方不知道我们接下要操作什么数据库。

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.49</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    

    yaml配置数据库账号密码驱动等信息

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/springdata?useSSL=false
        username: root
        password: root
        driver-class-name: com.mysql.jdbc.Driver
    

    创建一个简单的数据库表

    create table `student` (
    	`age` int (11),
    	`name` varchar (255),
    	`num` int (11)
    ); 
    insert into `student` (`age`, `name`, `num`) values('12','小明','1');
    insert into `student` (`age`, `name`, `num`) values('13','小红','2');
    
    

    @Test测试jdbc连接

    @Slf4j
    @SpringBootTest
    class SpringbootReviewApplicationTests {
    
        @Test
        public void testImport(){
        }
        
        @Autowired
        JdbcTemplate jdbcTemplate;
        
        @Test
        void contextLoads() {
            List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from student");
            for (Map<String, Object> map : maps) {
                for (String s : map.keySet()) {
                    log.info(s + map.get(s));
                }
            }
            Long aLong = jdbcTemplate.queryForObject("select count(*) from student", Long.class);
            log.info("记录总数:{}",aLong);
        }
    
    }
    

    使用Druid数据源

    数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标.数据库连接池正式针对这个问题提出来的.数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。 数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中, 这些数据库连接的数量是由最小数据库连接数来设定的.无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量.连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

    引入starter:

    <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid-spring-boot-starter</artifactId>
       <version>1.1.17</version>
    </dependency>
    

    官方文档:

    https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

    参数配置:

    监控页访问地址:http://localhost:8080/druid/index.html

    spring:
      mvc:
        hiddenmethod:
          filter:
            enabled: true
      servlet:
        multipart:
          enabled: true
          file-size-threshold: 2KB
          max-request-size: 215MB
      datasource:
        url: jdbc:mysql://localhost:3306/springdata?useSSL=false
        username: root
        password: root
        driver-class-name: com.mysql.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
    
        druid:
          aop-patterns: com.top.springbootweb.*  #监控SpringBean
          filters: stat,wall     # 底层开启功能,stat(sql监控),wall(防火墙)
          
          stat-view-servlet:   # 配置监控页功能
            enabled: true
            login-username: admin
            login-password: admin
            resetEnable: false
    
          web-stat-filter:  # 监控web
            enabled: true
            urlPattern: /*
            exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
    
    
          filter:
            stat:    # 对上面filters里面的stat的详细配置
              slow-sql-millis: 1000
              logSlowSql: true
              enabled: true
            wall:
              enabled: true
              config:
                drop-table-allow: false
    

    参数意义:

    注入bean:

    将druidDataSource,监控servlet和过滤器filter注入

    @Configuration
    public class DruidConfig {
    
        // 默认的自动配置是判断容器中没有才会配@ConditionalOnMissingBean(DataSource.class)
        @ConfigurationProperties("spring.datasource")
        @Bean
        public DataSource dataSource() throws SQLException {
            DruidDataSource druidDataSource = new DruidDataSource();
            return druidDataSource;
        }
    
        /**
         * 配置 druid的监控页功能
         *
         * @return
         */
        @Bean
        public ServletRegistrationBean statViewServlet() {
            StatViewServlet statViewServlet = new StatViewServlet();
            ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
    //        registrationBean.addInitParameter("loginUsername", "admin");
    //        registrationBean.addInitParameter("loginPassword", "admin");
            //白名单:
            servletRegistrationBean.addInitParameter("allow","127.0.0.1");
            //IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page.
            servletRegistrationBean.addInitParameter("deny","192.168.1.73");
            return registrationBean;
        }
    
        /**
         * WebStatFilter 用于采集web-jdbc关联监控的数据。
         */
        @Bean
        public FilterRegistrationBean webStatFilter() {
            WebStatFilter webStatFilter = new WebStatFilter();
            FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
            filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
            filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
    
            return filterRegistrationBean;
        }
    
    }
    

    controller:

    访问/student之后,进入监控页可以查看对sql语句执行的监控

    @RestController
    @Slf4j
    public class DruidTestController {
        @Autowired
        JdbcTemplate jdbcTemplate;
        @GetMapping("/student")
        public List<Map<String, Object>> contextLoads() {
            DataSource dataSource = jdbcTemplate.getDataSource();
            log.info(dataSource.toString());
            List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from student");
            for (Map<String, Object> map : maps) {
                for (String s : map.keySet()) {
                    log.info(s+map.get(s));
                }
            }
            return maps;
    
        }
    }
    //INFO 22000 --- [nio-9999-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
    

    image-20211217161610443

    使用redis

    Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

    Lettuce与Jedis区别

    Jedis在实现上是直接连接Redis Server,如果在多线程环境下是非线程安全的。每个线程都去拿自己的Jedis 实例,当连接数量增多时,资源消耗阶梯式增大,连接成本就较高(使用JedisPool会有一定的改善)。
    Lettuce的连接是基于Netty的,异步、多线程、事件驱动,连接实例可以在多个线程间共享,当多线程使用同一连接实例时,是线程安全的。

    切换Jedis

    默认添加了Lettuce,添加jedis依赖,并在配置文件中配置spring.redis.client-type: jedis

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--        导入jedis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
    

    引入redis依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    测试连接是否成功

    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
      
    @Test
    void testRedis(){
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        operations.set("hello","world");
        String hello = operations.get("hello");
        System.out.println(hello);
    }
    

    更改默认模板的序列化方式

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.RedisSerializer;
    
    /**
     * 编写配置类,构造RedisTemplate
     * 这个springboot已经帮我们配了,但是默认object,这里改成string
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);
            // 设置key的序列化方式
            template.setKeySerializer(RedisSerializer.string());
            // 设置value的序列化方式
            template.setValueSerializer(RedisSerializer.json());
            // 设置hash的key的序列化方式
            template.setHashKeySerializer(RedisSerializer.string());
            // 设置hash的value的序列化方式
            template.setHashValueSerializer(RedisSerializer.json());
            template.afterPropertiesSet();
            return template;
        }
    
    }
    

    写RedisUtil类,更方便地使用redis

    package com.top.springbootweb.Utils;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    
    @Component
    public class RedisUtil {
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        /**
         * 指定缓存失效时间
         * @param key 键
         * @param time 时间(秒)
         * @return true / false
         */
        public boolean expire(String key, long time) {
            try {
                if (time > 0) {
                    redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据 key 获取过期时间
         * @param key 键
         * @return
         */
        public long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
    
        /**
         * 判断 key 是否存在
         * @param key 键
         * @return true / false
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除缓存
         * @SuppressWarnings("unchecked") 忽略类型转换警告
         * @param key 键(一个或者多个)
         */
        @SuppressWarnings("unchecked")
        public void del(String... key) {
            if (key != null && key.length > 0) {
                if (key.length == 1) {
                    redisTemplate.delete(key[0]);
                } else {
                    // 传入一个 Collection<String> 集合
                    redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
                }
            }
        }
    
        //  ============================== String ==============================
    
        /**
         * 普通缓存获取
         * @param key 键
         * @return 值
         */
        public Object get(String key) {
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
    
        /**
         * 普通缓存放入
         * @param key 键
         * @param value 值
         * @return true / false
         */
        public boolean set(String key, Object value) {
            try {
                redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 普通缓存放入并设置时间
         * @param key 键
         * @param value 值
         * @param time 时间(秒),如果 time < 0 则设置无限时间
         * @return true / false
         */
        public boolean set(String key, Object value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 递增
         * @param key 键
         * @param delta 递增大小
         * @return
         */
        public long incr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递增因子必须大于 0");
            }
            return redisTemplate.opsForValue().increment(key, delta);
        }
    
        /**
         * 递减
         * @param key 键
         * @param delta 递减大小
         * @return
         */
        public long decr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递减因子必须大于 0");
            }
            return redisTemplate.opsForValue().increment(key, delta);
        }
    
        //  ============================== Map ==============================
    
        /**
         * HashGet
         * @param key 键(no null)
         * @param item 项(no null)
         * @return 值
         */
        public Object hget(String key, String item) {
            return redisTemplate.opsForHash().get(key, item);
        }
    
        /**
         * 获取 key 对应的 map
         * @param key 键(no null)
         * @return 对应的多个键值
         */
        public Map<Object, Object> hmget(String key) {
            return redisTemplate.opsForHash().entries(key);
        }
    
        /**
         * HashSet
         * @param key 键
         * @param map 值
         * @return true / false
         */
        public boolean hmset(String key, Map<Object, Object> map) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * HashSet 并设置时间
         * @param key 键
         * @param map 值
         * @param time 时间
         * @return true / false
         */
        public boolean hmset(String key, Map<Object, Object> map, long time) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张 Hash表 中放入数据,如不存在则创建
         * @param key 键
         * @param item 项
         * @param value 值
         * @return true / false
         */
        public boolean hset(String key, String item, Object value) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张 Hash表 中放入数据,并设置时间,如不存在则创建
         * @param key 键
         * @param item 项
         * @param value 值
         * @param time 时间(如果原来的 Hash表 设置了时间,这里会覆盖)
         * @return true / false
         */
        public boolean hset(String key, String item, Object value, long time) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除 Hash表 中的值
         * @param key 键
         * @param item 项(可以多个,no null)
         */
        public void hdel(String key, Object... item) {
            redisTemplate.opsForHash().delete(key, item);
        }
    
        /**
         * 判断 Hash表 中是否有该键的值
         * @param key 键(no null)
         * @param item 值(no null)
         * @return true / false
         */
        public boolean hHasKey(String key, String item) {
            return redisTemplate.opsForHash().hasKey(key, item);
        }
    
        /**
         * Hash递增,如果不存在则创建一个,并把新增的值返回
         * @param key 键
         * @param item 项
         * @param by 递增大小 > 0
         * @return
         */
        public Double hincr(String key, String item, Double by) {
            return redisTemplate.opsForHash().increment(key, item, by);
        }
    
        /**
         * Hash递减
         * @param key 键
         * @param item 项
         * @param by 递减大小
         * @return
         */
        public Double hdecr(String key, String item, Double by) {
            return redisTemplate.opsForHash().increment(key, item, -by);
        }
    
        //  ============================== Set ==============================
    
        /**
         * 根据 key 获取 set 中的所有值
         * @param key 键
         * @return 值
         */
        public Set<Object> sGet(String key) {
            try {
                return redisTemplate.opsForSet().members(key);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 从键为 key 的 set 中,根据 value 查询是否存在
         * @param key 键
         * @param value 值
         * @return true / false
         */
        public boolean sHasKey(String key, Object value) {
            try {
                return redisTemplate.opsForSet().isMember(key, value);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将数据放入 set缓存
         * @param key 键值
         * @param values 值(可以多个)
         * @return 成功个数
         */
        public long sSet(String key, Object... values) {
            try {
                return redisTemplate.opsForSet().add(key, values);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 将数据放入 set缓存,并设置时间
         * @param key 键
         * @param time 时间
         * @param values 值(可以多个)
         * @return 成功放入个数
         */
        public long sSet(String key, long time, Object... values) {
            try {
                long count = redisTemplate.opsForSet().add(key, values);
                if (time > 0) {
                    expire(key, time);
                }
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 获取 set缓存的长度
         * @param key 键
         * @return 长度
         */
        public long sGetSetSize(String key) {
            try {
                return redisTemplate.opsForSet().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 移除 set缓存中,值为 value 的
         * @param key 键
         * @param values 值
         * @return 成功移除个数
         */
        public long setRemove(String key, Object... values) {
            try {
                return redisTemplate.opsForSet().remove(key, values);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        //  ============================== List ==============================
    
        /**
         * 获取 list缓存的内容
         * @param key 键
         * @param start 开始
         * @param end 结束(0 到 -1 代表所有值)
         * @return
         */
        public List<Object> lGet(String key, long start, long end) {
            try {
                return redisTemplate.opsForList().range(key, start, end);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 获取 list缓存的长度
         * @param key 键
         * @return 长度
         */
        public long lGetListSize(String key) {
            try {
                return redisTemplate.opsForList().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 根据索引 index 获取键为 key 的 list 中的元素
         * @param key 键
         * @param index 索引
         *              当 index >= 0 时 {0:表头, 1:第二个元素}
         *              当 index < 0 时 {-1:表尾, -2:倒数第二个元素}
         * @return 值
         */
        public Object lGetIndex(String key, long index) {
            try {
                return redisTemplate.opsForList().index(key, index);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 将值 value 插入键为 key 的 list 中,如果 list 不存在则创建空 list
         * @param key 键
         * @param value 值
         * @return true / false
         */
        public boolean lSet(String key, Object value) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将值 value 插入键为 key 的 list 中,并设置时间
         * @param key 键
         * @param value 值
         * @param time 时间
         * @return true / false
         */
        public boolean lSet(String key, Object value, long time) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将 values 插入键为 key 的 list 中
         * @param key 键
         * @param values 值
         * @return true / false
         */
        public boolean lSetList(String key, List<Object> values) {
            try {
                redisTemplate.opsForList().rightPushAll(key, values);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将 values 插入键为 key 的 list 中,并设置时间
         * @param key 键
         * @param values 值
         * @param time 时间
         * @return true / false
         */
        public boolean lSetList(String key, List<Object> values, long time) {
            try {
                redisTemplate.opsForList().rightPushAll(key, values);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据索引 index 修改键为 key 的值
         * @param key 键
         * @param index 索引
         * @param value 值
         * @return true / false
         */
        public boolean lUpdateIndex(String key, long index, Object value) {
            try {
                redisTemplate.opsForList().set(key, index, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 在键为 key 的 list 中删除值为 value 的元素
         * @param key 键
         * @param count 如果 count == 0 则删除 list 中所有值为 value 的元素
         *              如果 count > 0 则删除 list 中最左边那个值为 value 的元素
         *              如果 count < 0 则删除 list 中最右边那个值为 value 的元素
         * @param value
         * @return
         */
        public long lRemove(String key, long count, Object value) {
            try {
                return redisTemplate.opsForList().remove(key, count, value);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
    }
    

    测试RedisUtil

    @SpringBootTest
    @Slf4j
    public class RedisTest {
        @Autowired
        RedisUtil redisUtil;
    
        @Test
        public void testExpire() throws InterruptedException {
            redisUtil.set("expire",3);
            redisUtil.expire("expire", 4);
            log.info("expire time = "+redisUtil.getExpire("expire"));
            log.info(String.valueOf("hasKey?   " + redisUtil.hasKey("expire")));
            Thread.currentThread().sleep(5000);
            redisUtil.get("expire");
            log.info(String.valueOf("hasKey?   " + redisUtil.hasKey("expire")));
        }
        @Test
        public void testDelete(){
            redisUtil.set("1",1);
            redisUtil.set("2",2);
            log.info(String.valueOf(redisUtil.hasKey("1")));
            log.info(String.valueOf(redisUtil.hasKey("2")));
            redisUtil.del("1","2");
            log.info(String.valueOf(redisUtil.hasKey("1")));
            log.info(String.valueOf(redisUtil.hasKey("2")));
        }
        @Test
        public void testMap(){
            redisUtil.hset("xiaoming","age",17);
            redisUtil.hset("xiaoming","name","xiaoming");
        }
    
    }
    

    未经作者同意请勿转载

    本文来自博客园作者:aixueforever,原文链接:https://www.cnblogs.com/aslanvon/p/15770183.html

  • 相关阅读:
    arcgis api for js入门开发系列二十打印地图的那些事
    arcgis api 3.x for js 入门开发系列十九图层在线编辑
    arcgis api 3.x for js 入门开发系列十八风向流动图(附源码下载)
    influxDB 0.9 C# 读写类
    [InfluxDB] 安装与配置
    分布式,集群,冗余的理解
    CentOS 7.0系统安装配置图解教程
    InfluxDB学习之InfluxDB的基本操作| Linux大学
    InfluxDB v1.6.4 下载
    InfluxDB中文文档
  • 原文地址:https://www.cnblogs.com/aslanvon/p/15770183.html
Copyright © 2011-2022 走看看