使用httpclient的时候遇到NoHttpResponseException的处理

在使用httpclient的时候,偶尔能遇到 NoHttpResponseException 这个异常。

根据这个异常的一些信息,感觉起来很像是网络的因素导致,但是其实不是的,主要是因为以下原因导致:
1,nginx并发压力非常大的时候,排队都来不及处理了,nginx直接丢弃了,这个时候会报这个错误。
2,还有一种情况,网上没有进行任何报道,那是在 nginx reload 的时候,假如 httpclient 刚好在快速的进行请求,这个时候就会报这个异常。这是因为:nginx reload不会对现有的请求连接进行中断,但是对于keepalive的连接,假如刚好没有在进行数据传输,nginx会认为该连接已经请求完毕,所以就主动关闭了,而几乎在同时,httpclient还没有获得主动关闭的响应,于是认为该连接是可用的,用这个连接发送数据的时候,就会收到这个异常。

对于第二个情况,有一篇文章这么讲道:https://github.com/alibaba/tengine/issues/1074 ,但是对于nginx core不是随便人都能改的,同时这么改了后也不知是否有其他副作用,总归解决办法不是特别理想。

经过查找资料以及分析,大家都在说可以用重试的机制来解决,从上述2种原因来看,也确实重试是最好的解决办法,那么对于 httpclient 来讲,怎么实现重试呢?下面贴一段代码:

HttpClientBuilder.create().setRetryHandler((exception, executionCount, context) -> {
            if (executionCount > 1) {
                return false;
            }

            if(exception instanceof NoHttpResponseException) {
                log.info("NoHttpResponseException occured, will retry request");
                return true;
            }

            return false;
        }).build();

以上的代码对于重试,只是用了最简单重试,具体复杂的重试大家可以自行设计。

如何在springboot中设定keepalive timeout

查看springboot的官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server.server.tomcat.keep-alive-timeout

可以看到对server.tomcat.keep-alive-timeout这个参数的定义如下:
Time to wait for another HTTP request before the connection is closed. When not set the connectionTimeout is used. When set to -1 there will be no timeout.

可是在配置文件中按照上面所说,进行了定义:(注意,单位是毫秒,spring的文档历来是垃圾中的战斗机)

server:
  tomcat:
    keep-alive-timeout: 30000

运行后,发现不起任何作用。

于是只能按照上面所说的,对connection-timeout进行设置:

server:
  tomcat:
    connection-timeout: 30000

这样就起效了,说明keep-alive-timeout没有用,文档有误。

上述是通过配置来更改,还可以通过代码来设定keepalive_timeout,如下:

@Configuration
public class Config {

    @Bean   
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat
        .addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                ((AbstractProtocol) connector.getProtocolHandler())
                        .setKeepAliveTimeout(30000);
            }
        });
        return tomcat;
    }

}

zookeeper和curator的版本问题

有一个工程,用到了curator 4.0.1版本,在本地进行调试开发,为了方便,启动了本地的一个低版本的zookeeper,结果在启动的时候报错 zookeeper 连接不上,当时就怀疑了 zookeeper 的版本过低导致。

于是到 apache 官网下载最新版本的 zookeeper,下载了:https://mirrors.bfsu.edu.cn/apache/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz ,然后进行解压,居然说解压失败,有文件重复,以为自己下载的不正确,反复用不同的工具下载了好几遍都是同样的错误。为了解决这个问题,我在linux环境中下载这个包,进行解压,没有报错了,然后再压缩,再下载到本地后解压,没有错了。

启动 zookeeper 之前,需要将 zookeeper 解压后的 conf/zoo_sample.cfg 改名为 zoo.cfg,然后 bin/zkServer.cmd start 启动即可。

然后运行 curator 4.0.1 ,这样就顺利连接上了。

网上的人这么说的:
Curator 存在版本兼容问题。
Curator 2.x.x-兼容两个zk 3.4.x 和zk 3.5.x,
Curator 3.x.x-兼容兼容zk 3.5。

AES加密算法简述

AES是对称性加密算法中最为流行的算法之一。

对于AES加密算法的流程,可以参考这篇博文:AES加密模式与填充
AES有5种模式,其中ECB模式是不安全的,已经废弃;CBC是APPLE默认的加密模式,所以我们也选用CBC来讲解。

我们用java的代码来举例AES算法的一些流程和细节问题:

public static String aesEncrypt(String plainText, String secret,String iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes("utf-8"),"AES");

            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes("utf-8"));

            cipher.init(Cipher.ENCRYPT_MODE,keySpec,ivParameterSpec);

            byte[] byteContent = plainText.getBytes("utf-8");
            byte[] result = cipher.doFinal(byteContent);
            String encryptText = Base64.getEncoder().encodeToString(result);
            return encryptText;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

结合上篇博文中的CBC模式图,可以看到:
1,初始化向量(iv)就好比随机数中的种子。
2,PKCS5Padding,因为AES的数据库用128位去切分,当最后的部分不足128位的时候,那么就需要进行填充来达到128位。

其中,AES对secret(密钥)和iv的长度有规定:
1,secret有128位,192位,256位。在java中,128位的AES是jdk自带的,但是大于128位的AES,则需要引入一个额外的security包。从线上运维来看,这可能会增加一定的复杂度,所以,我从个人角度上来看,128位足够,减少运维的复杂度。加密算法的安全在乎与密码是否安全保护好,而不在于是128位还是256位。
2,iv是128位的。
3,我们在输secret的时候,往往不会在乎多少长,比如任意长度的secret,而不会刚好128位,对于iv是同样的道理。那么我们可以采用下面的方式方法来简化这个流程:

String md5 = md5Hex(secret);
secret = md5.substring(0,15);
String iv = md5.substring(16);

maven发生501, ReasonPhrase:HTTPS Required错误

最近用maven打包的时候,发生了如下的错误:
501, ReasonPhrase:HTTPS Required

这是因为在访问maven central库的时候,已经不再支持http方式的连接,必须要用https了。

在 maven 的 settings.xml 中设定如下内容即可解决:

<mirror>
      <id>central</id>
      <mirrorOf>central</mirrorOf>
      <name>central</name>
      <url>https://repo.maven.apache.org/maven2/</url>
</mirror>

pom在eclipse中出现Unknown line 1的错误

最近用eclipse开发基于springboot2的项目,导入springboot2的工程后,pom中第一行出现了Unknown的错误,把它从error中删除后,不影响其他正常使用,但是过一会又会出现这个错误,非常讨厌。

经过查找,是 m2e plugin的bug,有2个解决办法:
1,升级 m2e plugin,比较麻烦,我不选择这个。
2,在pom.xml中加入如下的配置:
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>

然后再 maven -> update projects… 一下,OK,一切搞定。

springboot的contextpath设置

一般情况下,我们构建的springboot工程不会使用contextpath的,使用contextpath的这种模式已经很老很老,而且有很多弊端。

但是,假如确实有需要用contextpath的话,springboot也提供了这种模式。

server.context-path=yourcontextpathname #这个是springboot 2.0 以前的写法

server.servlet.context-path=yourcontextpathname #这个是springboot 2.0 以后的写法

假如使用了 dubbox,并暴露了 rest,那么在启动的时候,别忘记了也要修改 dubbox 定义的 contextpath 路径,否则会误以为上述的修改没有作用导致启动报错,我就是犯过这样的错误,导致花了不少时间查找资料。

Auto-configuration的规范

尽管springboot的官方文档很糟糕,但是关于Auto-configuration还是写得比较好的,位于springboot reference中的Creating Your Own Auto-configuration。

至于为什么要用 Auto-configuration,可以见我写得这一篇文章:@SpringBootApplication中几点解惑

springboot官方提供的一个样例,非常不错,看了以后,auto-configuration就学会了:Master Spring Boot auto-configuration

auto-configuration的代码分层架构

官方推荐分成2个工程,一个starter,另一个autoconfigure。
但是我个人认为就那么一个starter就好了,何必分成2个工程,感觉多此一举的,因为starter纯粹就是一个空工程。
不过,话反过来说,学会了2个分开的工程,合成一个工程也是超级简单的,所以无所谓一个工程还是2个工程。

命名常规

官方的auto-configuration,比如yyy来举例,那么应该是 spring-boot-starter-yyy.jar,spring-boot-autoconfigure-yyy.jar
非官方的auto-configuration,比如xxx来举例,那么应该是 xxx-spring-boot-starter.jar,xxx-spring-boot-autoconfigure.jar
上面已经描述很清晰了,不再解释了。

spring-factories

对于auto-configuration工程来说,已经取消了Component auto scan的功能,代而取之的是采用spring-factories的方式,这个spring-factories文件位于autoconfigure这个工程中。

spring-provides

这个文件在springboot的官方文档中是没有提到的,那这个spring-provides文件用来干什么的?我查了有个人这么解释的:

It's for tooling. STS (and other IDEs if they chose to) can index those files and make autocomplete suggestions based on stuff that isn't yet on the classpath.

哦,原来是给STS,或者IDEA用的,这个文件放置在starter这个工程中,那就不用去管这个文件了。

好了,springboot的 auto-configuration就这么简单。

如何在tomcat中禁用JSESSIONID

首先这是一个“伪话题”,原因有下面2点:
1. 现在的应用部署越来越多采用分布式集群部署,从而对于session的使用越来越少。采用session来存储数据的方式逐渐都转移到了缓存中实现。
2. 前后端分离的应用开发模式越来越多,而 jsp 的使用也是越来越少。

但是,我为什么还是要把这个话题拿出来在这里再叙述一遍呢?原因是看到外面大量关于这个话题的文章,基本上都是错的,为了防止读者受到错误影响,我写下了这篇文章,希望对你有所帮助。

JSESSIONID的产生机制

tomcat只有在程序中使用了 xxx.getSession() 的时候才会创建session(JSESSIONID是它的标识),其他任何时候都不会主动去创建。

这个时候有人问了,我的程序里明确没有调用 xxx.getSession(),为什么还会创建session?原因是用到了jsp作为渲染机制,你去看一下在tomcat的工作目录中所对应的jsp编译好的.class或者也有.java文件,能够看到:session = pageContext.getSession();这样一段代码。这个session也就是jsp几个内置对象中含有session对象的来源。所以,只要你用到了 jsp,那么就会默认产生session。

那不用jsp,而用 servlet 的话,会不会主动创建 session 呢?不会的,因为对于Controller或者Action来说,本质上还是servlet。不会的,除非你自己写 xxx.getSession()。

那我也不用jsp,用其他的模板引擎,比如 freemarker 或者 Thymeleaf,会不会主动创建 session 呢?不会的,除非这个模板引擎中也主动调用xxx.getSession,否则不会。

那我既不用jsp,也不用servlet,用的是 spring的Controller或者 struts 的Action,那会不会主动创建 session 呢?不会的,因为对于Controller或者Action来说,本质上还是servlet。

禁用tomcat的session

通过上面的机制分析,很明显禁用tomcat的session的最好途径就是告诉jsp,不要主动创建session,那么方式是在每个jsp页面中加入:
<%@ page session="false" %>
加好以后,创新一下该jsp页面,会发现编译好的.class或者.java中 session = pageContext.getSession(); 这句话已经消失不见了。
加好以后,在浏览器中清除cookie后,再看一下 JSESSIONID 已经不再出现。
那在每个页面中加入 <%@ page session="false" %>,太麻烦了,一般在jsp工程中,大家都会在开始就用 <%@ include file="xxx.jsp"%> 这样来引入一些每个页面中都需要的元素,那么就把 <%@ page session="false" %> 加入到 xxx.jsp 中。

另一种做法:禁用tomcat的session

上一种方法是非常不错的,但是会出现一些纰漏的地方:
1. 会不会有个别jsp页面没有加入 <%@ page session="false" %>
2. 在servlet中,会不会某个程序员随心写了句 session = request.getSession(),但是 session 却没有去用
为了堵住这些纰漏的地方,还有一种采用连根拔起的方式方法。

import java.io.IOException;

import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Session;
import org.apache.catalina.session.ManagerBase;

public class SessionManager extends ManagerBase implements Lifecycle {

    private Session globalSession;

    @Override
    protected synchronized void startInternal() throws LifecycleException {
        super.startInternal();
        setState(LifecycleState.STARTING);
    }

    @Override
    protected synchronized void stopInternal() throws LifecycleException {
        setState(LifecycleState.STOPPING);
    }

    @Override
    public void load() throws ClassNotFoundException, IOException {
    }

    @Override
    public void unload() throws IOException {
    }

    @Override
    public Session createSession(String sessionId) {
        if(globalSession == null) {
            globalSession = super.createSession(sessionId);
        } else {
            globalSession.setValid(true);
        }

        return globalSession;
    }

    @Override
    public Session createEmptySession() {
        if(globalSession == null) {
            globalSession = super.createSession(sessionId);
        } else {
            globalSession.setValid(true);
        }

        return globalSession;
    }

}

SessionManager的原理其实是用同一个 globalSession 来代替每次创建新的 session,不创建这个 session 的话,那么会在访问 jsp 页面的时候报错:Page needs a session and none is available。

还需要注意这个globalSession需要进行激活,否则在tomcat的session失效后,会将此session标注为失效,那么尽管globalSession还是存在的,但是是失效的状态,还是会报错说session不存在。所以globalSession.setValid(true);这句很重要。

然后,在tomcat的server.xml中进行设置,大致如下:

<Context docBase="D:\test" path="/test" reloadable="false" sessionCookieName="yoursessionname">
    <Manager className="xxx.xxx.SessionManager" />
</Context>

上面的sessionCookieName默认是JSESSIONID,其实这个是有点点缺陷的,能够让别人知道你的应用是用java开发的,所以,换个其他的名字会更好一些。

springboot中使用上面例子的SessionManager

上面这个方法是用传统方式开发应用,那么在springboot下,因为内嵌了tomcat,使得更加简便

@Configuration
public class TomcatConfig {

    @Bean
    public EmbeddedServletContainerFactory servletContainer(){
        TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
        factory.addContextCustomizers(new TomcatContextCustomizer() {
        @Override
        public void customize(Context context) {
            SessionManager sessionManager = new SessionManager();
            context.setManager(sessionManager);
            context.setSessionCookieName("yoursessionname");
        }
    });
        return factory;
    }
}

这种方式,最好做成 Auto-configuration 的jar,在pom.xml中引入,更加方便,具体可以参考一下这篇文章:@SpringBootApplication中几点解惑

综述

  1. 最好在jsp页面中加入 <%@ page session="false" %>
  2. 用上面的SessionManager方式
    最后说一下,现代的java应用开发,几乎不考虑 session 了,所以这篇文章对您可能没有多大用处,对一些传统应用还是有些帮助的。