zoukankan      html  css  js  c++  java
  • 本地tomcat调用远程接口报错:java.lang.reflect.InvocationTargetException

      今天碰到一个奇怪的问题,本地Eclipse起了一个tomcat通过http去调一个外部接口,结果竟然报了一个反射的异常,先看下完整日志:

    四月 12, 2018 4:00:14 下午 org.apache.catalina.startup.Catalina start
    信息: Server startup in 31334 ms
    [2018-04-12 16:01:00] DEBUG EnvironmentInterceptor:33 - EnvironmentInterceptor.preHandle requesturl:http://127.0.0.1:8080/ms-search-war/ms.search.searchService/getSearchRank
    [2018-04-12 16:01:00] DEBUG EnvironmentInterceptor:34 - RequestHeads,User-Agent=Apache-HttpClient/4.1.1 (java 1.5),X-Identity-ID=15077870000,X-Auth-Token=123,X-Login-Type=2
    [2018-04-12 16:01:00] DEBUG MsCommonAspect:128 - Enter into UrlAdviser.setUrlParam request param = {"paramMap": [{"key": "pluginCode","value": "search_rank"},{"key": "title","value": ""},{"key": "rankType","value": "5"},{"key": "rankDateType","value": "2"},{"key": "pageNo","value": "1"},{"key": "pageSize","value": "10"},{"key": "isMarginTop","value": ""},{"key": "isMarginBottom","value": ""},{"key": "isPaddingTop","value": ""},{"key": "isShowLine","value": ""},{"key": "isAjax","value": ""},{"key": "nodeId","value": ""},{"key": "book_id_list","value": ""},{"key": "listen_book_id_list","value": ""},{"key": "magzine_id_list","value": ""},{"key": "kw","value": "+++++"},{"key": "itemType","value": "0"},{"key": "source","value": "1"}]},identityId = 15077870000
    [2018-04-12 16:01:00] INFO  PerformanceMonitorComponent:66 - [15ms] - interface:[ IGaiaClient ],method:[ get ]USER_SESSION_KEY:15077870000:cm
    
    [2018-04-12 16:01:00] DEBUG MsCommonAspect:169 - Exit UrlAdviser.setUrlParam success , nln= null ,cm = M801005I ,identityId = 15077870000 
    [2018-04-12 16:01:00] DEBUG GetSearchRankMethodImpl:83 - Enter GetSearchRankMethodImpl.getSearchRank ,identityId:15077870000 ComponentRequest :{"paramMap": [{"key": "pluginCode","value": "search_rank"},{"key": "title","value": ""},{"key": "rankType","value": "5"},{"key": "rankDateType","value": "2"},{"key": "pageNo","value": "1"},{"key": "pageSize","value": "10"},{"key": "isMarginTop","value": ""},{"key": "isMarginBottom","value": ""},{"key": "isPaddingTop","value": ""},{"key": "isShowLine","value": ""},{"key": "isAjax","value": ""},{"key": "nodeId","value": ""},{"key": "book_id_list","value": ""},{"key": "listen_book_id_list","value": ""},{"key": "magzine_id_list","value": ""},{"key": "kw","value": "+++++"},{"key": "itemType","value": "0"},{"key": "source","value": "1"}]}
    [2018-04-12 16:01:01] DEBUG CampaignEngine:218 - enter CampaignEngine.getAppMiguSearch request:{"bookType":"0","categoryType":"0","chargeMode":"0","ct":"4","isContinue":"0","isNeedCorrect":"0","pageIndex":1,"pageNum":1,"phoneNumber":"15077870000","queryWord":"+++++","searchType":"0","sorter":"0","wordCount":"0"}
    [2018-04-12 16:01:08] DEBUG ResourceLeakDetectorFactory:76 - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@62c3bb38
    [2018-04-12 16:01:26] ERROR ServiceMethodImpl:101 - invoke failed, request_id = 601424C46E1591BD5BC9D9590C8EB793, sc={ rpc: { cname: ms.search.searchService:1.0.0.dev, method: getSearchRank, operation: ms.search.searchService:1.0.0.dev/getSearchRank}, implVersion: 0, complete: false, canceled: false}, ex=java.lang.reflect.InvocationTargetException
    [2018-04-12 16:01:34] DEBUG MsCommonAspect:84 - Enter setActionCdrParamAspect,identityId:15077870000
    [2018-04-12 16:01:34] DEBUG MsCommonAspect:108 - Exit setActionCdrParamAspect,identityId:15077870000,ActionCdrParams:
    [2018-04-12 16:01:34] INFO  MethodInvoker:151 - call: ms.search.searchService/getSearchRank, request_id=601424C46E1591BD5BC9D9590C8EB793, expected= 0, version=0, code=1, rt=33763132
    [2018-04-12 16:01:34] DEBUG EnvironmentInterceptor:55 - EnvironmentInterceptor completed, requesturl= http://127.0.0.1:8080/ms-search-war/ms.search.searchService/getSearchRank and server delayTime = 34008
    [2018-04-12 16:03:01] DEBUG AbstractPool:114 - server removed, node=ZJHZ-CMREAD-TEST213:8080, server={ node: ZJHZ-CMREAD-TEST213:8080, hostname: ZJHZ-CMREAD-TEST213, port: 8080, status: 8, weight: 8, capacity: 128, breaker: { state :CLOSED, working: 0, delay: 15000000000, failureThreshold: [80/100, 0.8], successThreshold: [12/16, 0.75]}, version: 2447894418 }
    [2018-04-12 16:03:45] DEBUG AbstractPool:107 - server updated, node=ZJHZ-CMREAD-TEST213:8080, server={ node: ZJHZ-CMREAD-TEST213:8080, hostname: ZJHZ-CMREAD-TEST213, port: 8080, status: 1, weight: 8, capacity: 128, breaker: { state :CLOSED, working: 0, delay: 15000000000, failureThreshold: [80/100, 0.8], successThreshold: [12/16, 0.75]}, version: 2447934730 }

      从日志里其实看不出啥来,通过eclipse调试才发现,进入这一行代码后发生了诡异的事情:

    String response = HttpTools.getDataFromFirstSerach(uri.toString(), jsonData);

      诡异的事情就是直接跳入异常了

        /**
         * Constructs a InvocationTargetException with a target exception.
         *
         * @param target the target exception
         */
        public InvocationTargetException(Throwable target) {
            super((Throwable)null);  // Disallow initCause
            this.target = target;
        }

      从异常看,是程序找不到调用代码了,但是eclipse里面是可以找到HttpTools类和getDataFromFirstSerach方法的。想了一下,应该是编译出问题了,class文件里没有对应的代码。直接到eclipse部署的本地tomcat路径下找class:打开Servers窗口 -> 找到Server Locations,看Server path和Deploy path,这里就是本地eclipse部署路径:

      进入本地部署路径:

      进入wtpwebapps -> 进入war包 -> 找到引用的jar -> 打开jar包,根据包路径找类,果然没有HttpTools这个类。估计是之前引用的jar包编译出了问题,没编译完。

      解决办法:重新编译问题jar包,刷新引用jar包的工程,最后clean一下tomcat,再次进入到tomcat本地部署路径E:workspace.metadata.pluginsorg.eclipse.wst.server.core mp0wtpwebapps,发现HttpTools类出现了,重新启动tomcat,问题依然存在。这就奇怪了,重新调试,发现另一个问题:

    java.lang.NoSuchMethodError: io.netty.handler.ssl.SslContextBuilder.protocols([Ljava/lang/String;)Lio/netty/handler/ssl/SslContextBuilder;

      有点摸不着头脑,代码调试时流程没变,直接跳入异常,但异常显示的是上面的问题,找不到SslContextBuilder.protocols这个方法。

      问题定位:仔细看了下HttpTools这个类:

    import java.io.IOException;
    import java.nio.charset.Charset;
    import java.util.Date;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.apache.http.HttpEntity;
    import org.apache.http.client.config.RequestConfig;
    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.util.EntityUtils;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.asynchttpclient.AsyncHttpClient;
    import org.asynchttpclient.AsyncHttpClientConfig;
    import org.asynchttpclient.BoundRequestBuilder;
    import org.asynchttpclient.DefaultAsyncHttpClient;
    import org.asynchttpclient.DefaultAsyncHttpClientConfig;
    import org.asynchttpclient.Response;
    import org.springframework.http.HttpStatus;/**
     * 基础的http方法 此类包含了通过HTTP协议的请求响应等方法。 此类属于公共方法,负责提供接口给业务调用。 目前请求支持4种请求方式: 1 xml请求报文通过POST方法请求URL,返回响应报文 2
     * xml请求报文通过PUT方法请求URL,返回响应报文 3 无参数的请求通过GET方法请求URL,返回响应报文 4 无参数的请求通过DELETE方法请求URL,返回操作结果 使用方法: 先得到HttpUtil的实例(
     * 使用连接池方式),通过HttpUtil.getInstance().getHttpClient()返回HttpClient对象 然后调用指定的方法即可
     * 
     */
    public class HttpTools
    {/**
         * HttpUtil类构造函数
         */
        public HttpTools()
        {
            
        }
        
        /**
         * 单例模式返回唯一的HttpUtil的实例 在创建HttpUtil实例的时候创建HttpClient对象,并且设置HttpClient超时的属性。
         * 创建HttpClient实例,默认是SimpleHttpConnectionManager创建的,不支持多线程。 使用多线程技术就是说,client可以在多个线程中被用来执行多个方法。
         * 每次调用HttpClient.executeMethod() 方法,都会去链接管理器申请一个连接实例, 申请成功这个链接实例被签出(checkout),随之在链接使用完后必须归还管理器。 管理器支持两个设置:
         * maxConnectionsPerHost 每个主机的最大并行链接数,默认为2 maxTotalConnections 客户端总并行链接最大数,默认为20
         * 
         * @return HttpUtil
         */
        public static HttpTools getInstance()
        {
            return instance;
        }
        
        private static AsyncHttpClient asynHttpClient = getAsyncHttpClient();
        
        /**
         * 获取请求类的客户端
         * 
         * @return
         */
        public static AsyncHttpClient getAsyncHttpClient()
        {
            AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().setFollowRedirect(false)
                .setConnectTimeout(PropertiesConfig.getInt("asynHttp.connectTimeout", 500))
                .setRequestTimeout(PropertiesConfig.getInt("asynHttp.requestTimeout", 10000))
                .setReadTimeout(PropertiesConfig.getInt("asynHttp.readTimeout", 10000))
                .build();
            AsyncHttpClient client = new DefaultAsyncHttpClient(config);
            return client;
        }
         public static String getDataFromFirstSerach(String uri,String  jsonParam)
        {
            long startTime = System.currentTimeMillis();
            String responseContent = null;
            CloseableHttpResponse response = null;
            
            // 设置超时
            RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(PropertiesConfig.getInt("timeOut", 15000))
                .setConnectTimeout(PropertiesConfig.getInt("timeOut", 15000))
                .setConnectionRequestTimeout(PropertiesConfig.getInt("timeOut", 15000))
                .build();
            
            // 调用一级搜索接口
            CloseableHttpClient httpClient = HttpClients.createDefault();
            try
            {
                
                StringEntity entity = new StringEntity(jsonParam, "utf-8");// 解决中文乱码问题
                entity.setContentEncoding("UTF-8");
                entity.setContentType("application/json");
                String timeTapms = new Date().getTime() + "";
                String mgauth = PropertiesConfig.getProperty(APPID) + PropertiesConfig.getProperty(APPKEY) + timeTapms;
                HttpPost httpPost = new HttpPost(uri);
                httpPost.setConfig(requestConfig);
                httpPost.setEntity(entity);
                httpPost.addHeader(ParamConstants.MGAUTH, CipherOperateUtil.encryptByMD5(mgauth));
                httpPost.addHeader(ParamConstants.REQTIME, timeTapms);
                response = httpClient.execute(httpPost);
                
                // 请求响应成功则获取响应
                if (null != response && response.getStatusLine().getStatusCode() == 200)
                {
                    HttpEntity httpentity = response.getEntity();
                    responseContent = EntityUtils.toString(httpentity, "UTF-8");
                }
            }
            catch (Exception e)
            {
                logger.error(" httpTools.getDataFromFirstSerach call classifySearch error message:{},request:{} ",e.getMessage(),jsonParam);
            }
            finally
            {
                try
                {
                    // 关闭连接,释放资源
                    if (response != null)
                    {
                        response.close();
                    }
                    if (httpClient != null)
                    {
                        httpClient.close();
                    }
                }
                catch (IOException e)
                {
                    logger.error(" httpTools.getDataFromFirstSerach call classifySearch error message:{},request:{} ",e.getMessage(),jsonParam);
                }
            }
            long endTime = System.currentTimeMillis();
    
            if (logger.isDebugEnabled())
            {
                logger.debug("=======================call getDataFromFirstSerach interface = {},spend time = {} ms",
                    uri,
                    endTime - startTime);
            }
    
            return responseContent;
        }
        
    }

      从上面看出,类一开始就通过静态方法getAsyncHttpClient得到异步请求的客户端对象asynHttpClient,所以应该在getAsyncHttpClient方法加断点,否则就直接进入异常了。异常跟踪直接看日志:

    ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.
    Exception in thread "main" java.lang.NoSuchMethodError: io.netty.handler.ssl.SslContextBuilder.protocols([Ljava/lang/String;)Lio/netty/handler/ssl/SslContextBuilder;
        at org.asynchttpclient.netty.ssl.DefaultSslEngineFactory.buildSslContext(DefaultSslEngineFactory.java:45)
        at org.asynchttpclient.netty.ssl.DefaultSslEngineFactory.init(DefaultSslEngineFactory.java:69)
        at org.asynchttpclient.netty.channel.ChannelManager.<init>(ChannelManager.java:110)
        at org.asynchttpclient.DefaultAsyncHttpClient.<init>(DefaultAsyncHttpClient.java:85)
        at cn.migu.newportal.cache.util.HttpTools.getAsyncHttpClient(HttpTools.java:109)
        at cn.migu.newportal.cache.util.HttpTools.<clinit>(HttpTools.java:95)

      DefaultAsyncHttpClient:

        /**
         * Create a new HTTP Asynchronous Client using the specified
         * {@link DefaultAsyncHttpClientConfig} configuration. This configuration
         * will be passed to the default {@link AsyncHttpClient} that will be
         * selected based on the classpath configuration.
         *
         * @param config a {@link DefaultAsyncHttpClientConfig}
         */
        public DefaultAsyncHttpClient(AsyncHttpClientConfig config) {
    
            this.config = config;
    
            allowStopNettyTimer = config.getNettyTimer() == null;
            nettyTimer = allowStopNettyTimer ? newNettyTimer() : config.getNettyTimer();
    
            channelManager = new ChannelManager(config, nettyTimer);
            requestSender = new NettyRequestSender(config, channelManager, nettyTimer, new AsyncHttpClientState(closed));
            channelManager.configureBootstraps(requestSender);
        }

      ChannelManager:

        public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) {
    
            this.config = config;
            this.sslEngineFactory = config.getSslEngineFactory() != null ? config.getSslEngineFactory() : new DefaultSslEngineFactory();
            try {
                this.sslEngineFactory.init(config);
            } catch (SSLException e) {
                throw new RuntimeException("Could not initialize sslEngineFactory", e);
            }
    
            ChannelPool channelPool = config.getChannelPool();
            if (channelPool == null) {
                if (config.isKeepAlive()) {
                    channelPool = new DefaultChannelPool(config, nettyTimer);
                } else {
                    channelPool = NoopChannelPool.INSTANCE;
                }
            }
            this.channelPool = channelPool;
    
            
            tooManyConnections = unknownStackTrace(new TooManyConnectionsException(config.getMaxConnections()), ChannelManager.class, "acquireChannelLock");
            tooManyConnectionsPerHost = unknownStackTrace(new TooManyConnectionsPerHostException(config.getMaxConnectionsPerHost()), ChannelManager.class, "acquireChannelLock");
            maxTotalConnectionsEnabled = config.getMaxConnections() > 0;
            maxConnectionsPerHostEnabled = config.getMaxConnectionsPerHost() > 0;
    
            if (maxTotalConnectionsEnabled || maxConnectionsPerHostEnabled) {
                openChannels = new DefaultChannelGroup("asyncHttpClient", GlobalEventExecutor.INSTANCE) {
                    @Override
                    public boolean remove(Object o) {
                        boolean removed = super.remove(o);
                        if (removed) {
                            if (maxTotalConnectionsEnabled)
                                freeChannels.release();
                            if (maxConnectionsPerHostEnabled) {
                                Object partitionKey = channelId2PartitionKey.remove(Channel.class.cast(o));
                                if (partitionKey != null) {
                                    Semaphore hostFreeChannels = freeChannelsPerHost.get(partitionKey);
                                    if (hostFreeChannels != null)
                                        hostFreeChannels.release();
                                }
                            }
                        }
                        return removed;
                    }
                };
                freeChannels = new Semaphore(config.getMaxConnections());
            } else {
                openChannels = new DefaultChannelGroup("asyncHttpClient", GlobalEventExecutor.INSTANCE);
                freeChannels = null;
            }
    
            handshakeTimeout = config.getHandshakeTimeout();
    
            // check if external EventLoopGroup is defined
            ThreadFactory threadFactory = config.getThreadFactory() != null ? config.getThreadFactory() : new DefaultThreadFactory(config.getThreadPoolName());
            allowReleaseEventLoopGroup = config.getEventLoopGroup() == null;
            ChannelFactory<? extends Channel> channelFactory;
            if (allowReleaseEventLoopGroup) {
                if (config.isUseNativeTransport()) {
                    eventLoopGroup = newEpollEventLoopGroup(config.getIoThreadsCount(), threadFactory);
                    channelFactory = getEpollSocketChannelFactory();
    
                } else {
                    eventLoopGroup = new NioEventLoopGroup(config.getIoThreadsCount(), threadFactory);
                    channelFactory = NioSocketChannelFactory.INSTANCE;
                }
    
            } else {
                eventLoopGroup = config.getEventLoopGroup();
                if (eventLoopGroup instanceof OioEventLoopGroup)
                    throw new IllegalArgumentException("Oio is not supported");
    
                if (eventLoopGroup instanceof NioEventLoopGroup) {
                    channelFactory = NioSocketChannelFactory.INSTANCE;
                } else {
                    channelFactory = getEpollSocketChannelFactory();
                }
            }
    
            httpBootstrap = newBootstrap(channelFactory, eventLoopGroup, config);
            wsBootstrap = newBootstrap(channelFactory, eventLoopGroup, config);
    
            // for reactive streams
            httpBootstrap.option(ChannelOption.AUTO_READ, false);
        }

      DefaultSslEngineFactory:

    package org.asynchttpclient.netty.ssl;
    
    import io.netty.buffer.ByteBufAllocator;
    import io.netty.handler.ssl.SslContext;
    import io.netty.handler.ssl.SslContextBuilder;
    import io.netty.handler.ssl.SslProvider;
    import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
    import java.util.Arrays;
    import javax.net.ssl.SSLEngine;
    import javax.net.ssl.SSLException;
    import org.asynchttpclient.AsyncHttpClientConfig;
    import org.asynchttpclient.util.MiscUtils;
    
    public class DefaultSslEngineFactory
      extends SslEngineFactoryBase
    {
      private volatile SslContext sslContext;
      
      private SslContext buildSslContext(AsyncHttpClientConfig config)
        throws SSLException
      {
        if (config.getSslContext() != null) {
          return config.getSslContext();
        }
        SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().sslProvider(config.isUseOpenSsl() ? SslProvider.OPENSSL : SslProvider.JDK).sessionCacheSize(config.getSslSessionCacheSize()).sessionTimeout(config.getSslSessionTimeout());
        if (MiscUtils.isNonEmpty(config.getEnabledProtocols())) {
          sslContextBuilder.protocols(config.getEnabledProtocols());
        }
        if (MiscUtils.isNonEmpty(config.getEnabledCipherSuites())) {
          sslContextBuilder.ciphers(Arrays.asList(config.getEnabledCipherSuites()));
        }
        if (config.isUseInsecureTrustManager()) {
          sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
        }
        return configureSslContextBuilder(sslContextBuilder).build();
      }
      
      public SSLEngine newSslEngine(AsyncHttpClientConfig config, String peerHost, int peerPort)
      {
        SSLEngine sslEngine = this.sslContext.newEngine(ByteBufAllocator.DEFAULT, peerHost, peerPort);
        configureSslEngine(sslEngine, config);
        return sslEngine;
      }
      
      public void init(AsyncHttpClientConfig config)
        throws SSLException
      {
        this.sslContext = buildSslContext(config);
      }
      
      protected SslContextBuilder configureSslContextBuilder(SslContextBuilder builder)
      {
        return builder;
      }
    }

      DefaultSslEngineFactory调用SslContextBuilder.protocols方法时找不到了,进入SslContextBuilder类,确实没有protocols方法。去看了下pomx.ml文件,引入的async-http-client.jar是2.1.0-alpha26版本,匹配的netty-all.jar却是4.1.8.Final版本的,而这个版本的SslContextBuilder并不存在protocols方法。

      问题解决:使用2.1.0-alpha6版本async-http-client的jar包,这个包里DefaultSslEngineFactory没有调用SslContextBuilder.protocols方法:

     * Copyright (c) 2015 AsyncHttpClient Project. All rights reserved.
    package org.asynchttpclient.netty.ssl;
    
    import io.netty.buffer.ByteBufAllocator;
    import io.netty.handler.ssl.SslContext;
    import io.netty.handler.ssl.SslContextBuilder;
    import io.netty.handler.ssl.SslProvider;
    
    import javax.net.ssl.SSLEngine;
    import javax.net.ssl.SSLException;
    
    import org.asynchttpclient.AsyncHttpClientConfig;
    
    public class DefaultSslEngineFactory extends SslEngineFactoryBase {
    
        private volatile SslContext sslContext;
    
        private SslContext buildSslContext(AsyncHttpClientConfig config) throws SSLException {
            if (config.getSslContext() != null)
                return config.getSslContext();
    
            SslContextBuilder sslContextBuilder = SslContextBuilder.forClient()//
                    .sslProvider(config.isUseOpenSsl() ? SslProvider.OPENSSL : SslProvider.JDK)//
                    .sessionCacheSize(config.getSslSessionCacheSize())//
                    .sessionTimeout(config.getSslSessionTimeout());
    
            if (config.isUseInsecureTrustManager()) {
                sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
            }
    
            return configureSslContextBuilder(sslContextBuilder).build();
        }
    
        @Override
        public SSLEngine newSslEngine(AsyncHttpClientConfig config, String peerHost, int peerPort) {
            // FIXME should be using ctx allocator
            SSLEngine sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT, peerHost, peerPort);
            configureSslEngine(sslEngine, config);
            return sslEngine;
        }
    
        @Override
        public void init(AsyncHttpClientConfig config) throws SSLException {
            sslContext = buildSslContext(config);
        }
    
        /**
         * The last step of configuring the SslContextBuilder used to create an SslContext when no context is provided in
         * the {@link AsyncHttpClientConfig}. This defaults to no-op and is intended to be overridden as needed.
         *
         * @param builder builder with normal configuration applied
         * @return builder to be used to build context (can be the same object as the input)
         */
        protected SslContextBuilder configureSslContextBuilder(SslContextBuilder builder) {
            // default to no op
            return builder;
        }
    
    }

      重启后测试通过,问题解决。

  • 相关阅读:
    mstsc远程桌面 mstsc /v:ip /admin
    JS模块化编程(五)---按照AMD规范扩展全局对象
    常见问题
    django--用户认证组件
    Django
    Django
    Django
    Django
    Django
    第六模块-图书管理系统--多表
  • 原文地址:https://www.cnblogs.com/wuxun1997/p/8808604.html
Copyright © 2011-2022 走看看