• websocketcometd源码阅读基于注解初始化(三)


    配置例子

     <!-- CometD Servlet -->
      <servlet>
        <servlet-name>cometd</servlet-name>
        <servlet-class>org.cometd.annotation.server.AnnotationCometDServlet</servlet-class>
        <!--liqiang todo 600000-->
        <init-param>
          <param-name>maxProcessing</param-name>
          <param-value>600000</param-value>
        </init-param>
        <init-param>
          <param-name>timeout</param-name>
          <param-value>20000</param-value>
        </init-param>
        <init-param>
          <param-name>interval</param-name>
          <param-value>0</param-value>
        </init-param>
        <init-param>
          <param-name>maxInterval</param-name>
          <param-value>10000</param-value>
        </init-param>
        <init-param>
          <param-name>handshakeReconnect</param-name>
          <param-value>true</param-value>
        </init-param>
        <init-param>
          <param-name>maxLazyTimeout</param-name>
          <param-value>5000</param-value>
        </init-param>
        <init-param>
          <param-name>long-polling.multiSessionInterval</param-name>
          <param-value>2000</param-value>
        </init-param>
        <init-param>
          <param-name>services</param-name>
          <param-value>org.cometd.examples.ChatService</param-value>
        </init-param>
        <init-param>
          <param-name>ws.cometdURLMapping</param-name>
          <param-value>/cometd/*</param-value>
        </init-param>
        <!--容器启动时调用init方法初始化 而不是第一次调用时-->
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
      </servlet>

    基于注解的使用例子

    @Service("chat") //comted注解
    public class ChatService {
        //保存用户信息 不同房间
        private final ConcurrentMap<String, Map<String, String>> _members = new ConcurrentHashMap<>();
        @Inject //comted注解注入
        private BayeuxServer _bayeux;
        @Session //comted注解注入
        private ServerSession _session;
    
        //初始化 "/chat/**", "/members/**" 渠道
        @Configure({"/chat/**", "/members/**"})
        protected void configureChatStarStar(ConfigurableServerChannel channel) {
            DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(), new BadWordFilter());
            channel.addListener(noMarkup);
            channel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
        }
        //初始化 "/service/members" 渠道
        @Configure("/service/members")
        protected void configureMembers(ConfigurableServerChannel channel) {
            channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
            channel.setPersistent(true);
        }
    
        /**
         * 渠道监听器
         * @param client
         * @param message
         */
        @Listener("/service/members")
        public void handleMembership(ServerSession client, ServerMessage message) {
            //获得消息
            Map<String, Object> data = message.getDataAsMap();
            //获得房间信息
            String room = ((String)data.get("room")).substring("/chat/".length());
            //获得房间的用户信息
            Map<String, String> roomMembers = _members.get(room);
            if (roomMembers == null) {
                Map<String, String> new_room = new ConcurrentHashMap<>();
                //加入房间
                roomMembers = _members.putIfAbsent(room, new_room);
                if (roomMembers == null) {
                    roomMembers = new_room;
                }
            }
            Map<String, String> members = roomMembers;
            //获得房间的用户信息
            String userName = (String)data.get("user");
            //用户信息与clientId映射
            members.put(userName, client.getId());
            //添加监听器 此客户端断开连接后 告诉当前房间所有人连接词用户下线 以及清除连接信息
            client.addListener((ServerSession.RemovedListener)(s, m, t) -> {
                //清除此房间连接信息
                members.values().remove(s.getId());
                //通知所有用户
                broadcastMembers(room, members.keySet());
            });
            //建立连接后推送订阅者最新的成员名单
            broadcastMembers(room, members.keySet());
        }
    
        private void broadcastMembers(String room, Set<String> members) {
            // Broadcast the new members list 推送订阅此房间的所有用户告知用户下线
            ClientSessionChannel channel = _session.getLocalSession().getChannel("/members/" + room);
            channel.publish(members);
        }
    
        @Configure("/service/privatechat")
        protected void configurePrivateChat(ConfigurableServerChannel channel) {
            DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(), new BadWordFilter());
            channel.setPersistent(true);
            channel.addListener(noMarkup);
            channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
        }
    
        @Listener("/service/privatechat")
        public void privateChat(ServerSession client, ServerMessage message) {
            Map<String, Object> data = message.getDataAsMap();
            String room = ((String)data.get("room")).substring("/chat/".length());
            Map<String, String> membersMap = _members.get(room);
            if (membersMap == null) {
                Map<String, String> new_room = new ConcurrentHashMap<>();
                membersMap = _members.putIfAbsent(room, new_room);
                if (membersMap == null) {
                    membersMap = new_room;
                }
            }
            String[] peerNames = ((String)data.get("peer")).split(",");
            ArrayList<ServerSession> peers = new ArrayList<>(peerNames.length);
    
            for (String peerName : peerNames) {
                String peerId = membersMap.get(peerName);
                if (peerId != null) {
                    ServerSession peer = _bayeux.getSession(peerId);
                    if (peer != null) {
                        peers.add(peer);
                    }
                }
            }
    
            if (peers.size() > 0) {
                Map<String, Object> chat = new HashMap<>();
                String text = (String)data.get("chat");
                chat.put("chat", text);
                chat.put("user", data.get("user"));
                chat.put("scope", "private");
                ServerMessage.Mutable forward = _bayeux.newMessage();
                forward.setChannel("/chat/" + room);
                forward.setId(message.getId());
                forward.setData(chat);
    
                // test for lazy messages
                if (text.lastIndexOf("lazy") > 0) {
                    forward.setLazy(true);
                }
    
                for (ServerSession peer : peers) {
                    if (peer != client) {
                        peer.deliver(_session, forward, Promise.noop());
                    }
                }
                client.deliver(_session, forward, Promise.noop());
            }
        }
    
        class BadWordFilter extends JSONDataFilter {
            @Override
            protected Object filterString(ServerSession session, ServerChannel channel, String string) {
                if (string.contains("dang")) {
                    throw new DataFilter.AbortException();
                }
                return string;
            }
        }
    }

    源码

    <1>

    org.cometd.annotation.server.AnnotationCometDServlet#init

       @Override
        public void init() throws ServletException {
            //<2>继承了comet 这里是先对comte进行初始化
            super.init();
    
            //基于注解方式初始化处理类对象的Processor  new ServerAnnotationProcessor(bayeuxServer);
            processor = newServerAnnotationProcessor(getBayeux());
    
            //获得servlet initParameter的Service配置
            String servicesParam = getInitParameter("services");
            if (servicesParam != null && servicesParam.length() > 0) {
    
                for (String serviceClass : servicesParam.split(",")) {
                    //<3>创建处理类对象 也就是xml配置的chatServerce
                    Object service = processService(processor, serviceClass.trim());
                    services.add(service);
                    //设置当前Service到requestAttribute
                    registerService(service);
                }
            }
        }
    
        protected ServerAnnotationProcessor newServerAnnotationProcessor(BayeuxServer bayeuxServer) {
            return new ServerAnnotationProcessor(bayeuxServer);
        }

    <3>

    org.cometd.annotation.server.AnnotationCometDServlet#processService

        protected Object processService(ServerAnnotationProcessor processor, String serviceClassName) throws ServletException {
            try {
                //反射根据类的全名称创建类的对象
                Object service = newService(serviceClassName);
                //<4>初始化
                processor.process(service);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Processed annotated service {}", service);
                }
                return service;
            } catch (Exception x) {
                LOGGER.warn("Failed to create annotated service " + serviceClassName, x);
                throw new ServletException(x);
            }
        }
    
        protected Object newService(String serviceClassName) throws Exception {
            return Loader.loadClass(getClass(), serviceClassName).newInstance();
        }

    <4>

    org.cometd.annotation.server.ServerAnnotationProcessor#process

      public boolean process(Object bean) {
            //<5>基于注解依赖注入主要处理 @Inject 和@Session注解
            boolean result = processDependencies(bean);
            //<11>根据@Configure注解的方法初始化channel
            result |= processConfigurations(bean);
            //<13>处理Subscription 订阅渠道
            result |= processCallbacks(bean);
            //<15>调用@PostConstruct 进行初始化操作
            result |= processPostConstruct(bean);
            return result;
        }

    <5>

    org.cometd.annotation.server.ServerAnnotationProcessor#processDependencies

     public boolean processDependencies(Object bean) {
            if (bean == null) {
                return false;
            }
    
            Class<?> klass = bean.getClass();
            //首选需要打了comted的@Service注解
            Service serviceAnnotation = klass.getAnnotation(Service.class);
            if (serviceAnnotation == null) {
                return false;
            }
    
            //容器中bean对象
            List<Object> injectables = new ArrayList<>(Arrays.asList(this.injectables));
            //默认增加是bayeuxServer 所以支持@Inject进行 bayeuxServer注入
            injectables.add(0, bayeuxServer);
            //<6>针对 bean的@Inject注解进行 注入
            boolean result = processInjectables(bean, injectables);
            //<7>针对Service初始化loaclSession 同时初始化一个serverSession与bayeuxServer模拟建立连接
            LocalSession session = findOrCreateLocalSession(bean, serviceAnnotation.value());
            //<10>session注入到bean 后续我们可以通过操作session发送消息
            result |= processSession(bean, session);
            return result;
        }

    <6>

    org.cometd.annotation.AnnotationProcessor#processInjectables

     protected boolean processInjectables(Object bean, List<Object> injectables) {
            boolean result = false;
            //遍历容器
            for (Object injectable : injectables) {
                result |= processInjectable(bean, injectable);
            }
            return result;
        }
    
        protected boolean processInjectable(Object bean, Object injectable) {
            boolean result = false;
            for (Class<?> c = bean.getClass(); c != Object.class; c = c.getSuperclass()) {
                //反射获得fields 获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。
                Field[] fields = c.getDeclaredFields();
                for (Field field : fields) {
                    //如果打了@Inject注解
                    if (field.getAnnotation(Inject.class) != null) {
                        /**
                         * 如果是A.isAssignableFrom(B) 确定一个类(B)是不是继承来自于另一个父类(A),一个接口(A)是不是实现了另外一个接口(B),
                         * 或者两个类相同。主要,这里比较的维度不是实例对象,而是类本身,因为这个方法本身就是Class类的方法,判断的肯定是和类信息相关的。
                         */
                        if (field.getType().isAssignableFrom(injectable.getClass())) {
                            Object value = getField(bean, field);
                            if (value != null) {
                                if (LOGGER.isDebugEnabled()) {
                                    LOGGER.debug("Avoid injection of field {} on bean {}, it's already injected with {}", field, bean, value);
                                }
                                continue;
                            }
                            //完成注入
                            setField(bean, field, injectable);
                            result = true;
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug("Injected {} to field {} on bean {}", injectable, field, bean);
                            }
                        }
                    }
                }
            }
    
            //针对打在set方法上 的注入
            List<Method> methods = findAnnotatedMethods(bean, Inject.class);
            for (Method method : methods) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == 1) {
                    if (parameterTypes[0].isAssignableFrom(injectable.getClass())) {
                        invokePrivate(bean, method, injectable);
                        result = true;
                        if (LOGGER.isDebugEnabled()) {
                            LOGGER.debug("Injected {} to method {} on bean {}", injectable, method, bean);
                        }
                    }
                }
            }
            return result;
        }

    <7>

    org.cometd.annotation.server.ServerAnnotationProcessor#findOrCreateLocalSession

    private LocalSession findOrCreateLocalSession(Object bean, String name) {
            LocalSession session = sessions.get(bean);
            if (session == null) {
                //<8>创建一个Local session
                session = bayeuxServer.newLocalSession(name);
                //bean的名字作为key维护放入sessions
                LocalSession existing = sessions.putIfAbsent(bean, session);
                if (existing != null) {
                    session = existing;
                } else {
                    //<9>默认握手加入bayeuxServer
                    session.handshake();
                }
            }
            return session;
        }

    <8>

    org.cometd.server.BayeuxServerImpl#newLocalSession

    @Override
        public LocalSession newLocalSession(String idHint) {
            return new LocalSessionImpl(this, idHint);
        }

    <9>

    org.cometd.server.LocalSessionImpl#handshake

     @Override
        public void handshake(Map<String, Object> template, ClientSession.MessageListener callback) {
            if (_session != null) {
                throw new IllegalStateException("Method handshake() invoke multiple times for local session " + this);
            }
    
            ServerSessionImpl session = new ServerSessionImpl(_bayeux, this, _idHint);
    
            ServerMessage.Mutable hsMessage = newMessage();
            if (template != null) {
                hsMessage.putAll(template);
            }
            String messageId = newMessageId();
            hsMessage.setId(messageId);
            hsMessage.setChannel(Channel.META_HANDSHAKE);
            registerCallback(messageId, callback);
    
            doSend(session, hsMessage, Promise.from(hsReply -> {
                if (hsReply != null && hsReply.isSuccessful()) {
                    _session = session;
                    ServerMessage.Mutable cnMessage = newMessage();
                    cnMessage.setId(newMessageId());
                    cnMessage.setChannel(Channel.META_CONNECT);
                    cnMessage.getAdvice(true).put(Message.INTERVAL_FIELD, -1L);
                    cnMessage.setClientId(session.getId());
                    doSend(session, cnMessage, Promise.from(cnReply -> {
                        // Nothing more to do.
                    }, failure -> messageFailure(cnMessage, failure)));
                }
            }, failure -> messageFailure(hsMessage, failure)));
        }

    <10>

    org.cometd.annotation.server.ServerAnnotationProcessor#processSession

    private boolean processSession(Object bean, LocalSession localSession) {
            ServerSession serverSession = localSession.getServerSession();
    
            boolean result = false;
            for (Class<?> c = bean.getClass(); c != Object.class; c = c.getSuperclass()) {
                Field[] fields = c.getDeclaredFields();
                for (Field field : fields) {
                    if (field.getAnnotation(Session.class) != null) {
                        Object value = null;
                        if (field.getType().isAssignableFrom(localSession.getClass())) {
                            value = localSession;
                        } else if (field.getType().isAssignableFrom(serverSession.getClass())) {
                            value = serverSession;
                        }
    
                        if (value != null) {
                            setField(bean, field, value);
                            result = true;
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug("Injected {} to field {} on bean {}", value, field, bean);
                            }
                        }
                    }
                }
            }
    
            List<Method> methods = findAnnotatedMethods(bean, Session.class);
            for (Method method : methods) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == 1) {
                    Object value = null;
                    if (parameterTypes[0].isAssignableFrom(localSession.getClass())) {
                        value = localSession;
                    } else if (parameterTypes[0].isAssignableFrom(serverSession.getClass())) {
                        value = serverSession;
                    }
    
                    if (value != null) {
                        invokePrivate(bean, method, value);
                        result = true;
                        if (LOGGER.isDebugEnabled()) {
                            LOGGER.debug("Injected {} to method {} on bean {}", value, method, bean);
                        }
                    }
                }
            }
            return result;
        }

    <11>

    org.cometd.annotation.server.ServerAnnotationProcessor#processConfigurations

    public boolean processConfigurations(Object bean) {
            if (bean == null) {
                return false;
            }
    
            Class<?> klass = bean.getClass();
            //首选需要打了@Service注解
            Service serviceAnnotation = klass.getAnnotation(Service.class);
            if (serviceAnnotation == null) {
                return false;
            }
    
            //首先需要打了打了@Configure的方法
            List<Method> methods = findAnnotatedMethods(bean, Configure.class);
            if (methods.isEmpty()) {
                return false;
            }
    
            for (Method method : methods) {
                Configure configure = method.getAnnotation(Configure.class);
                //@Configure配置的value为channelName
                String[] channels = configure.value();
                for (String channelName : channels) {
                    //定义一个Initializer接口的匿名方法 内部会调用调用@Configure注解方法
                    Initializer init = channel -> {
                        if (LOGGER.isDebugEnabled()) {
                            LOGGER.debug("Configure channel {} with method {} on bean {}", channel, method, bean);
                        }
                        invokePrivate(bean, method, channel);
                    };
    
                    //<12>针对@Configure创建Channel
                    MarkedReference<ServerChannel> initializedChannel = bayeuxServer.createChannelIfAbsent(channelName, init);
    
                    //内部初始化成功会将marked设置为true
                    if (!initializedChannel.isMarked()) {
                        //是否配置了configureIfExists 为true 默认false 如果为true则会调用打了@Configure的方法
                        if (configure.configureIfExists()) {
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug("Configure again channel {} with method {} on bean {}", channelName, method, bean);
                            }
                            init.configureChannel(initializedChannel.getReference());
                        } else if (configure.errorIfExists()) {
                            throw new IllegalStateException("Channel already configured: " + channelName);
                        } else {
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug("Channel {} already initialized. Not called method {} on bean {}", channelName, method, bean);
                            }
                        }
                    }
                }
            }
            return true;
        }

    <12>

    org.cometd.server.BayeuxServerImpl#createChannelIfAbsent

      @Override
        public MarkedReference<ServerChannel> createChannelIfAbsent(String channelName, Initializer... initializers) {
            ChannelId channelId;
            boolean initialized = false;
            //尝试根据channelName获取 判断是否存在
            ServerChannelImpl channel = _channels.get(channelName);
            if (channel == null) {
                // Creating the ChannelId will also normalize the channelName.
                //尝试通过处理过的channelId获取
                channelId = new ChannelId(channelName);
                String id = channelId.getId();
                if (!id.equals(channelName)) {
                    channelName = id;
                    channel = _channels.get(channelName);
                }
            } else {
                channelId = channel.getChannelId();
            }
    
            //表示没有被初始化
            if (channel == null) {
                //新建一个channel
                ServerChannelImpl candidate = new ServerChannelImpl(this, channelId);
                //放入_channels
                channel = _channels.putIfAbsent(channelName, candidate);
                if (channel == null) {
                    // My candidate channel was added to the map, so I'd better initialize it
    
                    channel = candidate;
                    if (_logger.isDebugEnabled()) {
                        _logger.debug("Added channel {}", channel);
                    }
    
                    try {
                        //通知 Initializer实现 可以对ServerChannelImpl做自定义配置
                        for (Initializer initializer : initializers) {
                            notifyConfigureChannel(initializer, channel);
                        }
    
                        //调用listeners中ChannelListener的configureChannel方法可以对channel进行自定义配置
                        for (BayeuxServer.BayeuxServerListener listener : _listeners) {
                            if (listener instanceof ServerChannel.Initializer) {
                                notifyConfigureChannel((Initializer)listener, channel);
                            }
                        }
                    } finally {
                        channel.initialized();
                    }
                    //调用listeners中ChannelListener的channelAdded表示已经被初始化
                    for (BayeuxServer.BayeuxServerListener listener : _listeners) {
                        if (listener instanceof BayeuxServer.ChannelListener) {
                            notifyChannelAdded((ChannelListener)listener, channel);
                        }
                    }
    
                    initialized = true;
                }
            } else {
                channel.resetSweeperPasses();
                // Double check if the sweeper removed this channel between the check at the top and here.
                // This is not 100% fool proof (e.g. this thread is preempted long enough for the sweeper
                // to remove the channel, but the alternative is to have a global lock)
                _channels.putIfAbsent(channelName, channel);
            }
            // Another thread may add this channel concurrently, so wait until it is initialized
            channel.waitForInitialized();
            return new MarkedReference<>(channel, initialized);
        }

    <13>

    org.cometd.annotation.server.ServerAnnotationProcessor#processCallbacks

     public boolean processCallbacks(Object bean) {
            if (bean == null) {
                return false;
            }
    
            Class<?> klass = bean.getClass();
            Service serviceAnnotation = klass.getAnnotation(Service.class);
            if (serviceAnnotation == null) {
                return false;
            }
    
            if (!Modifier.isPublic(klass.getModifiers())) {
                throw new IllegalArgumentException("Service class " + klass.getName() + " must be public");
            }
            //<7>根据打了Service的创建Session 如果创建过了就不会再创建
            LocalSession session = findOrCreateLocalSession(bean, serviceAnnotation.value());
            //<14>处理@@Listener注解往渠道新增监听器 监听渠道
            boolean result = processListener(bean, session);
            //处理Subscription  往渠道新增订阅器 订阅渠道
            result |= processSubscription(bean, session);
            //liqiangtodo @RemoteCall 好像也是监听的一种信息 处理类似RPC请求参考
            result |= processRemoteCall(bean, session);
            return result;
        }

    <14>

    org.cometd.annotation.server.ServerAnnotationProcessor#processListener

     private static final Class<?>[] signature = new Class<?>[]{ServerSession.class, ServerMessage.Mutable.class};
        private boolean processListener(Object bean, LocalSession localSession) {
            //这里主要是做个检查针对打了@Listener的方法不是public的则报错
            AnnotationProcessor.checkMethodsPublic(bean, Listener.class);
    
            boolean result = false;
            Method[] methods = bean.getClass().getMethods();
            for (Method method : methods) {
                if (method.getDeclaringClass() == Object.class) {
                    continue;
                }
    
                Listener listener = method.getAnnotation(Listener.class);
                if (listener != null) {
                    //获得@Listener 打了@Param的方法的value
                    List<String> paramNames = processParameters(method);
                    //检查方法的注入 必须注入signature的类型,以及@Param注解的变量必须是String
                    AnnotationProcessor.checkSignaturesMatch(method, ListenerCallback.signature, paramNames);
    
                    //@Listener监听的渠道名称
                    String[] channels = listener.value();
                    for (String channel : channels) {
                        ChannelId channelId = new ChannelId(channel);
                        if (channelId.isTemplate()) {
                            List<String> parameters = channelId.getParameters();
                            if (parameters.size() != paramNames.size()) {
                                throw new IllegalArgumentException("Wrong number of template parameters in annotation @" +
                                        Listener.class.getSimpleName() + " on method " +
                                        method.getDeclaringClass().getName() + "." + method.getName() + "(...)");
                            }
                            if (!parameters.equals(paramNames)) {
                                throw new IllegalArgumentException("Wrong parameter names in annotation @" +
                                        Listener.class.getSimpleName() + " on method " +
                                        method.getDeclaringClass().getName() + "." + method.getName() + "(...)");
                            }
                            channel = channelId.getRegularPart() + "/" + (parameters.size() < 2 ? ChannelId.WILD : ChannelId.DEEPWILD);
                        }
    
                        //渠道不存在的创建渠道
                        MarkedReference<ServerChannel> initializedChannel = bayeuxServer.createChannelIfAbsent(channel);
                        ListenerCallback listenerCallback = new ListenerCallback(localSession, bean, method, paramNames, channelId, channel, listener.receiveOwnPublishes());
                        //在渠道里面新增此监听器获取消息回调
                        initializedChannel.getReference().addListener(listenerCallback);
    
                        List<ListenerCallback> callbacks = listeners.get(bean);
                        if (callbacks == null) {
                            callbacks = new CopyOnWriteArrayList<>();
                            List<ListenerCallback> existing = listeners.putIfAbsent(bean, callbacks);
                            if (existing != null) {
                                callbacks = existing;
                            }
                        }
                        callbacks.add(listenerCallback);
                        result = true;
                        if (LOGGER.isDebugEnabled()) {
                            LOGGER.debug("Registered listener for channel {} to method {} on bean {}", channel, method, bean);
                        }
                    }
                }
            }
            return result;
        }

    <15>

    org.cometd.annotation.AnnotationProcessor#processPostConstruct

      protected boolean processPostConstruct(Object bean) {
            if (bean == null) {
                return false;
            }
    
            List<Method> postConstructs = findLifeCycleMethods(bean, PostConstruct.class);
    
            boolean result = false;
            for (Method method : postConstructs) {
                invokePrivate(bean, method);
                result = true;
            }
    
            return result;
        }
  • 相关阅读:
    蔚来汽车笔试题---软件测试
    python 装饰器
    adb
    新手安装禅道至本地
    各种验证码应该如何给值
    int col = Integer.parseInt(args[0]);的用法
    找不到jsp文件:ctrl+shift+R
    通过服务器获取验证码
    Sublime Text 2: [Decode error
    爬虫爬取新闻(二)
  • 原文地址:https://www.cnblogs.com/LQBlog/p/16553995.html
Copyright © 2020-2023  润新知