项目中引用的是servlet3.1的jar包,但是沿用旧的servlet2.5的规则编写代码。正好利用这个机会,尝试下servlet新版本带来的新特性
一、配置优化
1.简化配置文件servlet的新建
原servlet2.5在新建servlet时,需要在web.xml中创建对应对象,才可以访问
shortly
com.kaistart.gateway.servlet.KaistartGatewayServlet
1
shortly
/api
注解方式可以调整如下:
@WebServlet(urlPatterns = "/api",loadOnStartup = 1)
public class KaistartGatewayServlet extends HttpServlet {
}
其中loadOnStarup的作用如下:
1.load-on-startup元素标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法)。
2.它的值必须是一个整数,表示servlet应该被载入的顺序。;
3.当值为0或者大于0时,表示容器在启动时就加载并初始化这个servlet。
4.当值小于0或者没有指定时,则表示容器在该Servlet被请求时,才会去加载。
5.正数的值越小,该Servlet的优先级就越高,应用启动时就优先加载。
6.当值相同的时候,容器就会自己选择优先加载。
2.过滤器、监听器的创建简化
原
contextConfigLocation
classpath*:/spring/spring.xml
log4jConfigLocation
classpath:log4j.xml
org.springframework.web.context.ContextLoaderListener
SpringCharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
SpringCharacterEncodingFilter
/*
新:
package com.kaistart.gateway.init;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
*
* 升级servlet 简化web.xml配置
* @author chenhailong
* @date 2019年5月29日 下午6:46:27
*/
public class Init implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//配置文件
servletContext.setInitParameter("contextConfigLocation","classpath*:/spring/spring.xml");
//日志文件
servletContext.setInitParameter("log4jConfigLocation", "classpath:log4j.xml");
//编码过滤器
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
Dynamic filterRegistration = servletContext.addFilter("SpringCharacterEncodingFilter",new CharacterEncodingFilter());
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
//监听器
servletContext.addListener(new ContextLoaderListener());
}
}
3.welcome-file-list \ session-config没有找到配置
4. 开始使用servlet3.0开始的异步特性
a.获取AsyncContext对象应该使用request.startAsync();
AsyncContext async = request.startAsync();
如果使用AsyncContext async = request.getAsyncContext();,会报错
java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
org.apache.catalina.connector.Request.getAsyncContext(Request.java:1695)
org.apache.catalina.connector.RequestFacade.getAsyncContext(RequestFacade.java:1055)
com.kaistart.gateway.servlet.KaistartGatewayCallbackServlet.service(KaistartGatewayCallbackServlet.java:70)
javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
b.需要设置request请求支持异步处理
@WebServlet(urlPatterns = "/callback/*", loadOnStartup = 1,asyncSupported = true)
否则报错
java.lang.IllegalStateException: A filter or servlet of the current chain does not support asynchronous operations.
org.apache.catalina.connector.Request.startAsync(Request.java:1630)
org.apache.catalina.connector.Request.startAsync(Request.java:1623)
org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:1030)
com.kaistart.gateway.servlet.KaistartGatewayCallbackServlet.service(KaistartGatewayCallbackServlet.java:70)
javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
c. 当采用以下这种写法,没有使用线程池时如下(压测时出现):
final PrintWriter writer = response.getWriter();
writer.println("异步之前输出的内容。");
writer.flush();
//开始异步调用,获取对应的AsyncContext。
final AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(1*5000L);
asyncContext.start(new Runnable() {
@Override
public void run() {
writer.println("111.");
writer.flush();
asyncContext.complete();
}
});
tomcat容器会容易出现以下类似问题,
https://github.com/spring-projects/spring-boot/issues/15057
报错一:
java.lang.IllegalStateException: Calling [asyncError()] is not valid for a request with Async state [COMPLETE_PENDING]
at org.apache.coyote.AsyncStateMachine.asyncError(AsyncStateMachine.java:412)
at org.apache.coyote.http11.AbstractHttp11Processor.action(AbstractHttp11Processor.java:847)
at org.apache.coyote.Request.action(Request.java:378)
at org.apache.catalina.core.AsyncContextImpl.setErrorState(AsyncContextImpl.java:412)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:543)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1132)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:684)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1539)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1495)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
报错二:
java.lang.IllegalStateException: Calling [asyncComplete()] is not valid for a request with Async state [MUST_ERROR]
at org.apache.coyote.AsyncStateMachine.doComplete(AsyncStateMachine.java:304)
at org.apache.coyote.AsyncStateMachine.asyncComplete(AsyncStateMachine.java:289)
at org.apache.coyote.http11.AbstractHttp11Processor.action(AbstractHttp11Processor.java:876)
at org.apache.coyote.Request.action(Request.java:378)
at org.apache.catalina.core.AsyncContextImpl.complete(AsyncContextImpl.java:96)
at com.kaistart.gateway.servlet.KaistartGatewayCallbackServlet.lambda$0(KaistartGatewayCallbackServlet.java:93)
at org.apache.catalina.core.AsyncContextImpl$RunnableWrapper.run(AsyncContextImpl.java:559)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
采用线程池之后,异常没有出现。应该和asyncContext.start()方法有关
二、附上异步完整代码 + 压测结果
运行相关环境:mac、tomcat8.0、jdk1.8.0_171、eclipse、jmeter、servlet3.1jar包
1、web.xml配置 替换为java类初始化
package com.kaistart.gateway.init;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
* 升级servlet 代替web.xml文件中的配置
* @author chenhailong
* @date 2019年5月29日 下午6:46:27
*/
public class Init implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//配置文件
servletContext.setInitParameter("contextConfigLocation","classpath*:/spring/spring.xml");
//日志文件
servletContext.setInitParameter("log4jConfigLocation", "classpath:log4j.xml");
//编码过滤器
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
Dynamic filterRegistration = servletContext.addFilter("SpringCharacterEncodingFilter",new CharacterEncodingFilter());
filterRegistration.setAsyncSupported(true);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
//监听器
servletContext.addListener(new ContextLoaderListener());
}
}
2、容器启动后,创建线程池相关
package com.kaistart.gateway.init;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
*
* 容器启动后创建线程池
* @author chenhailong
* @date 2019年5月30日 下午6:44:01
*/
@WebListener
public class ContextListener implements ServletContextListener {
private int coreThread = 50; //核心线程数
private int maxThread = 100; //最大线程数
private long liveTime = 30000L;//超过核心线程数的超时时间设置
private int queueSize = 500; //队列长度
@Override
public void contextInitialized(ServletContextEvent sce) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(coreThread, maxThread, liveTime, TimeUnit.SECONDS,
new LinkedBlockingQueue(queueSize), new ThreadPoolExecutor.DiscardPolicy());
sce.getServletContext().setAttribute("executor", executor);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
ThreadPoolExecutor executor = (ThreadPoolExecutor)sce.getServletContext().getAttribute("executor");
executor.shutdown();
}
}
3、监听异步执行,这个可以不做
package com.kaistart.gateway.init;
import java.io.IOException;
import javax.servlet.AsyncEvent;
import javax.servlet.annotation.WebListener;
/**
* 用来监听异步操作不同事件的后续处理,这里在暂不处理
* @author chenhailong
* @date 2019年5月30日 下午7:18:22
*/
@WebListener
public class AsyncsListener implements javax.servlet.AsyncListener {
@Override
public void onComplete(AsyncEvent event) throws IOException {
//TODO when complete
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
//TODO when timeout
}
@Override
public void onError(AsyncEvent event) throws IOException {
//TODO when error
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
//TODO startasync
}
}
4.创建普通servlet请求
package com.kaistart.gateway.servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.kaistart.gateway.common.exception.GatewayException;
/**
*
*
* @author chenhailong
* @date 2019年1月21日 上午11:30:50
*/
@WebServlet(urlPatterns = "/testapi",loadOnStartup = 1)
public class KaistartGatewayTest extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(KaistartGatewayTest.class);
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) {
try {
//这里可以写具体的业务实现
Thread.sleep(1000);
} catch (GatewayException e) {
} catch (Throwable e) {
logger.error("eid="+e.hashCode()+" "+e.getMessage(), e);
}
}
}
5.创建异步servlet请求
package com.kaistart.gateway.servlet;
import java.util.concurrent.ThreadPoolExecutor;
import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.kaistart.gateway.init.AsyncsListener;
/**
*
* 注:使用servlet异步操作,如不使用线程池,直接使用 async.start(new Runnable())会报错:https://github.com/spring-projects/spring-boot/issues/15057
*
* @author wuyuan.lfk
* @date 2019年1月21日 上午11:30:50
*/
@WebServlet(urlPatterns = "/api",loadOnStartup = 1,asyncSupported = true)
public class KaistartGatewayServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) {
if(request.isAsyncSupported()) {
AsyncContext async = request.startAsync();
async.setTimeout(30*1000L);
async.addListener(new AsyncsListener());
//获取线程池
ThreadPoolExecutor executor = (ThreadPoolExecutor)request.getServletContext().getAttribute("executor");
executor.execute(new AsyncRequestProcessor(
async)
);
System.out.println("doService() -> 完成");
}
}
public class AsyncRequestProcessor implements Runnable{
private AsyncContext context;
public AsyncRequestProcessor(AsyncContext context) {
this.context = context;
}
@Override
public void run() {
context.start(() -> {
try {
//这里可以写具体的业务实现
Thread.sleep(1000);
System.out.println("doRun() -> 完成");
} catch (Exception e) {
e.printStackTrace();
}
context.complete();
});
}
}
}
6. 利用jmeter创建线程组,配置好信息后测试如下:
从代码上调整
1. 默认方式 async.start( new thread() )
2. 队列方式 blockingqueue
3. 线程池方式 executors
从压测变量上测试
1.线程数
2.持续时间
3.并发数
数据均没有特别大的差异,可能还有部分没弄明白。
并参考其他压测数据:https://blog.csdn.net/iteye_21091/article/details/82617598。
可能要在特定的场景下发挥异步的性能提升。需要去发现下
后来发现,可能是测试方式及测试数据的理解不正确。重新理解, throughput是表示每秒的吞吐量,吞吐量 = 总线程数/总运行时间。这样的话,单纯的以吞吐量来对比同步、异步之间的性能提升十分不准确。因为运行时间内的前期、中期、后期的吞吐量值是有很大差别的。如下图:
同步、异步的差别主要在B环节,业务逻辑的执行放到了新的线程中执行。这样的话异步的请求线程直接是走A -C逻辑后释放,可以处理其他请求,但是当前请求完成结束需要等待B的 asyncContext.complete()通知。 B环节的业务逻辑执行时间长短,直接拉开了同、异步的请求吞吐量。可以通过在代码中打印当前线程信息、请求次数等信息观察,需要结合tomcat工作线程池理解。这是我目前的理解了。
其他:servlet4.0 添加http2的特性增加了pushbuilder对象,这里就不举例了,参考如下:
https://www.jianshu.com/p/2d768df76501