Spring Cloud Netflix Zuul源码分析之请求处理篇-上

栏目: 编程工具 · 发布时间: 7年前

内容简介:微信公众号:如有问题或建议,请在下方留言;最近更新:2019-01-03

微信公众号:如有问题或建议,请在下方留言;

最近更新:2019-01-03

微信公众号:I am CR7

如有问题或建议,请在下方留言

最近更新:2019-01-03

前言

经过前面两篇文章的铺垫,大戏正式上场。本文将对zuul是如何根据配置的路由信息,转发请求到后端微服务,进行详细分析。补充一点:本着由浅入深的原则,这里只对简单URL方式进行分析,serviceId方式后续会单独讲解。

请求如何进入ZuulServlet

上一篇 Spring Cloud Netflix Zuul源码分析之路由注册篇 简化版doFilter()源码里,我们讲解了过滤器链的执行,下面,笔者将从servlet.service(request, response);入手,先来讲一讲,请求是如何进入到ZuulServlet中。

时序图

Spring Cloud Netflix Zuul源码分析之请求处理篇-上
ZuulServlet时序图
高清大图请看 user-gold-cdn.xitu.io/2019/1/3/16…

简化版doDispatch源码

1protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
2    // 1、根据请求获取handlerMapping对应的HandlerExecutionChain
3    HandlerExecutionChain mappedHandler = getHandler(processedRequest);
4    // 2、根据请求对应的handler,获取对应的handlerAdapter
5    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
6    // 3、调用handlerAdapter的handle方法进行处理
7    ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
8    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
9}
复制代码

下面我们逐一来看每一行方法内部的源码。

1、getHandler()

 1protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
 2    if (this.handlerMappings != null) {
 3        // 之前初始化保存到内存中的handlerMapping,遍历查找请求对应的Handler
 4        for (HandlerMapping hm : this.handlerMappings) {
 5            if (logger.isTraceEnabled()) {
 6                logger.trace(
 7                        "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
 8            }
 9            HandlerExecutionChain handler = hm.getHandler(request);
10            if (handler != null) {
11                return handler;
12            }
13        }
14    }
15    return null;
16}
复制代码

日志:

12018-12-28 15:35:07.087 [http-nio-8558-exec-2] DEBUG o.s.c.n.zuul.web.ZuulHandlerMapping - Matching patterns for request [/role-api/info/7] are [/role-api/**]
22018-12-28 15:35:29.916 [http-nio-8558-exec-2] DEBUG o.s.c.n.zuul.web.ZuulHandlerMapping - URI Template variables for request [/role-api/info/7] are {}
32018-12-28 15:35:34.000 [http-nio-8558-exec-2] DEBUG o.s.c.n.zuul.web.ZuulHandlerMapping - Mapping [/role-api/info/7] to HandlerExecutionChain with handler [org.springframework.cloud.netflix.zuul.web.ZuulController@6d25d8f8] and 1 interceptor
复制代码

查询handlerMappings后,找到了ZuulController,封装到HandlerExecutionChain里。

2、getHandlerAdapter()

 1protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
 2    if (this.handlerAdapters != null) {
 3        // 之前初始化保存到内存中的handlerAdapters,遍历查找ZuulController对应的HandlerAdapter
 4        for (HandlerAdapter ha : this.handlerAdapters) {
 5            if (logger.isTraceEnabled()) {
 6                logger.trace("Testing handler adapter [" + ha + "]");
 7            }
 8            // 查看HandlerAdapter是否支持当前请求对应的Handler
 9            if (ha.supports(handler)) {
10                return ha;
11            }
12        }
13    }
14    throw new ServletException("No adapter for handler [" + handler +
15            "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
16}
17
18// SimpleControllerHandlerAdapter
19@Override
20public boolean supports(Object handler) {
21    // ZuulController刚好满足条件
22    return (handler instanceof Controller);
23}
复制代码

查询handlerAdapters后,找到了ZuulController支持的SimpleControllerHandlerAdapter。

3、handle()

 1// SimpleControllerHandlerAdapter
 2@Override
 3@Nullable
 4public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
 5        throws Exception {
 6
 7    return ((Controller) handler).handleRequest(request, response);
 8}
 9
10// ZuulController
11@Override
12public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
13    try {
14        // We don't care about the other features of the base class, just want to
15        // handle the request
16        return super.handleRequestInternal(request, response);
17    }
18    finally {
19        // @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
20        RequestContext.getCurrentContext().unset();
21    }
22}
23
24// ServletWrappingController
25@Override
26protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
27        throws Exception {
28
29    Assert.state(this.servletInstance != null, "No Servlet instance");
30    // 到这里,我们就找到了调用ZuulServlet.service()的地方
31    this.servletInstance.service(request, response);
32    return null;
33}
复制代码

小结

至此,请求如何进入到ZuulServlet,我们就分析完毕了。小结一下:

  • 根据请求获取对应的Handler[ZuulController]
  • 根据Handler获取对应的HandlerAdapter[SimpleControllerHandlerAdapter]
  • 调用HandlerAdapter的handle方法

下面,我们开始讲解本文最核心的部分:ZuulServlet的service方法。

ZuulServlet

时序图

Spring Cloud Netflix Zuul源码分析之请求处理篇-上
service时序图

源码

 1Override
 2public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
 3    try {
 4        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
 5
 6        // Marks this request as having passed through the "Zuul engine", as opposed to servlets
 7        // explicitly bound in web.xml, for which requests will not have the same data attached
 8        RequestContext context = RequestContext.getCurrentContext();
 9        context.setZuulEngineRan();
10
11        try {
12            preRoute();
13        } catch (ZuulException e) {
14            error(e);
15            postRoute();
16            return;
17        }
18        try {
19            route();
20        } catch (ZuulException e) {
21            error(e);
22            postRoute();
23            return;
24        }
25        try {
26            postRoute();
27        } catch (ZuulException e) {
28            error(e);
29            return;
30        }
31
32    } catch (Throwable e) {
33        error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
34    } finally {
35        RequestContext.getCurrentContext().unset();
36    }
37}
复制代码

阅读完上述源码,足以明白ZuulServlet的处理过程就是执行一系列过滤器。一共分为四种类型:前置、路由、后置、错误。下面按照正常请求,对各类型过滤器进行逐一讲解。为了不让大家困惑,这里先列出笔者的思路:

  • 先让大家知道,一次简单URL请求经历了哪些过滤器,日志输出了什么
  • 再回头去看日志的输出从何而来,深入具体的过滤器,进行源码分析

前置过滤器

Zuul默认提供了五个前置过滤器:

  • ServletDetectionFilter:执行顺序为-3,五个前置过滤器中最先执行。

该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法来判断请求处理的源头,以实现后续不同的处理机制。

  • Servlet30WrapperFilter:执行顺序为-2,第二个执行的前置过滤器。

该过滤器总是会被执行,主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象。

  • FormBodyWrapperFilter:执行顺序为-1,第三个执行的前置过滤器。

该过滤器的执行有条件要求:要么是Context-Type为application/x-www-form-urlencoded的请求,要么是Context-Type为multipart/form-data,且是由String的DispatcherServlet处理的请求。主要用来将请求包装成FormBodyRequestWrapper对象。

  • DebugFilter:执行顺序为1,第四个执行的前置过滤器。

该过滤器的执行有两个条件:要么配置里指定zuul.debug.request为true,要么请求参数debug为true。主要用来将当前请求上下文中的debugRouting和debugRequest参数设置为true。这样的好处是可以灵活的调整配置或者请求参数,来决定是否启用debug模式,从而在后续过滤器中利用debug打印一些日志,便于线上分析问题。

  • PreDecorationFilter:执行顺序为5,最后一个执行的前置过滤器。

该过滤器的执行要求请求上下文中不存在forward.do和serviceId参数,如果有一个存在的话,说明当前请求已经被处理过了(因为这二个信息就是根据当前请求的路由信息加载进来的)。主要用来对当前请求做预处理操作,如路由的匹配,将匹配到的路由信息存入请求上下文,便于后面的路由过滤器获取。还包括为HTTP头添加信息,如X-Forwarded-Host、X-Forwarded-Port等等。

断点图

Spring Cloud Netflix Zuul源码分析之请求处理篇-上
前置过滤器

日志

12018-12-29 14:35:13.785 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - Finding route for path: /role-api/info/7
22018-12-29 14:35:27.866 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - servletPath=/
32018-12-29 14:35:28.248 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - zuulServletPath=/zuul
42018-12-29 14:35:29.421 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - RequestUtils.isDispatcherServletRequest()=true
52018-12-29 14:35:30.492 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - RequestUtils.isZuulServletRequest()=false
62018-12-29 14:35:49.898 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - adjustedPath=/role-api/info/7
72018-12-29 14:36:09.935 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - Matching pattern:/user-api/**
82018-12-29 14:36:14.516 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - Matching pattern:/role-api/**
92018-12-29 14:36:23.042 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - route matched=ZuulRoute{id='role-api', path='/role-api/**', serviceId='null', url='http://localhost:9092/', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
复制代码

路由过滤器

Zuul默认提供了三个路由过滤器:

  • RibbonRoutingFilter:执行顺序为10,三个路由过滤器中最先执行。

该过滤器的执行只有一个条件,那就是请求上下文中必须存在serviceId参数,即只对通过serviceId配置路由规则的请求生效,简单URL请求不进行处理。

  • SimpleHostRoutingFilter:执行顺序为100,第二个执行的路由过滤器。

该过滤器的执行只有一个条件,那就是请求上下文中必须存在routeHost参数,即只对通过url配置路由规则的请求生效。该过滤器调用原生的httpclient包,对routeHost参数对应的物理地址发送请求,并没有使用ribbon和hystrix,该类请求是没有断路器和线程隔离的保护机制。

  • SendForwardFilter:执行顺序为500,最后一个执行的路由过滤器。

该过滤器的执行只有一个条件,那就是请求上下文中必须存在forward.do参数,即用来处理路由规则中的forward本地跳转。

断点图

Spring Cloud Netflix Zuul源码分析之请求处理篇-上
路由过滤器

日志

 12018-12-29 14:49:26.219 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.f.r.SimpleHostRoutingFilter - /info/7
 22018-12-29 14:49:31.611 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.f.r.SimpleHostRoutingFilter - localhost 9092 http
 32018-12-29 14:49:50.557 [http-nio-8558-exec-2] DEBUG o.a.h.c.protocol.RequestAuthCache - Auth cache not set in the context
 42018-12-29 14:49:50.584 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection request: [route: {}->http://localhost:9092][total kept alive: 0; route allocated: 0 of 1000; total allocated: 0 of 1000]
 52018-12-29 14:49:50.907 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection leased: [id: 0][route: {}->http://localhost:9092][total kept alive: 0; route allocated: 1 of 1000; total allocated: 1 of 1000]
 62018-12-29 14:49:50.941 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Opening connection {}->http://localhost:9092
 72018-12-29 14:49:50.993 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultHttpClientConnectionOperator - Connecting to localhost/127.0.0.1:9092
 82018-12-29 14:49:51.054 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultHttpClientConnectionOperator - Connection established 127.0.0.1:1172<->127.0.0.1:9092
 92018-12-29 14:49:51.055 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultManagedHttpClientConnection - http-outgoing-0: set socket timeout to 10000
102018-12-29 14:49:51.056 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Executing request GET /info/7 HTTP/1.1
112018-12-29 14:49:51.056 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Target auth state: UNCHALLENGED
122018-12-29 14:49:51.059 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Proxy auth state: UNCHALLENGED
132018-12-29 14:49:51.118 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> GET /info/7 HTTP/1.1
142018-12-29 14:49:51.119 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> cache-control: no-cache
152018-12-29 14:49:51.119 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> postman-token: 70dbc6db-0aff-467b-a2d6-453b3627e856
162018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> user-agent: PostmanRuntime/7.4.0
172018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> accept: */*
182018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> accept-encoding: gzip, deflate
192018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-host: localhost:8558
202018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-proto: http
212018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-prefix: /role-api
222018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-port: 8558
232018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-for: 0:0:0:0:0:0:0:1
242018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> Host: localhost:9092
252018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> Connection: Keep-Alive
262018-12-29 14:49:51.122 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "GET /info/7 HTTP/1.1[\r][\n]"
272018-12-29 14:49:51.122 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "cache-control: no-cache[\r][\n]"
282018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "postman-token: 70dbc6db-0aff-467b-a2d6-453b3627e856[\r][\n]"
292018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "user-agent: PostmanRuntime/7.4.0[\r][\n]"
302018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "accept: */*[\r][\n]"
312018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "accept-encoding: gzip, deflate[\r][\n]"
322018-12-29 14:49:51.124 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-host: localhost:8558[\r][\n]"
332018-12-29 14:49:51.125 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-proto: http[\r][\n]"
342018-12-29 14:49:51.126 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-prefix: /role-api[\r][\n]"
352018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-port: 8558[\r][\n]"
362018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-for: 0:0:0:0:0:0:0:1[\r][\n]"
372018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "Host: localhost:9092[\r][\n]"
382018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
392018-12-29 14:49:51.128 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
402018-12-29 14:49:54.932 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 << "HTTP/1.1 200 [\r][\n]"
412018-12-29 14:49:54.933 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Type: text/plain;charset=UTF-8[\r][\n]"
422018-12-29 14:49:54.933 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Length: 30[\r][\n]"
432018-12-29 14:49:54.933 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 << "Date: Sat, 29 Dec 2018 06:49:54 GMT[\r][\n]"
442018-12-29 14:49:54.933 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
452018-12-29 14:49:54.934 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 << "hello I am is service Role-API"
462018-12-29 14:49:54.953 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 << HTTP/1.1 200 
472018-12-29 14:49:54.954 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 << Content-Type: text/plain;charset=UTF-8
482018-12-29 14:49:54.954 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 << Content-Length: 30
492018-12-29 14:49:54.954 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 << Date: Sat, 29 Dec 2018 06:49:54 GMT
502018-12-29 14:49:55.091 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Connection can be kept alive indefinitely
51
复制代码

后置过滤器

Zuul默认提供了一个后置过滤器:

  • SendResponseFilter:执行顺序为1000,只有这一个后置过滤器。

该过滤器执行条件要求请求上下文中必须包含请求响应相关的头信息或者响应数据流或者响应体,只要有一个满足,就会执行。主要用来将请求上下文里的响应信息写入到响应内容,返回给请求客户端。

断点图

Spring Cloud Netflix Zuul源码分析之请求处理篇-上
后置过滤器

日志

12018-12-29 14:54:02.786 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection [id: 0][route: {}->http://localhost:9092] can be kept alive indefinitely
22018-12-29 14:54:02.786 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultManagedHttpClientConnection - http-outgoing-0: set socket timeout to 0
32018-12-29 14:54:02.787 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection released: [id: 0][route: {}->http://localhost:9092][total kept alive: 1; route allocated: 1 of 1000; total allocated: 1 of 1000]
42018-12-29 14:55:33.931 [http-nio-8558-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
52018-12-29 14:56:22.849 [http-nio-8558-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Successfully completed request
复制代码

小结

以上就是一次简单URL请求在ZuulServlet的处理过程,下面我们深入研究三个重要的类,分别是:前置过滤器PreDecorationFilter、路由过滤器SimpleHostRoutingFilter、后置过滤器SendResponseFilter。因篇幅原因,后面内容请看 Spring Cloud Netflix Zuul源码分析之请求处理篇-下

总结

到这里,我们就讲完了一个简单URL请求在Zuul中整个处理过程。写作过程中,笔者一直在思考,如何行文能让大家更好的理解。虽然修改了很多次,但是还是觉得不够完美,只能一边写一边总结一边改进。希望大家多多留言,给出意见和建议,那笔者真是感激不尽!!!最后,感谢大家的支持,祝新年快乐,祁琛,2019年1月3日。

Spring Cloud Netflix Zuul源码分析之请求处理篇-上

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Design systems

Design systems

Not all design systems are equally effective. Some can generate coherent user experiences, others produce confusing patchwork designs. Some inspire teams to contribute to them, others are neglected. S......一起来看看 《Design systems》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具