• 14、Cahin of Responsibility 责任链 COR设计模式


    责任链模型初体现

    • 通过上面的代码可以看到帖子处理器会对帖子进行不同的过滤, 我们可以把一种过滤方法对应为一个过滤器, 并且向上抽取出过滤器接口.

     public class Demo2 {
         public static void main(String[] args) {
             String msg = "大家好 :), <script>haha</script> 我要说超级敏感的话";
             MsgProcessor mp = new MsgProcessor();
             mp.setMsg(msg);
             System.out.println(mp.process());
        }
     }
     
     class MsgProcessor{
         private String msg;
         private Filter[] filters = {new HtmlFilter(), new SensitiveFilter(), new ExpressionFilter()};
         public String process(){
             for(Filter f : filters){
                 msg = f.doFilter(msg);
            }
             return msg;
        }
     
         public String getMsg() {
             return msg;
        }
     
         public void setMsg(String msg) {
             this.msg = msg;
        }
     }
     //过滤器接口
     interface Filter{
         public String doFilter(String s);
     }
     //处理html标签
     class HtmlFilter implements Filter{
         @Override
         public String doFilter(String s) {
             return s.replace("<", "[").replace(">", "]");
        }
     }
     //处理敏感词句
     class SensitiveFilter implements Filter{
         @Override
         public String doFilter(String s) {
             return s.replace("敏感", "正常");
        }
     }
     //处理表情
     class ExpressionFilter implements Filter{
         @Override
         public String doFilter(String s) {
             return s.replace(":)", "^_^");
        }
     }

     

    • 上面的代码已经具备了责任链的模型. 在帖子发送到服务器的过程中, 它将依次经过3个过滤器, 这三个过滤器就构成一条过滤器链.

    • image-20200729150037295

     

    • 下面我们考虑, 如果我们要在帖子处理过程中加入新的过滤器链条, 加在原链条的末尾或中间, 该怎么办呢?

    • 消息经过过滤器链条的过程会得到处理, 我们可以把过滤器链条看成一个过滤器, 让他也实现Filter接口, 那么就可以在一条过滤链中任意加入其他过滤器和过滤链了.

    • 下面的代码实现了过滤链FilterChain, 用过滤链替代原来的MsgProcessor.

     public class Demo3 {
         public static void main(String[] args) {
             String msg = "大家好 :), <script>haha</script> 我要说超级敏感的话";//待处理的帖子
             FilterChain fc1 = new FilterChain();//创建一条过滤器链1
             fc1.add(new HtmlFilter())
                    .add(new SensitiveFilter());//往过滤器链1中添加过滤器
     
             FilterChain fc2 = new FilterChain();//创建一条过滤器链2
             fc2.add(new ExpressionFilter());//往过滤器链2中添加过滤器
     
             fc1.add(fc2);//把过滤器链2当作过滤器添加到过滤器链1中,(过滤器链实现了Filter接口)
     
             msg = fc1.doFilter(msg);//使用过滤器链1对帖子进行过滤
             System.out.println(msg);
        }
     }
     
     class FilterChain implements Filter{
     
         private List<Filter> list = new ArrayList<>();
     
         public FilterChain add(Filter filter){
             this.list.add(filter);
             return this;
        }
     
         @Override
         public String doFilter(String s) {
             for(Filter f : list){
                 s = f.doFilter(s);
            }
             return s;
        }
     }
     
     class HtmlFilter implements Filter{
         @Override
         public String doFilter(String s) {
             return s.replace("<", "[").replace(">", "]");
        }
     }
     
     class SensitiveFilter implements Filter{
         @Override
         public String doFilter(String s) {
             return s.replace("敏感", "正常");
        }
     }
     
     class ExpressionFilter implements Filter{
         @Override
         public String doFilter(String s) {
             return s.replace(":)", "^_^");
        }
     }
     
     interface Filter{
         public String doFilter(String s);
     }

     

    更精巧设计, 展现责任链模式

    • 在继续优化之前, 我们考虑更现实的需求, 一个请求(发出一个帖子)作为数据报发送给服务器, 服务器除了需要对请求进行过滤外, 还需要给出响应, 并且可能要对响应也进行处理. 如下图所示

    image-20200729145957738

    • 当一个消息(包含请求体和响应体)发往服务器时, 它将依次经过过滤器1, 2, 3. 而当处理完成后, 封装好响应发出服务器时, 它也将依次经过过滤器3, 2, 1.

    • 大家可能会觉得有点像栈结构, 但是像归像, 这一逻辑应该如何实现呢?

    • 首先我们可以让过滤器持有过滤器链的引用, 通过调用过滤器链依次执行每个过滤器. 为了能让过滤器依次执行每个过滤器, 过滤器会持有一个index序号, 通过序号控制执行顺序. 至于后面对response的倒序请求, 则通过方法返回实现. 这部分设计纯用文字难以讲清, 请务必看下面的代码和代码后的分析, 配图.

    • 这个部分是责任链的精髓了, 懂了这部分代码, 看Web开发中的过滤器源码就没压力了.

     public class Demo4 {
         public static void main(String[] args) {
             String msg = "大家好 :), <script>haha</script> 我要说超级敏感的话";//以下三行模拟一个请求
             Request request = new Request();
             request.setRequestStr(msg);
     
             Response response = new Response();//响应
     
             FilterChain fc = new FilterChain();//过滤器链
             HtmlFilter f1 = new HtmlFilter();//创建过滤器
             SensitiveFilter f2 = new SensitiveFilter();
             ExpressionFilter f3 = new ExpressionFilter();
             fc.add(f1);//把过滤器添加到过滤器链中
             fc.add(f2);
             fc.add(f3);
     
             fc.doFilter(request, response, fc);//直接调用过滤器链的doFilter()方法进行处理
             System.out.println(request.getRequestStr());
        }
     }
     
     interface Filter{
         public void doFilter(Request request, Response response, FilterChain fc);
     }
     
     class FilterChain implements Filter{
     
         private List<Filter> list = new ArrayList<>();
     
         private int index = 0;
     
         public FilterChain add(Filter filter){
             this.list.add(filter);
             return this;
        }
     
         @Override
         public void doFilter(Request request, Response response, FilterChain fc) {
             if(index == list.size()){
                 return;//这里是逆序处理响应的关键, 当index为容器大小时, 证明对request的处理已经完成, 下面进入对response的处理.
            }
             Filter f = list.get(index);//过滤器链按index的顺序拿到filter
             index++;
             f.doFilter(request, response, fc);
        }
     
     }
     
     class HtmlFilter implements Filter{
         @Override
         public void doFilter(Request request, Response response, FilterChain fc) {
             request.setRequestStr(request.getRequestStr().replace("<", "[").replace(">","]"));
             System.out.println("在HtmlFilter中处理request");//先处理request
             fc.doFilter(request, response, fc);//调用过滤器链的doFilter方法, 让它去执行下一个Filter的doFilter方法, 处理response的代码将被挂起
             System.out.println("在HtmlFilter中处理response");
        }
     }
     
     class SensitiveFilter implements Filter{
         @Override
         public void doFilter(Request request, Response response, FilterChain fc) {
             request.setRequestStr(request.getRequestStr().replace("敏感", "正常"));
             System.out.println("在SensitiveFilter中处理request");
             fc.doFilter(request, response, fc);
             System.out.println("在SensitiveFilter中处理response");
        }
     }
     
     class ExpressionFilter implements Filter{
         @Override
         public void doFilter(Request request, Response response, FilterChain fc) {
             request.setRequestStr(request.getRequestStr().replace(":)", "^_^"));
             System.out.println("在ExpressionFilter中处理request");
             fc.doFilter(request, response, fc);
             System.out.println("在ExpressionFilter中处理response");
        }
     }
     
     class Request{
         private String requestStr;//真正的Request对象中是包含很多信息的, 这里仅用一个字符串作模拟
     
         public String getRequestStr() {
             return requestStr;
        }
     
         public void setRequestStr(String requestStr) {
             this.requestStr = requestStr;
        }
     }
     
     class Response{
         private String responseStr;
     
         public String getResponseStr() {
             return responseStr;
        }
     
         public void setResponseStr(String responseStr) {
             this.responseStr = responseStr;
        }
     }

     

    • 下面我描述一次整个过程, 你可以根据文字找到相应的代码进行理解.

    • 首先我们分别创建一个RequestResponse对象. Request在传入进后端时需要依次被过滤器1, 2, 3进行处理, Response对象在输出时要依次被过滤器3, 2, 1处理.

    • 创建好请求和响应对象后我们创建过滤器链, 并依次加入过滤器1, 2, 3. 整个处理流程将交给过滤器链决定.

    • 接着我们调用过滤器链的doFilter()方法对request对象进行处理

    • 这时过滤器链中的index值为0, 通过index我们找到第一个过滤器并调用它的doFilter()方法, 我们观察这段代码

     class HtmlFilter implements Filter{
         @Override
         public void doFilter(Request request, Response response, FilterChain fc) {
             request.setRequestStr(request.getRequestStr().replace("<", "[").replace(">","]"));
             System.out.println("在HtmlFilter中处理request");//先处理request
             
             fc.doFilter(request, response, fc);//调用过滤器链的doFilter方法, 让它去执行下一个Filter的doFilter方法, 处理response的代码将被挂起
             
             //在返回的过程中执行response
             System.out.println("在HtmlFilter中处理response");
        }
     }
    • 进入doFilter()方法后, 首先会对request请求进行处理, 然后又调用了过滤器链的doFilter()方法. 这就是整个责任链模式的精妙之处, 它解释了为什么要给doFilter()加上一个过滤器链参数, 就是为了让每个过滤器可以调用过滤器链本身执行下一个过滤器.

    • 为什么要调用过滤器链本身? 因为当调用过滤器本身后, 程序将跳转回到过滤器链的doFilter方法执行, 这时index为1, 也就是拿到第二个过滤器, 然后继续处理.

    • 正是由于这个跳转, 使得过滤器中对response的处理暂时无法执行, 它必须等待上面的对过滤器链的方法返回才能被执行.

    • 所以最后我们将看到response响应被过滤器3, 2, 1(和请求倒序)执行.

    image-20200729150231127

    • 放大招了, 如果看了上面的图还是不懂, 欢迎给我留言.

    • 整个责任链模式已经从无到有展现出来了

     

    阅读Tomcat中的Filter过滤器源码, 加深理解.

    • 相信通过上面的讲解, 你已经对整个责任链模式有了进一步的理解.

    • 下面我们通过阅读Tomcat里Filter的源码感受一下.

     public interface Filter {
         void init(FilterConfig var1) throws ServletException;
         //熟悉的doFilter(), 熟悉的3个参数request, reponse, filterChain.
         void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
     
         void destroy();
     }
    • 我们可以看到Filter接口的定义和我们的讲解的差不多, 只是多了初始化和销毁的方法.

     public interface FilterChain {
         void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
     }
    • 同时我们也看到它把FilterChain也向上抽取成接口, 不过这里的FilterChain没有实现Filter接口, 也就是说我们不能把两条FilterChain拼接在一起, 换个角度想Tomcat中的过滤器的可扩展性还没有我们例子中的好呢^_^

     

     

     

    扩展:

    1、监听器

    结论

    img

    执行流程图说明:

    1)用户通过在浏览器中输出网址http://localhost:8080/web_servlet_01_war/hello,访问web容器中部署的servlet服务;

    2)用户请求信息被web容器监听到,web容器会浏览器请求信息进行封装HttpServletRequest、响应信息会被封装到HttpServletResponse,并找到对应的servlet容器;

    3)请求进入servlet容器会被listener监听到,listener分为两类:ServletRequestListener、ServletContextListener。

    ServletRequestListener会在每次servlet请求过程中,都会执行它的#requestInitialized方法,然后交给filter去执行;

    ServletContextListener会在servlet服务启动时,调用它的#contextInitialized()方法;在servlet服务关闭时,调用它的#contextDestroyed()方法。

    4)然后请求被filter执行,调用filter#doFilter() before方法,filter的初始化时刻:“Servlet服务启动” 或 “第一次访问初始化”,根据依据<load-on-startup>1</load-on-startup>参数;

    5)请求交给servlet#service()方法;

    6)servlet#service()方法执行完成后,会回到filter#doFilter() after方法;

    7)执行ServletRequestListener#requestDestroyed()方法;

    8)将请求响应反馈给浏览器;

    9)当服务关闭时,会执行servlet#destory()、filter#destory()、ServletContextListener#contextDestroyed()

    源码分析

    img

    Tomcat接收到请求后,会在容器(Engine、Host、Context、Wrapper各级组件)中匹配,并且在它们的管道中流转,最终会适配到一个StandardWrapper的基础阀的-org.apache.catalina.core.StandardWrapperValve 的invoke方法。

    下边具体看下请求匹配到最基础的 StandardWrapper 组件的管道中,之后是如何处理的:

    StandardWrapperValve阀的#invoke方法:

    复制代码

     final class StandardWrapperValve extends ValveBase {
        ...
        @Override
        public final void invoke(Request request, Response response)
            throws IOException, ServletException {
     
            // Initialize local variables we may need
            boolean unavailable = false;
            Throwable throwable = null;
            // This should be a Request attribute...
            long t1=System.currentTimeMillis();
            requestCount.incrementAndGet();
            StandardWrapper wrapper = (StandardWrapper) getContainer();
            Servlet servlet = null;
            Context context = (Context) wrapper.getParent();
     
            // Check for the application being marked unavailable
            if (!context.getState().isAvailable()) {
                response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, sm.getString("standardContext.isUnavailable"));
                unavailable = true;
            }
     
            // Check for the servlet being marked unavailable
            if (!unavailable && wrapper.isUnavailable()) {
                container.getLogger().info(sm.getString("standardWrapper.isUnavailable", wrapper.getName()));
                long available = wrapper.getAvailable();
                if ((available > 0L) && (available < Long.MAX_VALUE)) {
                    response.setDateHeader("Retry-After", available);
                    response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, sm.getString("standardWrapper.isUnavailable", wrapper.getName()));
                } else if (available == Long.MAX_VALUE) {
                    response.sendError(HttpServletResponse.SC_NOT_FOUND, sm.getString("standardWrapper.notFound", wrapper.getName()));
                }
                unavailable = true;
            }
     
            // 分配一个servlet实例用来处理请求
            // Allocate a servlet instance to process this request
            try {
                if (!unavailable) {
                    // 调用StandardWrapper#allocate()方法,获取到servlet实例
                servlet = wrapper.allocate();
                }
            } catch (UnavailableException e) {
                ...
            } catch (ServletException e) {
                ...
            } catch (Throwable e) {
                ...
            }
     
            ...
     
            // 为当前请求创建一个过滤器链
          // Create the filter chain for this request
            ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
     
            // 为当前请求调用过滤器链,注意:这也会调用servlet实例的service()方法
            // Call the filter chain for this request. NOTE: This also calls the servlet's service() method
            try {
                if ((servlet != null) && (filterChain != null)) {
                    // Swallow output if needed
                    if (context.getSwallowOutput()) {
                        try {
                            SystemLogHandler.startCapture();
                            if (request.isAsyncDispatching()) {
                                request.getAsyncContextInternal().doInternalDispatch();
                            } else {
                                filterChain.doFilter(request.getRequest(), response.getResponse());
                            }
                        } finally {
                            String log = SystemLogHandler.stopCapture();
                            if (log != null && log.length() > 0) {
                                context.getLogger().info(log);
                            }
                        }
                    } else {
                        if (request.isAsyncDispatching()) {
                            request.getAsyncContextInternal().doInternalDispatch();
                        } else {
                            filterChain.doFilter(request.getRequest(), response.getResponse());
                        }
                    }
     
                }
            } catch (ClientAbortException | CloseNowException e) {
                ...
            } catch (IOException e) {
                ...
            } catch (UnavailableException e) {
                ...
            } catch (ServletException e) {
                ...
            } catch (Throwable e) {
                ...
            } finally {
                // Release the filter chain (if any) for this request
                if (filterChain != null) {
                    filterChain.release();
                }
     
                // Deallocate the allocated servlet instance
                try {
                    if (servlet != null) {
                        wrapper.deallocate(servlet);
                    }
                } catch (Throwable e) {
                    ...
                }
     
                // If this servlet has been marked permanently unavailable,
                // unload it and release this instance
                try {
                    if ((servlet != null) &&
                        (wrapper.getAvailable() == Long.MAX_VALUE)) {
                        wrapper.unload();
                    }
                } catch (Throwable e) {
                    ...
                }
     
                long t2=System.currentTimeMillis();
                long time=t2-t1;
                processingTime += time;
                if( time > maxTime) maxTime=time;
                if( time < minTime) minTime=time;
            }
        }
     }

    复制代码

    上边代码主要包含 1)servlet = wrapper.allocate(); 调用StandardWrapper#allocate()方法,获取到servlet实例 2)ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);为当前请求创建一个过滤链,(非异步情况下)并调用filterChain.doFilter(request.getRequest(), response.getResponse()); 3)filter#doFilter()、servlet#service()的执行是在filterChain.doFilter(request.getRequest(), response.getResponse());代码内部执行的。

    ApplicationFilterChain#doFilter内部代码:

    复制代码

     public final class ApplicationFilterChain implements FilterChain {
        ...
     
        @Override
        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
            if( Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                try {
                    java.security.AccessController.doPrivileged(
                        new java.security.PrivilegedExceptionAction<Void>() {
                            @Override
                            public Void run()
                                throws ServletException, IOException {
                                internalDoFilter(req,res);
                                return null;
                            }
                        }
                    );
                } catch( PrivilegedActionException pe) {
                    ...
                }
            } else {
                internalDoFilter(request,response);
            }
        }
     
        private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
            // Call the next filter if there is one
            if (pos < n) {
                ApplicationFilterConfig filterConfig = filters[pos++];
                try {
                    Filter filter = filterConfig.getFilter();
     
                    if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                        request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                    }
                    if( Globals.IS_SECURITY_ENABLED ) {
                        final ServletRequest req = request;
                        final ServletResponse res = response;
                        Principal principal = ((HttpServletRequest) req).getUserPrincipal();
     
                        Object[] args = new Object[]{req, res, this};
                        SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                    } else {
                        filter.doFilter(request, response, this);
                    }
                } catch (IOException | ServletException | RuntimeException e) {
                    throw e;
                } catch (Throwable e) {
                    ...
                }
                return;
            }
     
            // We fell off the end of the chain -- call the servlet instance
            try {
                if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                    lastServicedRequest.set(request);
                    lastServicedResponse.set(response);
                }
     
                if (request.isAsyncSupported() && !servletSupportsAsync) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                // Use potentially wrapped request from this point
                if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal = ((HttpServletRequest) req).getUserPrincipal();
                    Object[] args = new Object[]{req, res};
                    SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);
                } else {
                    servlet.service(request, response);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                throw new ServletException(sm.getString("filterChain.servlet"), e);
            } finally {
                if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                    lastServicedRequest.set(null);
                    lastServicedResponse.set(null);
                }
            }
        }
     
        ...
     }

    复制代码

    1)上边代码会递归调用 ApplicationFilterChain#doFilter(...); 2)递归执行到最底层doFiler,之后会调用上边代码中 servlet.service() 或者 SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);,也即是执行 servlet 的 service() 方法。然后一次从 底层 到 顶层 返回递归调用代码出,结束调用。这也就是之职责链的模式的应用,具体请参考《设计模式(九)责任链(Chain of Responsibility)

    ServletRequestListener 触发:

    org.apache.catalina.core.StandardHostValve#invoke方法:

    复制代码

    final class StandardHostValve extends ValveBase {
        ...
    
        @Override
        public final void invoke(Request request, Response response)
            throws IOException, ServletException {
    
            // Select the Context to be used for this Request
            Context context = request.getContext();
            if (context == null) {
                return;
            }
    
            if (request.isAsyncSupported()) {
                request.setAsyncSupported(context.getPipeline().isAsyncSupported());
            }
    
            boolean asyncAtStart = request.isAsync();
    
            try {
                context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
    
                if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
                    // Don't fire listeners during async processing (the listener fired for the request that called startAsync()).
                    // If a request init listener throws an exception, the request is aborted.
                    return;
                }
    
                // Ask this Context to process this request. Requests that are already in error must have been routed here to check for
                // application defined error pages so DO NOT forward them to the the application for processing.
                try {
                    if (!response.isErrorReportRequired()) {
                        context.getPipeline().getFirst().invoke(request, response);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
                    // If a new error occurred while trying to report a previous error allow the original error to be reported.
                    if (!response.isErrorReportRequired()) {
                        request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                        throwable(request, response, t);
                    }
                }
    
                // Now that the request/response pair is back under container control lift the suspension so that the error handling can
                // complete and/or the container can flush any remaining data
                response.setSuspended(false);
    
                Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
    
                // Protect against NPEs if the context was destroyed during a long running request.
                if (!context.getState().isAvailable()) {
                    return;
                }
    
                // Look for (and render if found) an application level error page
                if (response.isErrorReportRequired()) {
                    // If an error has occurred that prevents further I/O, don't waste time producing an error report that will never be read
                    AtomicBoolean result = new AtomicBoolean(false);
                    response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
                    if (result.get()) {
                        if (t != null) {
                            throwable(request, response, t);
                        } else {
                            status(request, response);
                        }
                    }
                }
    
                if (!request.isAsync() && !asyncAtStart) {
                    context.fireRequestDestroyEvent(request.getRequest());
                }
            } finally {
                // Access a session (if present) to update last accessed time, based on a strict interpretation of the specification
                if (ACCESS_SESSION) {
                    request.getSession(false);
                }
    
                context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
            }
        }
    
        ...
    }

    复制代码

    说明: 1)context.fireRequestInitEvent(request.getRequest()):就是调用 StandardContext#fireRequestInitEvent(...) 触发 ServletRequestListener#requestInitialized(event)

    2)context.fireRequestDestroyEvent(request.getRequest()):就是调用 StandardContext#fireRequestDestroyEvent(...) 触发 ServletRequestListener#requestDestroyed(event)

     

    参考资料:

    Tomcat系列源码分析

    公众号发哥讲

    这是一个稍偏基础和偏技术的公众号,甚至其中包括一些可能阅读量很低的包含代码的技术文,不知道你是不是喜欢,期待你的关注。

    代码分享

    https://gitee.com/naimaohome

    微信公众号 点击关于我,加入QQ群,即可获取到代码以及高级进阶视频和电子书!!

    img

    如果你觉得文章还不错,就请点击右上角选择发送给朋友或者转发到朋友圈~

    ● 扫码关注我们

    据说看到好文章不推荐的人,服务器容易宕机!

    本文版权归 发哥讲博客园 共有,原创文章,未经允许不得转载,否则保留追究法律责任的权利。

     

     

  • 相关阅读:
    Hostker云主机
    Orz 终于有了自己的博客地址
    BZOJ 1635: [Usaco2007 Jan]Tallest Cow 最高的牛
    BZOJ 1636: [Usaco2007 Jan]Balanced Lineup
    BZOJ 2252: [2010Beijing wc]矩阵距离
    BZOJ 2253: [2010 Beijing wc]纸箱堆叠
    BZOJ 无数据题集合
    BZOJ 1087: [SCOI2005]互不侵犯King
    BZOJ 3236: [Ahoi2013]作业
    POJ2352:Stars
  • 原文地址:https://www.cnblogs.com/naimao/p/13446482.html
Copyright © 2020-2023  润新知