zoukankan      html  css  js  c++  java
  • hystrix服务容错处理笔记

    0 环境

    系统环境: win10

    编辑器:idea

    springcloud:H版

    1 前言

    hystrix叫断路器/熔断器。相当于保险丝

    • 微服务中存在多个服务可直接调用的服务 调用时突然出现故障(常在河边走 哪有不湿鞋) 可能整个系统凉了(服务雪崩效应 --> 【 | 类似前段时间 都缺人其实 假设而已 可能比喻不恰当 超市A -> 食品厂B --> 原料厂C | A催货 --> B需要原料 催货 --> C这边没有人手 B只能催C 等这边有货 而A不断催B B只能继续催C 就这样凉了】) 通过hystrix解决这个问题 某一个模
      块故障了 通过我们之前配置好的东西 使的整个系统能运转

    2 基本用法

    • 创建一个springboot项目 配置依赖 进入项目 | 配置yml --> 端口设置 应用名 eureka连接 | 开启断路器。。。
    • 项目用到的eureka server和provider以及hystrix

    2.1 创建项目

    2.1 创建项目

    2.2 yml配置

    spring:
      application:
        name: hystrix
    server:
      port: 3000
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:1234/eureka
    

    2.3 开启断路器和提供RestTemplate实例

    // 开启断路器
    @SpringCloudApplication
    public class HystrixBaseApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(HystrixBaseApplication.class, args);
        }
    
        // 提供RestTemplate实例
        @Bean
        @LoadBalanced
        RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    

    在这里插入图片描述

    2.4 提供hystrix接口

    2.4.1 consumer与hystrix的区别

    • 注解式
    // hystrix
    @Service
    public class HelloService {
        @Autowired
        RestTemplate restTemplate;
    	/**
    	    * @Description:  在这个方法中 我们会发起远程调用 调用provider中提供的hello接口
    	     *               但是这个调用可能会失败
    	     *               我们需要在方法上添加@HystrixCommand注解 配置fallbackMethod属性
    	     *               该属性表示当你调用方法失败 可以用临时方法替代
    	     *               (服务降级 越往下降 -> 获取数据越容易 但数据的准确性也在降级)
    	     *               ignoreExceptions属性作为了解 忽略某个异常
    	    * @Param: []
    	    * @return: java.lang.String
    	    * @Author: 
    	    * @Date: 2020xx/xx
    	*/
    //    @HystrixCommand(fallbackMethod = "error")
        @HystrixCommand(fallbackMethod = "error", ignoreExceptions = ArithmeticException.class)
        public String hello(){
            // 异常处理 若不是提供方的错误 而是consumer本身的异常
    //        int i = 1/0;
            return restTemplate.getForObject("http://provider/hello", String.class);
        }
    /**
        * @Description: 方法名需要和 fallbackMethod属性中的名字一致 还有返回类型得一致 不然返回类型不一致 玩个啥
        * @Param:
        * @return:
        * @Author: 
        * @Date: 2020/xx/xx
        */
        public String error(){
            return "error";
        }
    }
    
    // hystrix
    @RestController
    public class HelloController {
        @Autowired
        HelloService helloService;
    	/**
    	     * @Description: 为啥要用到hystrix 首先eureka中 provider某个实例关闭了 server获取了 在
    	     *              在告诉consumer 这中间肯定会耗时(另一个场景就是请求延时) 那么会出现一个错误界面
    	     *              等consumer收到通知了 才会知道
    	     *              那么hystrix呢 会跳出eroor字符串 而不是一个错误界面 展示一个界面给用户(是不是好多了)
    	     * @Param:
    	     * @return:
    	     * @Author: 
    	     * @Date: 2020/xx/xx
        */
        @GetMapping("/hello")
        public String hello(){
            return helloService.hello();
        }
    }    
    
    // provider
    @RestController
    public class HelloController {
    
        @Value("${server.port}")
        Integer port;
    
        @GetMapping("/hello")
        public String hello(){
            return "hello>>>" + port;
        }
    }
    
    • 打包provider 找到target位置 java -jar xxxx --server.port=xxx 打开2窗口 设置2个不同的prot 并且开启eureka server
    • 体验一下consumer调用和hystrix调用的差别(若是调用失败 先到server上看看 是否注册上来了)
    • 启动consumer(之前的代码就行) 调用接口 ctrl+c关闭一个provider 在调用会出现一个错误页面提示 需要等待一会 才会跳出未关闭的provider端口 关闭端口有个传递时间 等consumer接收到 就会正常显示了
    • 重启关闭的provider端口 启动hystrix 调用hello接口 多次刷新url 2个接口均衡显示 关闭其中一个provider(速度要快 不然看不到效果) 再到页面刷新 会出现erro 自定义返回值体验是不是更好点

    3 请求命令

    • 继承方式实现

    基本使用

    // hystrix
    // 默认使用线程隔离策略(可以配置线程池的一些参数) 还可以信号量策略配置
    public class HelloCommand extends HystrixCommand<String> {
    
        @Autowired
        RestTemplate restTemplate;
    	
    	public HelloCommand(Setter setter, RestTemplate restTemplate) {
            super(setter);
            this.restTemplate = restTemplate;
        }
    	
    	@Override
        protected String run() throws Exception {
    //        int i = 1 / 0;
    // 获取当前线程的名称
    //        System.out.println(Thread.currentThread().getName());
            return restTemplate.getForObject("http://provider/hello", String.class);
    //        return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
        }
    }
    
    // hystrix controller
    /** 
        * @Description: 一个实例只能执行一次 可以直接执行 也可以先入队后执行
        * @Param:  
        * @return:  
        * @Author: 
        * @Date: 2020/xx/xx
        */
        @GetMapping("/hello1")
        public void hello1(){
            HelloCommand learn = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("learn")), restTemplate);
            // 直接执行
            String execute = learn.execute();
            System.out.println("直接执行:" + execute);
    
            HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("learn")), restTemplate);
            Future<String> queue = helloCommand.queue();
    
            try {
                // 先入队 后执行
                String s = queue.get();
                System.out.println(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
    
    
        }
    

    启动eureka server和provider以及hystrix 访问hello1接口 在控制台查看结果

    hystrix继承方式 实现降级

    public class HelloCommand extends HystrixCommand<String> {
    
        @Autowired
        RestTemplate restTemplate;
    
        String name;
    
        public HelloCommand(Setter setter, RestTemplate restTemplate, String name) {
            super(setter);
            this.name = name;
            this.restTemplate = restTemplate;
        }
    
        public HelloCommand(Setter setter, RestTemplate restTemplate) {
            super(setter);
            this.restTemplate = restTemplate;
        }
    
        // 缓存需要重写该方法
        @Override
        protected String getCacheKey() {
            return name;
        }
    
        /*
        *  请求失败的回调
        *
        * */
        @Override
        protected String getFallback() {
            return "error_extends";
        }
    
        @Override
        protected String run() throws Exception {
    //        int i = 1 / 0;
            return restTemplate.getForObject("http://provider/hello", String.class);
            // 获取当前线程的名称
    //        System.out.println(Thread.currentThread().getName());
        }
    }
    

    重启hystrix项目 访问hello1(启动2provider 都注册了 在关闭一个 刷新才能看到效果 与一开始的注解式方式相似)

    • 注解实现请求异步调用
    // hystrix controller层
    @GetMapping("/hello2")
        public void hello2(){
            Future<String> stringFuture = helloService.hello1();
    
            try {
                String s = stringFuture.get();
                System.out.println(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    
    // hystrix service
    /**
        * @Description: 通过注解实现请求异步调用
        * @Param:
        * @return:
        * @Author: 
        * @Date: 2020/xx/xx
        */
        @HystrixCommand(fallbackMethod = "error")
        public Future<String> hello1(){
            return new AsyncResult<String>(){
    
                @Override
                public String invoke() {
                    return restTemplate.getForObject("http://provider/hello", String.class);
                }
            };
        }
    

    重启hystrix项目 访问hello2

    4 异常处理

    • 注解式实现
    	// hystrix controller
    	@GetMapping("/hello")
    	public String hello(){
    	     return helloService.hello();
    	}
    
    // hystrix service
    @Service
    public class HelloService {
        @Autowired
        RestTemplate restTemplate;
    
        /**
        * @Description:  在这个方法中 我们会发起远程调用 调用provider中提供的hello接口
         *               但是这个调用可能会失败
         *               我们需要在方法上添加@HystrixCommand注解 配置fallbackMethod属性
         *               该属性表示当你调用方法失败 可以用临时方法替代
         *               (服务降级 越往下降 -> 获取数据越容易 但数据的准确性也在降级)
         *               ignoreExceptions属性作为了解 忽略某个异常
        * @Param: []
        * @return: java.lang.String
        * @Author: 
        * @Date: 2020/xx/xx
        */
    //    @HystrixCommand(fallbackMethod = "error")
        @HystrixCommand(fallbackMethod = "error", ignoreExceptions = ArithmeticException.class)
        public String hello(){
            // 异常处理 若不是提供方的错误 而是consumer本身的异常
            int i = 1/0;
            return restTemplate.getForObject("http://provider/hello", String.class);
        }
         /**
        * @Description: 方法名需要和 fallbackMethod属性中的名字一致 还有返回类型得一致 不然返回类型不一致 玩个啥
        * @Param:
        * @return:
        * @Author:
        * @Date: 2020/xx/xx
        */
        public String error(Throwable throwable){
            return "error: " + throwable.getMessage();
        }
    }
    
    • 继承式实现
    public class HelloCommand extends HystrixCommand<String> {
    
        @Autowired
        RestTemplate restTemplate;
        
        public HelloCommand(Setter setter, RestTemplate restTemplate) {
            super(setter);
            this.restTemplate = restTemplate;
        }
    
        /*
        *  请求失败的回调
        *
        * */
        @Override
        protected String getFallback() {
            // 在继承方式中出现异常  因为是重写方法 那么我们不可能在参数上添加Throwable
            // 用getExecutionException调用
            return "error_extends: " + getExecutionException().getMessage();
        }
    
        @Override
        protected String run() throws Exception {
            int i = 1 / 0;
            return restTemplate.getForObject("http://provider/hello", String.class);
            // 获取当前线程的名称
    //        System.out.println(Thread.currentThread().getName());
        }
    }
    

    重启hystrix 分别访问hello hello1接口

    5 请求缓存

    调用同一个接口 若参数一致 将之缓存

    5.1 加缓存

    	// 在provider中添加hello2接口
        @GetMapping("/hello2")
        public String hello2(String name){
            System.out.println(new Date() + "--->" + name);
            return "hello " + name;
        }
    
    • 注解式
    // hystrix service中实现
    	@HystrixCommand(fallbackMethod = "error1")
        // 这个注解表示该方法的请求结果会被缓存起来
        // 默认情况下 缓存key就是方法的n个参数的组合 缓存的value就是方法的返回值
        // key(n个param组合) : value
        @CacheResult
        public String hello2(String name){
            return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
        }
    
        @HystrixCommand(fallbackMethod = "error1")
        // 这个注解表示该方法的请求结果会被缓存起来
        // 默认情况下 缓存key就是方法的n个参数的组合 缓存的value就是方法的返回值
        // key(n个param组合) : value
        // 若是只是需要一个参数作为key 在该参数上添加@CacheKey即可
        // 多个请求中 只要name一样 哪怕id不同 二次请求也会使用第一次请求结果(name一样 id不同 --> 使用缓存)
        @CacheResult
        public String hello3(@CacheKey String name, Integer id){
            return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
        }
    	
    	public String error1(String name){
            return "error1" + name;
        }
    
    // hystrix controller
    /**
        * @Description: 注解式
        * @Param:
        * @return:
        * @Author: 水面行走
        * @Date: 2020/3/15
        */
        @GetMapping("/hello3")
        public void hello3(){
            // 需要初始化 不然会报错
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            // 缓存数据
            String learn = helloService.hello2("learn");
            // 使用缓存
            learn = helloService.hello2("learn");
            // 关闭
            context.close();
        }
    
    • 继承式
    public class HelloCommand extends HystrixCommand<String> {
    
        @Autowired
        RestTemplate restTemplate;
    
        String name;
    
        public HelloCommand(Setter setter, RestTemplate restTemplate, String name) {
            super(setter);
            this.name = name;
            this.restTemplate = restTemplate;
        }
    
        public HelloCommand(Setter setter, RestTemplate restTemplate) {
            super(setter);
            this.restTemplate = restTemplate;
        }
    	
    	// 清除缓存 需要定义一方法 HystrixRequestCache用来执行清除操作 根据getCacheKey的返回的key来清除 在controller调用这个方法进行清除
    	
        // 缓存需要重写该方法
        @Override
        protected String getCacheKey() {
            return name;
        }
    
        /*
        *  请求失败的回调
        *
        * */
        @Override
        protected String getFallback() {
            // 在继承方式中出现异常  因为是重写方法 那么我们不可能在参数上添加Throwable
            // 用getExecutionException调用
            return "error_extends: " + getExecutionException().getMessage();
        }
    
        @Override
        protected String run() throws Exception {
    //        int i = 1 / 0;
    //        return restTemplate.getForObject("http://provider/hello", String.class);
            // 获取当前线程的名称
    //        System.out.println(Thread.currentThread().getName());
            return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
        }
    }
    
    
    // hystrix controller
    @GetMapping("/hello5")
        public void hello5(){
            // 需要初始化 不然会报错
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            HelloCommand learn = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("learn")), restTemplate, "learn");
            // 直接执行
            String execute = learn.execute();
            System.out.println("直接执行:" + execute);
    
            HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("learn")), restTemplate, "learn");
            Future<String> queue = helloCommand.queue();
    
            try {
                // 先入队 后执行
                String s = queue.get();
                System.out.println("流程化:" + s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
    
            // 关闭
            context.close();
        }
    

    重启hystrix和provider 访问hello3端口 在控制台查看provider接口输出 确实只显示一次 缓存有效

    5.2 移除缓存

    // hystrix controller
    @GetMapping("/hello4")
        public void hello4(){
            // 需要初始化 不然会报错
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            // 缓存数据
            String learn = helloService.hello2("learn");
            // 删除缓存
            helloService.delUserByName("learn");
            // 因为缓存数据被删除了 需要向provider发起请求
            learn = helloService.hello2("learn");
            // 关闭
            context.close();
        }
    
    // hystrix service
    @HystrixCommand(fallbackMethod = "error1")
        // 这个注解表示该方法的请求结果会被缓存起来
        // 默认情况下 缓存key就是方法的n个参数的组合 缓存的value就是方法的返回值
        // key(n个param组合) : value
        @CacheResult
        public String hello2(String name){
            return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
        }
    
        @HystrixCommand(fallbackMethod = "error1")
        // 这个注解表示该方法的请求结果会被缓存起来
        // 默认情况下 缓存key就是方法的n个参数的组合 缓存的value就是方法的返回值
        // key(n个param组合) : value
        // 若是只是需要一个参数作为key 在该参数上添加@CacheKey即可
        // 多个请求中 只要name一样 哪怕id不同 二次请求也会使用第一次请求结果(name一样 id不同 --> 使用缓存)
        @CacheResult
        public String hello3(@CacheKey String name, Integer id){
            return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
        }
    
        public String error1(String name){
            return "error1" + name;
        }
    
        /**
         * @Description: 当我们删除了数据库数据 还会删除缓存数据 --> @CacheRemove登场
         *                删除缓存 哪里的缓存(指定) 比如删除某个指定的方法
         *               使用@CacheRemove配合commandKey属性 --> 指定缓存 对其诛之
         *               commandKey属性指定删除的某个方法
         * @Param: [name]
         * @return: java.lang.String
         * @Author: 水面行走
         * @Date: 2020/3/15
         */
        @HystrixCommand
        // 删除缓存
        @CacheRemove(commandKey = "hello2")
        public String delUserByName(String name){
            return null;
        }
    

    重启hystrix和provider 访问hello3端口 在控制台查看provider接口输出 显示两次次 缓存被移除

    6 请求合并

    频繁的调用provider接口 太浪费了 就有了将多个请求合并为一个请求的方式

    // 在provider中提供请求合并接口
    @RestController
    public class UserController {
        /**
        * @Description:  若consumer传过来过个id(1,2,3,4,5....这样的格式) 需要格式转换
         *               该接口处理单个请求/合并(多个)后请求
        * @Param:
        * @return:
        * @Author: 水面行走
        * @Date: 2020/3/15
        */
        @GetMapping("/user/{ids}")
        public List<User> getUserByIds(@PathVariable String ids){
            System.out.println("ids: " + ids);
            // string切割为string数组
            String[] split = ids.split(",");
    
            List<User> list = new ArrayList<>();
    
            // 将string数组值遍历封装为user中的属性 添加到list集合中
            for (String s : split) {
                User user = new User();
                // string转换int
                user.setId(Integer.parseInt(s));
                list.add(user);
    
            }
    
            return list;
    
        }
    }
    
    	// hystrix pom.xml添加commons模块
    	<dependency>
    	  <groupId>xxx</groupId>
    	  <artifactId>commons</artifactId>
    	  <version>1.0-SNAPSHOT</version>
    	</dependency>
    
    • 注解式
    // hystrix service
    @Service
    public class UserService {
    
        @Autowired
        RestTemplate restTemplate;
    
        @HystrixCollapser(batchMethod = "getUserByIds",collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds",value = "200")})
        public Future<User> getUsersByIds(Integer id){
            return null;
        }
    
        @HystrixCommand
        public List<User> getUserByIds(List<Integer> ids){
            // 数组.class 因为List.class 结果是个map类型 --> 类属性:value 处理很麻烦
            // 并且需要数组转换为string
            User[] users = restTemplate.getForObject("http://provider/user/{1}", User[].class, StringUtils.join(ids, ","));
    
            return Arrays.asList(users);
    
        }
    
    }
    
    // hystrix controller
    @GetMapping("/hello7")
        public void hello7() throws ExecutionException, InterruptedException {
            // 需要初始化 不然会报错
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
    
            Future<User> queue = userService.getUsersByIds(74);
            Future<User> queue1 = userService.getUsersByIds(64);
            Future<User> queue2 = userService.getUsersByIds(54);
            Future<User> queue3 = userService.getUsersByIds(44);
    
            User user = queue.get();
            User user1 = queue1.get();
            User user2 = queue2.get();
            User user3 = queue3.get();
            System.out.println(user);
            System.out.println(user1);
            System.out.println(user2);
            System.out.println(user3);
    
            // 关闭
            context.close();
        }
    
    • 继承式(了解即可)
    // hystrix
    public class UserBatchCommand extends HystrixCommand<List<User>> {
    
        private List<Integer> ids;
        private UserService userService;
    
        public UserBatchCommand(List<Integer> ids, UserService userService) {
            // 写死
            super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("batchCmd")).andCommandKey(HystrixCommandKey.Factory.asKey("batchKey")));
            this.ids = ids;
            this.userService = userService;
        }
    
        @Override
        protected List<User> run() throws Exception {
            return userService.getUserByIds(ids);
        }
    }
    
    // hystrix
    // 请求合并
    public class UserCollapseCommand extends HystrixCollapser<List<User>, User, Integer> {
    
        private Integer id;
        private UserService userService;
    
        public UserCollapseCommand(UserService userService, Integer id) {
            // 写死
            super(HystrixCollapser.Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("userCollapseCommand")).andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(200)));
            this.id = id;
            this.userService = userService;
        }
    
        // 请求参数
        @Override
        public Integer getRequestArgument() {
            return id;
        }
    
        // 请求合并方法
        @Override
        protected HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Integer>> collection) {
            List<Integer> list = new ArrayList<>(collection.size());
            for (CollapsedRequest<User, Integer> integerCollapsedRequest : collection) {
                list.add(integerCollapsedRequest.getArgument());
            }
            return new UserBatchCommand(list, userService);
        }
    
        // 请求结果分发
        @Override
        protected void mapResponseToRequests(List<User> users, Collection<CollapsedRequest<User, Integer>> collection) {
            int count = 0;
            for (CollapsedRequest<User, Integer> userIntegerCollapsedRequest : collection) {
    
                userIntegerCollapsedRequest.setResponse(users.get(count++));
            }
        }
    }
    
    // service 实现
    @Service
    public class UserService {
    
        @Autowired
        RestTemplate restTemplate;
    
        @HystrixCommand
        public List<User> getUserByIds(List<Integer> ids){
            // 数组.class 因为List.class 结果是个map类型 --> 类属性:value 处理很麻烦
            // 并且需要数组转换为string
            User[] users = restTemplate.getForObject("http://provider/user/{1}", User[].class, StringUtils.join(ids, ","));
    
            return Arrays.asList(users);
    
        }
    
    }
    
    // hystrix controller
    @GetMapping("/hello6")
        public void hello6() throws ExecutionException, InterruptedException {
            // 需要初始化 不然会报错
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            UserCollapseCommand cmd = new UserCollapseCommand(userService, 99);
            UserCollapseCommand cmd1 = new UserCollapseCommand(userService, 88);
            UserCollapseCommand cmd2 = new UserCollapseCommand(userService, 77);
            UserCollapseCommand cmd3 = new UserCollapseCommand(userService, 66);
    
            // 加入队列
            Future<User> queue = cmd.queue();
            Future<User> queue1 = cmd1.queue();
            Future<User> queue2 = cmd2.queue();
            Future<User> queue3 = cmd3.queue();
    
            User user = queue.get();
            User user1 = queue1.get();
            User user2 = queue2.get();
            User user3 = queue3.get();
            System.out.println(user);
            System.out.println(user1);
            System.out.println(user2);
            System.out.println(user3);
    
    
            // 关闭
            context.close();
        }
    

    7 总结

    降级处理 注解(@HystrixCommand(fallbackMethod = "xxx"))和继承式(重写getFallback())
    缓存 注解@CacheResult和重写getCacheKey()
    合并@HystrixCollapser(batchMethod = "getUserByIds",collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds",value = "200")}) --> 请求合并 延时 和@HystrixCommand

    作者:以罗伊
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    TSQL--按某字段列分组,在将各组中某列合并成一行
    疑难杂症--SQL SERVER 2012下数据库内存异常回收
    TSQL--删除登陆相关的用户
    杂谈--一次”失败“问题处理过程
    layer.open如何关闭自身弹出窗口
    简单的Http请求数据保存到Hdfs
    layui栅格布局问题
    redis单机版安装
    redis详细配置文件
    批量修改mysql数据库引擎
  • 原文地址:https://www.cnblogs.com/my-ordinary/p/12505424.html
Copyright © 2011-2022 走看看