深入理解 System.currentTimeMillis() 与时区

今天在解答同事对于 Unix Timestamp 的时区问题的疑问时,看到了这篇文章:

深入理解System.currentTimeMillis()

https://coderdeepwater.cn/2020/12/29/java_source_code/currentTimeMillis/

通过阅读发现,虽然该文章对于这个函数调用的原理解释的非常好,但是却在开头处犯了一个致命的错误,误导了我的同事。在这篇文章中,作者说:

深入理解System.currentTimeMillis()

System.currentTimeMills()的返回值取决于Java运行时系统的本地时区!千万不要忘记这一点!
同一时刻,在英国和中国的两个人同时用System.currentTimeMills()获取当前系统时间,其返回值不是一样的,除非手动将操作系统的时区设置成同一个时区(英国使用UTC+0,而中国使用UTC+8,中国比英国快8个小时).

这个观点是完全错误的,正因为这里的错误误导了我的同事,走入了错误的排查方向。

首先说结论:

  1. System.currentTimeMillis() 的返回值与系统时区无关。在一台时区和时间皆设置正确的机器上,这个函数的返回值总是当前时间与 UTC 0 时的 1970年1月1日 0时0分0秒即 1970-01-01 00:00:00 UTC 的差值
  2. 同一时刻,在地球上任意地点的两个人在时区和时间皆设置正确的机器上同时用 System.currentTimeMills() 获取当前系统时间,其返回值一定一样,与时区无关

首先明确几个概念:

  1. 时间:是一个客观存在的物理量,是物质运动变化的表现
  2. 时间单位:人为了方便对时间进行计量,人为定义了年、月、日、时、分、秒、毫秒等一系列时间单位
  3. 公历日期:人为规定的纪年体系,用来记录准确的时间
  4. 时区:由于地球自转的存在,地球上各个地区的日升日落时间并不一致。因此为了使用方便,人为将地球划分成了 24 个时区,每两个相邻时区间的时间表示相差 1 小时
  5. Unix 时间戳:人为规定的用来描述一个确切时间点的系统。以 1970-01-01 00:00:00 UTC 为基点,计算当前时间与基点的秒数差,不考虑闰秒

从这几个概念中,我们可以知道,Unix 时间戳是用于精确描述一个确定的时间点,具体的方式是计算两个时间点之间的秒数差。由于我们的宇宙只有单时间线,所以计算两个确定的时间点之间的秒数差,在地球上任何地方的时间都是一致的。因此我们可以知道,在一个确定的时间点上,地球上所有人计算得到的当前的 Unix 时间戳,都应当是一致的。

那么,为什么还有人遇到不同时区获取到的时间戳不一致的问题呢?这就要说到实际使用中对时间的展示方式了。

一个简单的例子,如果我说现在是北京时间 2020年01月01日 08时00分00秒,那么这可以表示一个准确的时刻。对于同一个时刻,我还可以说,现在是格林威治时间 2020年01月01日 00时00分00秒。这两个句子实际上表达的是同一个时刻。

但是,如果我只说现在是 2020年01月01日 00时00分00秒,那么不能表示一个准确的时刻。因为在句话里,我没有说明时区信息,导致缺失了定位到准确时刻的必要条件。

因此我们可以发现,我们日常说话时的表述,其实都是不精确的。那么在程序员的世界里,我们最容易犯的错误,也跟这个类似。

当我们查看一个服务器的时间信息的时候,大多数人不会关心时区信息。假设现在有一台 UTC 0 时区的服务器,很多人拿到后,在检查服务器时间时,第一反应是这个服务器的时间不对,因为桌面上的时钟是 2020年01月01日 08时00分00秒,而机器告诉我的是 2020年1月1日 00时00分00秒,晚了 8 小时。于是,使用 date -s "2020/01/01 08:00:00" 一把梭,再看机器的时间,完美!

那么这时候,如果我用 System.currentTimeMills()来获取时间戳,得到的是什么呢?得到的当然是 1577865600000,代表北京时间 2020年01月01日 16时00分00秒。因为机器本身运行在 0 时区,对于机器来说,现在是 0 时区的 8 点,那换算到 +8 的北京时区,自然是 16 点。

这也就是前面为何一直强调时区和时间皆设置正确的原因。刚说的案例就是典型的时间与时区设置不匹配。机器的时区是 0 时区的,但是用户设置进去的时间却是 +8 时区的。在这种情况下,System.currentTimeMills()自然获取不到正确的值。

因此,我们在修改机器的系统时间时,一定要带上时区信息。如:date -s "2020/01/01 08:00:00 +8" 。这样就可以准确的告诉系统我们想要设置的时刻,避免因为自动的时区转换导致调错机器的时间。

jOOQ 与 Spring 的一些注意事项

jOOQ 全称 Java Object Oriented Querying,即面向 Java 对象查询。它是 Data Geekery 公司研发的 DA 方案 (Data Access Layer),是一个 ORM 框架。

使用 jOOQ,既不像 Hibernate 等框架封装过高,无法触及 SQL 底层;也不像 MyBatis 等,配置太过繁琐。同时还是 Type Safe 的框架,编译时即可最大程度的发现问题。

不过在 jOOQ 配合 String Cloud 使用的时候,还是踩了几个小坑,特别说明一下。随时补充新遇到的问题。

一、事物问题

jOOQ 默认有一套自己的流式 API,来支持事物。不过,在 Spring 里面,我们使用的最多的还是 @EnableTransactionManagement@Transactional 注解。使用这二者可以开启 Spring 内置的基于注解和 Proxy 的事物处理机制,相对更灵活,更优雅。使用起来也更简单。

但是在跟 jOOQ 联动的时候,实际使用却发现事物始终不生效。但是奇怪的是,不论是打断点调试还是加日志,都能发现异常正常抛出了,也被 Spring 正常捕获了,Transaction 的 Rollback 也调用了,但是实际上事物就是没有撤销。

在多次排查 Spring 本身的配置问题后,突然想到问题可能处在 jOOQ 上。经过查找相关文档发现,由于我们的 SQL 都是通过 jOOQ 的 DSLContent 构建并执行的,所以默认情况下并不会受 Spring Transaction Manager 的管理。这里我们需要在配置 jOOQ 的时候,特别配置一下,才能让 @Transactional 注解生效。参考官网的样例,XML 配置如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">

    <!-- This is needed if you want to use the @Transactional annotation -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" >
        <!-- These properties are replaced by Maven "resources" -->
       <property name="url" value="${db.url}" />
       <property name="driverClassName" value="${db.driver}" />
       <property name="username" value="${db.username}" />
       <property name="password" value="${db.password}" />
    </bean>

    <!-- Configure Spring's transaction manager to use a DataSource -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- Configure jOOQ's ConnectionProvider to use Spring's TransactionAwareDataSourceProxy,
         which can dynamically discover the transaction context -->
    <bean id="transactionAwareDataSource"
        class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean class="org.jooq.impl.DataSourceConnectionProvider" name="connectionProvider">
        <constructor-arg ref="transactionAwareDataSource" />
    </bean>

    <!-- Configure the DSL object, optionally overriding jOOQ Exceptions with Spring Exceptions -->
    <bean id="dsl" class="org.jooq.impl.DefaultDSLContext">
        <constructor-arg ref="config" />
    </bean>
    
    <bean id="exceptionTranslator" class="org.jooq.example.spring.exception.ExceptionTranslator" />
    
    <!-- Invoking an internal, package-private constructor for the example
         Implement your own Configuration for more reliable behaviour -->
    <bean class="org.jooq.impl.DefaultConfiguration" name="config">
        <property name="SQLDialect"><value type="org.jooq.SQLDialect">H2</value></property>
        <property name="connectionProvider" ref="connectionProvider" />
        <property name="executeListenerProvider">
            <array>
                <bean class="org.jooq.impl.DefaultExecuteListenerProvider">
                    <constructor-arg index="0" ref="exceptionTranslator"/>
                </bean>
            </array>
        </property>
    </bean>
    
    <!-- This is the "business-logic" -->
    <bean id="books" class="org.jooq.example.spring.impl.DefaultBookService"/>
</beans>

核心要点在这段:

<!-- Configure jOOQ's ConnectionProvider to use Spring's TransactionAwareDataSourceProxy,
         which can dynamically discover the transaction context -->
    <bean id="transactionAwareDataSource"
        class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean class="org.jooq.impl.DataSourceConnectionProvider" name="connectionProvider">
        <constructor-arg ref="transactionAwareDataSource" />
    </bean>

这里要注意,一定要用 Spring 的 TransactionAwareDataSourceProxy 包装一层前面配置的 DataSource 对象。否则,jOOQ 拿到的就是一个没有被托管的原始 DataSource,那么就不会被 @Transactional 注解所管控。

对应的 Java 方式配置要点如下:

package de.maoxian.config;

import javax.sql.DataSource;

import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.impl.DefaultDSLContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class DbConfig {
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.maoxian")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public DSLContext dslContext(@Qualifier("maoxian-jooq-conf") org.jooq.Configuration configuration) {
        return new DefaultDSLContext(configuration);
    }

    @Bean("maoxian-jooq-conf")
    public org.jooq.Configuration jooqConf(ConnectionProvider connectionProvider) {
        return new DefaultConfiguration().derive(connectionProvider).derive(SQLDialect.MYSQL);
    }

    @Bean
    public ConnectionProvider connectionProvider(TransactionAwareDataSourceProxy transactionAwareDataSource) {
        return new DataSourceConnectionProvider(transactionAwareDataSource);
    }

    @Bean
    public TransactionAwareDataSourceProxy transactionAwareDataSourceProxy(DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }
}

重点在 ConnectionProvider 的配置。此处的 ConnectionProvider 在创建时,必须使用被 Spring 包过的 DataSource。如果直接使用 DataSource 而不是 TransactionAwareDataSourceProxy 则注解失效。


参考文档:

https://www.jooq.org/doc/latest/manual/getting-started/tutorials/jooq-with-spring/

记一则由于整数溢出导致的教科书级的死循环

首先铺垫一下,这段代码的输出是什么?

public static void main(String []args){
    System.out.println(Integer.MAX_VALUE + 1);
}

很多人可能很快就能答出来,正确答案是:-2147483648

那么接下来看这段代码:

public static void main(String []args){
    Long total = Long.MAX_VALUE;
    for (int i = 0; i < total; i++) {
        System.out.println(i);
    }
}

乍看之下似乎没啥大毛病,但是结合前面的铺垫,就会发现:

当 i 增长到 Integer.MAX_VALUE  的时候,“奇迹”出现了。接下来,下一个 i 值变为了 -2147483648。跟 total 一比,还是小,于是循环继续。

周而复始,这个循环就永远停不下来了。

当然,这里因为我的简化,还是能比较容易的看出这个死循环的。而实际使用中,这个 total 的取值往往是外部带来的。正常情况下,可能外部取值不会大过Integer.MAX_VALUE ,也就是 2147483648。但是当恶意请求出现或者是正常请求越过边界之后,问题就出现了。

所以当我们定义循环变量的时候,一定要小心。循环变量本身的取值范围一定要大于判断条件。简单来说,在上面的例子中,i 的取值必须要能够大于等于 total 可能的最大值。也就是说,在 total 是 Long 的情况下,i 也必须至少定义为 Long,才不会出现问题。

简单总结一下,就是对于控制循环的变量定义一定要非常谨慎。否则极有可能出现类似很难测出的 Bug,导致各种各样的问题

MyBatis 获取自增 ID 的小坑

在 MyBatis 中,获取自增 ID 主要有两个方法:

  1. 在 SQL 中增加两个属性,useGeneratedKeys  和 keyProperty
  2. 增加 selectKey  语句块,执行自定义 SQL 获取自增 ID

针对这几种方法,有几个小问题需要注意:

  1. useGeneratedKeys  是基于 JDBC 的 Prepared Statement  实现的,具体做法是调用 JDBC 的 getGeneratedKeys  方法,在 Prepared Statement  对象中取相应的值。当 DB 为 MySQL 的时候,会在响应时返回相应的自增字段值。但是,在某些实现 DB 分库分表的 Proxy 中,由于涉及 SQL 转换、重写的问题,可能对 Prepared Statement  的支持并不完整,导致 useGeneratedKeys  选项无法正常返回对应的自增 ID
  2. selectKey  方式是 MyBatis 自动的在执行完 Insert 语句之前/后自动执行对应的语句块,去生成/获取对于的 ID,并填充进相关的字段。在这里要注意,在某些 Prepared Statement  支持不完整的 Proxy 中,需要增加 statementType=”STATEMENT”  来强制指定不使用 Prepared Statement  来获取 ID
  3. keyProperty 这个参数也有一点需要注意。如果在 Mapper.java 中使用了 @Param  来对传入的参数进行了命名的话,这里接收自增值的属性需要用 paramName.fieldName  这种方式来写。如果只写 fieldName 则无法成功回传。最好的方式是不要用 @Param  来传参,而是全部封装成 bean 来进行传递

ScheduledExecutorService 一个小坑

今天在排查一个线上问题是发现一个使用 ScheduledExecutorService 执行的定时任务在执行了一次以后就再也没有执行过了。于是 Dump 了内存来检查问题。

首先,搜索对应 Task 的类,发现在堆中找不到这个类的实例。可是明明已经成功执行了一次,为何没有实例?

于是再去找 ScheduledExecutorService  对应的 ScheduledThreadPoolExecutor  类,成功筛选出了用来执行定时任务的实例。在实例的 queue 中,却只看到了 6 个 Task 对象,唯独不见了这个出问题的对象。百思不得解,因为日志中这个对象的 Logger 已经打印出来了,说明至少执行了一次,为啥会从内存中消失呢?

在同事的帮助下,查阅了 API 文档,发现了这么一句话:

If any execution of the task encounters an exception, subsequent executions are suppressed. Otherwise, the task will only terminate via cancellation or termination of the executor. If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.

注意通常的理解,这里的 suppressed 意思应该为抑制、压制,一般意义上理解为可能是说降低频率啊权重啊什么的。可是实际上,这里使用 stoped 更合适。从表现上看,你的 Task 只要出现了异常,就会被彻底扔掉,再也不会执行。

下面给出一个网上同样问题的复现代码:

import java.util.concurrent.Executors;

public class BadAssTask implements Runnable {

        @Override
        public void run() {
                System.out.println("Sleeping ...");
                try {
                        Thread.sleep(100);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
                System.out.println("Throwing ... ");
                throw new RuntimeException("bad ass!");
        }

        public static void main(String[] args) {
                Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(new BadAssTask(), 1, 1, TimeUnit.SECONDS);
        }

}

当我们注释掉 throw new RuntimeException(“bad ass!”);  的时候,可以看到每个 0.1s 会有一行 Sleeping … 输出。当开启注释时,Throwing … 之后再也没有 Sleeping 输出了。

于是来查看对应的代码,在 ScheduledThreadPoolExecutor  的代码中看到执行 Task 的实际调用方法为:

/**
 * Overrides FutureTask version so as to reset/requeue if periodic.
 */
public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

注意最后一个 if 语句。当 Task 执行出错的时候, runAndReset  的返回值为 False,所以 if 里面的内容不会执行,因此这个 task 就不会被放回队列,也就再也不会被执行了。

runAndReset 方法的代码如下:

/**
 * Executes the computation without setting its result, and then
 * resets this future to initial state, failing to do so if the
 * computation encounters an exception or is cancelled.  This is
 * designed for use with tasks that intrinsically execute more
 * than once.
 *
 * @return {@code true} if successfully run and reset
 */
protected boolean runAndReset() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return false;
    boolean ran = false;
    int s = state;
    try {
        Callable c = callable;
        if (c != null && s == NEW) {
            try {
                c.call(); // don't set result
                ran = true;
            } catch (Throwable ex) {
                setException(ex);
            }
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
    return ran && s == NEW;
}

注意其中的 setException 方法。这个方法会把 state 设置成出现异常的状态:

/**
 * Causes this future to report an {@link ExecutionException}
 * with the given throwable as its cause, unless this future has
 * already been set or has been cancelled.
 *
 * 

This method is invoked internally by the {@link #run} method * upon failure of the computation. * * @param t the cause of failure */ protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = t; UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state finishCompletion(); } }

于是在 runAndReset 最后的判断中,s == NEW 不成立,于是返回 False 了。

参考:

http://code.nomad-labs.com/2011/12/09/mother-fk-the-scheduledexecutorservice/

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ScheduledExecutorService.html#scheduleAtFixedRate-java.lang.Runnable-long-long-java.util.concurrent.TimeUnit-

Tomcat 在处理 Cookie 的时候的几个小坑

今天在代码中调用 HttpServletRequest  对象的 getCookies()  方法时,发现实际得到的 Cookie 数量与提交的不符。实际提交了 17 个 Cookie,但是获取到的只有 14 个。

经过排查,发现如果调用 getHeaders(“Cookie”)  方法,获取原始的 Cookie 串,是可以拿到正确的 17 个 Cookie 组成的字符串的。于是确认应该是 Tomcat 在处理 Cookie 的时候进行了过滤。

经过一番搜索,发现了这个文档:

http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html

其中,跟 Cookie 相关的参数有:

org.apache.tomcat.util.http. ServerCookie.ALLOW_EQUALS_IN_VALUE If this is true Tomcat will allow ‘=‘ characters when parsing unquoted cookie values. If false, cookie values containing ‘=‘ will be terminated when the ‘=‘ is encountered and the remainder of the cookie value will be dropped.

If not specified, the default value specification compliant value of false will be used.

org.apache.tomcat.util.http. ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0 If this is true Tomcat will allow HTTP separators in cookie names and values.

If not specified, the default specification compliant value of false will be used.

org.apache.tomcat.util.http. ServerCookie.ALLOW_NAME_ONLY If this is false then the requirements of the cookie specifications that cookies must have values will be enforced and cookies consisting only of a name but no value will be ignored.

If not specified, the default specification compliant value of false will be used.

org.apache.tomcat.util.http. ServerCookie.ALWAYS_ADD_EXPIRES If this is true Tomcat will always add an expires parameter to a SetCookie header even for cookies with version greater than zero. This is to work around a known IE6 and IE7 bug that causes IE to ignore the Max-Age parameter in a SetCookie header.

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be false, else the default value will be true.

org.apache.tomcat.util.http. ServerCookie.FWD_SLASH_IS_SEPARATOR If this is true then the / (forward slash) character will be treated as a separator. Note that this character is frequently used in cookie path attributes and some browsers will fail to process a cookie if the path attribute is quoted as is required by a strict adherence to the specifications. This is highly likely to break session tracking using cookies.

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be true, else the default value will be false.

org.apache.tomcat.util.http. ServerCookie.PRESERVE_COOKIE_HEADER If this is true Tomcat will ensure that cookie processing does not modify cookie header returned by HttpServletRequest.getHeader().

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be true, else the default value will be false.

org.apache.tomcat.util.http. ServerCookie.STRICT_NAMING If this is true then the requirements of the Servlet specification that Cookie names must adhere to RFC2109 (no use of separators) will be enforced.

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be true, else the default value will be false.

看了一下相关参数,跟我这个现象有关的是这两个选项:

org.apache.tomcat.util.http. ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0
org.apache.tomcat.util.http. ServerCookie.ALLOW_NAME_ONLY

于是按照这个选项去搜索,发现了这样一篇文章:

http://www.cnblogs.com/princessd8251/articles/4172103.html

其中详细说明了第一个选项的作用:

在网上查了下,cookie有以下版本:

版本0:由Netscape公司制定的,也被几乎所有的浏览器支持。Java中为了保持兼容性,目前只支持到版本0,Cookie的内容中不能包含空格,方括号,圆括号,等于号(=),逗号,双引号,斜杠,问号,@符号,冒号,分号。

版本1:根据RFC 2109(http://www.ietf.org/rfc/rfc2109.txt)文档制定的. 放宽了很多限制. 上面所限制的字符都可以使用. 但为了保持兼容性, 应该尽量避免使用这些特殊字符。

Tomcat 具体的过滤源码里面包含了这些字符:

如果 org.apache.tomcat.util.http.ServerCookie.FWD_SLASH_IS_SEPARATOR 设为 true (默认 False),’/’也会被作为http分隔符

如果 org.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0 设为 true (默认 False),tomcat将会允许cookie的name和value使用http分隔符

tomcat源码中的http分隔符:’\t’, ‘ ‘, ‘\”‘, ‘(‘, ‘)’, ‘,’, ‘:’, ‘;’, ‘<', '=', '>‘, ‘?’, ‘@’, ‘[‘, ‘\\’, ‘]’, ‘{‘, ‘}’

然后第二个选项就很好理解了,如果 Cookie 的 Value 是空,那么直接干掉。

于是,我们传的 Cookie 是这样的:

aaa=@abcdefg

Tomcat 先做了一次截断,将 @ 和后面的内容都去掉了;然后做了一次判断,发现 aaa 这个 Cookie 只有 name 没有 value,于是直接干掉了。

到此真相大白。

 

如果想要解决这个问题,有两个方法:

1. 在 Tomcat 的配置文件 catalina.properties  中设置 org.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0=true

2. 在代码中使用 request.getHeader(“Cookie”) 取出原始的 Cookie 串,自行处理。

Apache HttpClient 使用代理服务器 Proxy 的一个小坑

今天在调试一个用 HttpClient 写的 Demo 的时候遇到了一个问题:

org.apache.http.impl.execchain.RetryExec execute
INFO: I/O exception (org.apache.http.conn.UnsupportedSchemeException) caught when processing request to {tls}->http://proxyserver:port->https://servername:443: http protocol is not supported

也就是在通过 HTTP Proxy 进行 HTTPS 连接的时候,HttpClient 报了一个不支持 HTTP 协议。查了一下发现问题在于我使用 HttpClient 的方法。

由于我在使用 HttpClient 的时候是手动创建的 Registry ,而在创建的时候没有注册 HTTP 协议,导致报了上面那个错误。之前错误的代码是:

        // Create a registry of custom connection socket factories for supported
        // protocol schemes.
        Registry socketFactoryRegistry = RegistryBuilder.create()
                .register("https", sslsf)
                .build();

        // Create a connection manager with custom configuration.
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

需要修改为:

        // Create a registry of custom connection socket factories for supported
        // protocol schemes.
        Registry socketFactoryRegistry = RegistryBuilder.create()
                .register("https", sslsf)
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .build();

        // Create a connection manager with custom configuration.
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

加的这句保证了这个ConnectionSocketFactory 也可以处理 HTTP 协议,从而避免这个问题。

How to Install Java on Linux

Java allows you run cross-platform applications that can run on Mac OS-X, Linux, and Windows (among other OSs) without modification. Here’s how to install it on a GNU/Linux machine.

Steps

Manual Non-RPM Method

This is the generic method that also works with GNU/Linux clones that do not support RPM. It does not require administrator rights and allows installing multiple Java versions on the same computer.

  1. 1.Download the JDK.[1].
    • Click on the “Download” link under Java Platform (JDK) 7u4.
    • Accept the license and continue.
  2. 2.Select the right download. Under the “Product/File Description” you should pick your corresponding Linux option. For example, if you’re running Linux x86 (32-bit), you need to choose the corresponding “*.tar.gz” version.

    2943-21

  3. 3.Download the file as a .gz. Save it to your GNU/Linux machine.
  4. 4.Switch to the directory where you saved the file. You do not need to be a root and only must have the write access to the folder where you wish to install Java. If your administrator is not supportive, you may need to place Java into your home folder or (even better) on some shared network location
  5. 5.Uncompress the file. For example, you could do this in your home folder. Uncompressing will create a folder called “jdk1.7.0_04”. Java is now installed.
    • The installed Java jre is rather independent and can be easily moved into another place just by copying all its files.
    • You can install multiple different jre’s this way: they coexist together and can be used if some software requires the older version to run.
  6. 6.Launch Java. The Java executable you need to launch is located in a subfolder, called “bin.” This way of installation will not configure a default Java command for you — you must do this manually or always include the full path in your startup script.

Manual RPM Method

This seems like a “more civilized” way to install Java because it allows the installer to check the dependencies on some system libraries that may be missing. However, it does not support versioning easily and may fail even in some systems that do support RPMs. (Though the current Java installations are rather self-dependent and the required minimal requirements are usually satisfied anyway.)

  1. 1.Download the JDK.[2].
    • Click on the “Download” link under Java Platform (JDK) 7u4.
    • Accept the license and continue.
  2. 2.Select the right download. Under the “Product/File Description” you should pick your corresponding Linux option. For example, if you’re running Linux x86 (32-bit), you need to choose the corresponding “*.rpm” version, for example “jdk-7u4-linux-i586.rpm”.

    2943-2

  3. 3.Download the .rpm file. Save it to your GNU/Linux machine.
  4. 4.Log in as root and switch to the directory where you saved the file. Or, become root by running su and entering the super-user password.
  5. 5.Install the rpm file by executing ‘rpm -ivh filename.rpm’, where filename is the name of your .rpm file. (Such as jdk-7u4-linux-i586.rpm).
    • You may receive a message telling that program “rpm” is not installed.
    • If that is the case the program “rpm” is not installed. You need to install it writing: sudo apt-get install rpm. Enter your password, and you’re finished.
  6. 6.Create symbolic links. If you want to be able to execute this version of Java interpretor or compiler from any directory on your GNU/Linux system, you will have to create a few symbolic links:
    • sudo ln -s -v jdk1.7.0_04/bin/java /usr/bin/java
    • sudo ln -s -v jdk1.7.0_04/bin/javac /usr/bin/javac
    • Note that by installing using this method there could be dependencies that fail. It is better to use the package installer that handles all dependencies for you, since you will not be able to use Java correctly until the dependencies are resolved.

Ubuntu Method Using a GUI Package Manager

  1. 1.Open a package manager. You can use Synaptic or Adept Manager.
  2. 2.Install Open JDK. Java JDK and JRE are not available for installation through GUI Package Manager. You’ll need to install Open JDK instead.
    • Perform a search for openjdk-7-jdk.
    • Select openjdk-7-jdk for installation. Depending on the package manager, you may be asked if you wish to install the required dependencies or it will automatically select them without confirmation. For example, the openjdk-7-jre was already selected by Synaptic Package Manager. If you want to use Java as a plug-in in your browser, then also select to install icedtea-7-plugin.
  3. 3.Apply the changes. Click the button to apply the changes. Depending on the package manager, a pop-up window may appear asking for you to confirm the changes.
  4. 4.Wait for Java to install.

Ubuntu Method Using a Console

  1. 1.Enter one of the following commands into your console program:
    • sudo apt-get install openjdk-7-jdk openjdk-7-jre (if you don’t want the browser plug-in)
    • sudo apt-get install openjdk-7-jdk openjdk-7-jre icedtea-7-plugin (if you do need the plug-in)
    • apt-get automatically takes care of the dependencies and lists the changes for confirmation.
  2. 2.Enter y to confirm the installation.

Ubuntu OpenJDK Method Using a Console

  1. 1.Ubuntu no longer supports the Sun Java package in favor of OpenJDK.
  2. 2.Enter one of the following into your console program:
    • If you do not want the browser plugin, sudo apt-get install openjdk-6-jre.
    • If you do want the browser plugin, sudo apt-get install openjdk-6-jre icedtea6-plugin
    • If you’re working on a server without graphics, and want an environment just for running server applications (e.e. Tomcat or Glassfish), sudo apt-get install –no-install-recommends openjdk-6-jre-headless
    • If you need the full JDK (for writing Java programs): sudo apt-get install openjdk-6-jdk
    • apt-get automatically takes care of the dependencies and lists the changes for confirmation.
  3. 3.Enter y to confirm the installation.

Tips

  • It’s much easier to install from the repositories than from the download on Oracle’s site.
  • Even though it might be unfamiliar to you, using the console method is actually easier than the GUI method.
  • If you find how-to specific for your distribution we recommend using it because in some distributions, Java can be included in the software repositories (which you can benefit from).
  • On Red Hat 8.0 Linux, one could start a Nautilus file manager, desending to the directory, and click on the name of the rpm filename and activate the installation process. This does not work any more in Fedora Core 4 (other FC versions not tested). In reality, if you do this you will get a (warning) message saying that the file name indicates the file is not executable while the content of the file is of the type “executable”.
  • Another tip to follow progress of the rpm install is to use ‘rpm -ivv … ‘ which turns on theverbose output and gives you more info on the install as it progresses. You can do the same thing with other rpm actions to get rpm to be more verbose about what is going on.

Sources and Citations

  1.  www.oracle.com/technetwork/java/javase/downloads/index.html
  2.  www.oracle.com/technetwork/java/javase/downloads/index.html