跳至主要內容

20. 对响应数据进行压缩原理

安图新大约 4 分钟

20. 对响应数据进行压缩原理

前言

为了节省带宽,在响应数据比较大的情况下,可以对响应数据进行压缩,返回给前端页面压缩数据。

一、配置

# 开启压缩
server.compression.enabled=true
server.compression.mime-types=application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain
server.compression.min-response-size=2KB

二、原理

1.参数的注入

(1)从 AbstractApplicationContext 的 onRefresh()方法开始

@Override
	protected void onRefresh() {


		super.onRefresh();
		try {


			//创建web容器服务
			createWebServer();
		}
		catch (Throwable ex) {


			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}

(2)createWebServer( )

private void createWebServer() {


		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {


			StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
			//这里创建ServletWebServerFactory,通过spring容器获取:getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class)
			ServletWebServerFactory factory = getWebServerFactory();
			createWebServer.tag("factory", factory.getClass().toString());
			//获取WebServer
			this.webServer = factory.getWebServer(getSelfInitializer());
			createWebServer.end();
			getBeanFactory().registerSingleton("webServerGracefulShutdown",
					new WebServerGracefulShutdownLifecycle(this.webServer));
			getBeanFactory().registerSingleton("webServerStartStop",
					new WebServerStartStopLifecycle(this, this.webServer));
		}
		else if (servletContext != null) {


			try {


				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {


				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}

(3)WebServerFactoryCustomizerBeanPostProcessor

bean 的后置处理器的 postProcessBeforeInitialization 方法会在 bean 初始化过程中被调用

@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {


		if (bean instanceof WebServerFactory) {


			postProcessBeforeInitialization((WebServerFactory) bean);
		}
		return bean;
	}

postProcessBeforeInitialization( )

private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {


		LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory)
				.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
				.invoke((customizer) -> customizer.customize(webServerFactory));
	}

获取 WebServerFactoryCustomizer

private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {


		if (this.customizers == null) {


			// Look up does not include the parent context
			this.customizers = new ArrayList<>(getWebServerFactoryCustomizerBeans());
			this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
			this.customizers = Collections.unmodifiableList(this.customizers);
		}
		return this.customizers;
	}

private Collection<WebServerFactoryCustomizer<?>> getWebServerFactoryCustomizerBeans() {


		//根据类型获取WebServerFactoryCustomizer
		return (Collection) this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false).values();
	}

调用 ServletWebServerFactoryCustomizer 的 customize( )方法

@Override
	public void customize(ConfigurableServletWebServerFactory factory) {


		PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
		map.from(this.serverProperties::getPort).to(factory::setPort);
		map.from(this.serverProperties::getAddress).to(factory::setAddress);
		map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath);
		map.from(this.serverProperties.getServlet()::getApplicationDisplayName).to(factory::setDisplayName);
		map.from(this.serverProperties.getServlet()::isRegisterDefaultServlet).to(factory::setRegisterDefaultServlet);
		map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
		map.from(this.serverProperties::getSsl).to(factory::setSsl);
		map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
		//从serverProperties中获取Compression,并设置到ServletWebServerFactory中
		map.from(this.serverProperties::getCompression).to(factory::setCompression);
		map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
		map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
		map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
		map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
		for (WebListenerRegistrar registrar : this.webListenerRegistrars) {


			registrar.register(factory);
		}
	}

(4)Compression 类中的几个参数

public class Compression {


	//开关
	private boolean enabled = false;

	private String[] mimeTypes = new String[] {

      "text/html", "text/xml", "text/plain", "text/css", "text/javascript",
			"application/javascript", "application/json", "application/xml" };

	private String[] excludedUserAgents = null;
	//默认最小2KB
	private DataSize minResponseSize = DataSize.ofKilobytes(2);

}

此时压缩参数被设置到了 ServletWebServerFactory 中

2.CompressionHttpHandlerFactory 的创建

(5)获取 WebServer

this.webServer = factory.getWebServer(getSelfInitializer());

getWebServer( )

@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {


		Builder builder = this.delegate.createBuilder(this);
		DeploymentManager manager = createManager(initializers);
		return getUndertowWebServer(builder, manager, getPort());
	}

getUndertowWebServer( )

protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) {


		//创建HttpHandlerFactory
		List<HttpHandlerFactory> httpHandlerFactories = this.delegate.createHttpHandlerFactories(this,
				new DeploymentManagerHttpHandlerFactory(manager));
		return new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0);
	}

(6)createHttpHandlerFactories( )

List<HttpHandlerFactory> createHttpHandlerFactories(AbstractConfigurableWebServerFactory webServerFactory,
			HttpHandlerFactory... initialHttpHandlerFactories) {


		//这里从webServerFactory获取Compression
		List<HttpHandlerFactory> factories = createHttpHandlerFactories(webServerFactory.getCompression(),
				this.useForwardHeaders, webServerFactory.getServerHeader(), webServerFactory.getShutdown(),
				initialHttpHandlerFactories);
		if (isAccessLogEnabled()) {


			factories.add(new AccessLogHttpHandlerFactory(this.accessLogDirectory, this.accessLogPattern,
					this.accessLogPrefix, this.accessLogSuffix, this.accessLogRotate));
		}
		return factories;
	}
static List<HttpHandlerFactory> createHttpHandlerFactories(Compression compression, boolean useForwardHeaders,
			String serverHeader, Shutdown shutdown, HttpHandlerFactory... initialHttpHandlerFactories) {


		List<HttpHandlerFactory> factories = new ArrayList<>(Arrays.asList(initialHttpHandlerFactories));
		//compression不为null,并且开启了压缩,创建CompressionHttpHandlerFactory
		if (compression != null && compression.getEnabled()) {


			factories.add(new CompressionHttpHandlerFactory(compression));
		}
		if (useForwardHeaders) {


			factories.add(Handlers::proxyPeerAddress);
		}
		if (StringUtils.hasText(serverHeader)) {


			factories.add((next) -> Handlers.header(next, "Server", serverHeader));
		}
		if (shutdown == Shutdown.GRACEFUL) {


			factories.add(Handlers::gracefulShutdown);
		}
		return factories;
	}

(7)创建完 HttpHandlerFactory,构造 UndertowServletWebServer

new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0)

public UndertowServletWebServer(Builder builder, Iterable<HttpHandlerFactory> httpHandlerFactories,
			String contextPath, boolean autoStart) {


		super(builder, httpHandlerFactories, autoStart);
		this.contextPath = contextPath;
		this.manager = findManager(httpHandlerFactories);
	}

3.压缩的预言

(8)UndertowWebServer.start( )

@Override
	public void start() throws WebServerException {


		synchronized (this.monitor) {


			if (this.started) {


				return;
			}
			try {


				if (!this.autoStart) {


					return;
				}
				if (this.undertow == null) {


					//创建Undertow
					this.undertow = createUndertowServer();
				}
				this.undertow.start();
				this.started = true;
				String message = getStartLogMessage();
				logger.info(message);
			}
			catch (Exception ex) {


				try {


					PortInUseException.ifPortBindingException(ex, (bindException) -> {


						List<Port> failedPorts = getConfiguredPorts();
						failedPorts.removeAll(getActualPorts());
						if (failedPorts.size() == 1) {


							throw new PortInUseException(failedPorts.get(0).getNumber());
						}
					});
					throw new WebServerException("Unable to start embedded Undertow", ex);
				}
				finally {


					stopSilently();
				}
			}
		}
	}

createUndertowServer( )

private Undertow createUndertowServer() {


		this.closeables = new ArrayList<>();
		this.gracefulShutdown = null;
		HttpHandler handler = createHttpHandler();
		this.builder.setHandler(handler);
		return this.builder.build();
	}

createHttpHandler( )

@Override
	protected HttpHandler createHttpHandler() {


		HttpHandler handler = super.createHttpHandler();
		if (StringUtils.hasLength(this.contextPath)) {


			handler = Handlers.path().addPrefixPath(this.contextPath, handler);
		}
		return handler;
	}

创建 HttpHandler 的执行链

protected HttpHandler createHttpHandler() {


		HttpHandler handler = null;
		for (HttpHandlerFactory factory : this.httpHandlerFactories) {


			//获取HttpHandler
			handler = factory.getHandler(handler);
			if (handler instanceof Closeable) {


				this.closeables.add((Closeable) handler);
			}
			if (handler instanceof GracefulShutdownHandler) {


				Assert.isNull(this.gracefulShutdown, "Only a single GracefulShutdownHandler can be defined");
				this.gracefulShutdown = (GracefulShutdownHandler) handler;
			}
		}
		return handler;
	}

getHandler( )

CompressionHttpHandlerFactory.java

@Override
	public HttpHandler getHandler(HttpHandler next) {


		if (!this.compression.getEnabled()) {


			return next;
		}
		ContentEncodingRepository repository = new ContentEncodingRepository();
		repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50,
				Predicates.and(getCompressionPredicates(this.compression)));
		return new EncodingHandler(repository).setNext(next);
	}

getCompressionPredicates( )获取压缩的预言条件

private static Predicate[] getCompressionPredicates(Compression compression) {


		List<Predicate> predicates = new ArrayList<>();
		//字节长度的预言
		predicates.add(new MaxSizePredicate((int) compression.getMinResponseSize().toBytes()));
		//MimeType的预言
		predicates.add(new CompressibleMimeTypePredicate(compression.getMimeTypes()));
		if (compression.getExcludedUserAgents() != null) {


			for (String agent : compression.getExcludedUserAgents()) {


				RequestHeaderAttribute agentHeader = new RequestHeaderAttribute(new HttpString(HttpHeaders.USER_AGENT));
				//UserAgent的预言
				predicates.add(Predicates.not(Predicates.regex(agentHeader, agent)));
			}
		}
		return predicates.toArray(new Predicate[0]);
	}

(9)MaxSizePredicate

private static class MaxSizePredicate implements Predicate {



		private final Predicate maxContentSize;

		MaxSizePredicate(int size) {


			this.maxContentSize = Predicates.requestLargerThan(size);
		}

		@Override
		public boolean resolve(HttpServerExchange value) {


			//响应头包含Content-Length
			if (value.getResponseHeaders().contains(Headers.CONTENT_LENGTH)) {


				return this.maxContentSize.resolve(value);
			}
			return true;
		}

	}

maxContentSize.resolve( )

 @Override
    public boolean resolve(final HttpServerExchange exchange) {


    	//获取Content-Length 长度
        final String length = exchange.getResponseHeaders().getFirst(Headers.CONTENT_LENGTH);
        if (length == null) {


            return false;
        }
        //校验是否大于给定的大小
        return Long.parseLong(length) > size;
    }

(10)CompressibleMimeTypePredicate

private static class CompressibleMimeTypePredicate implements Predicate {



		//保存配置的mimeTypes
		private final List<MimeType> mimeTypes;

		CompressibleMimeTypePredicate(String[] mimeTypes) {


			this.mimeTypes = new ArrayList<>(mimeTypes.length);
			for (String mimeTypeString : mimeTypes) {


				this.mimeTypes.add(MimeTypeUtils.parseMimeType(mimeTypeString));
			}
		}

		@Override
		public boolean resolve(HttpServerExchange value) {


			//获取Content-Type
			String contentType = value.getResponseHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
			if (contentType != null) {


				try {


					MimeType parsed = MimeTypeUtils.parseMimeType(contentType);
					for (MimeType mimeType : this.mimeTypes) {


						//校验响应头中的Content-Type是否能匹配上配置的mimeTypes
						if (mimeType.isCompatibleWith(parsed)) {


							return true;
						}
					}
				}
				catch (InvalidMimeTypeException ex) {


					return false;
				}
			}
			return false;
		}

	}

(11)RegularExpressionPredicate

@Override
    public boolean resolve(final HttpServerExchange value) {


    	//获取User-Agent
        String input = matchAttribute.readAttribute(value);
        if(input == null) {


            return false;
        }
        Matcher matcher = pattern.matcher(input);
        final boolean matches;
        //正则匹配
        if (requireFullMatch) {


            matches = matcher.matches();
        } else {


            matches = matcher.find();
        }
        if (traceEnabled) {


            UndertowLogger.PREDICATE_LOGGER.tracef("Regex pattern [%s] %s input [%s] for %s.", pattern.toString(), (matches ? "MATCHES" : "DOES NOT MATCH" ), input, value);
        }
        if (matches) {


            Map<String, Object> context = value.getAttachment(PREDICATE_CONTEXT);
            if(context == null) {


                value.putAttachment(PREDICATE_CONTEXT, context = new TreeMap<>());
            }
            int count = matcher.groupCount();
            for (int i = 0; i <= count; ++i) {


                if (traceEnabled) {


                    UndertowLogger.PREDICATE_LOGGER.tracef("Storing regex match group [%s] as [%s] for %s.", i, matcher.group(i), value);
                }
                context.put(Integer.toString(i), matcher.group(i));
            }
        }
        return matches;
    }

总结

使用压缩功能的条件:

1、 开启压缩 enabled=true;

2、 MaxSizePredicate,校验 Content-Length;

3、 CompressibleMimeTypePredicate,校验 Content-Type;

4、 RegularExpressionPredicate,校验 User-Agent;