分类目录归档:JAVA

JVM性能调优实战之UseParallelGC

前言
在生产环境中,你是否遇到过这样的问题?
1)系统运行一段时间后old区空间正逐渐减少?
2)遇到秒杀促销活动等场景old区存储突然暴增,导致JVM Full GC。fgc time持续过长导致cpu 100%?
3)jvm eden区分配的内存实际并没有按照配置的指定?survivor区对象晋升到old区并没有达到默认的15次?
4)如何避免Full GC,JVM产生FGC后如何解决?
……
接下来的内容,我将从实际场景出发,分别使用三种垃圾收集器:ParallelGC,Concurrent Mark Sweep(cms),G1,进行参数调优,解决我们在生产中jvm遇到的各种问题。jvm调优会作为专题持续更新。

以下GC调优默认使用JDK1.8

JDK1.8默认使用ParallelGC。新生代采用的是Parallel Scavenge,老年代Parallel Old。
并发垃圾收集器调优的内容一般为:
1)关闭jvm自动分配策略。
2)survivior空间调优。
通过这两点调整,使创建的对象按照设定的阈值执行。

关闭JVM自动分配策略

先来看默认情况下jvm内存空间的分配情况:

-server -Xms4G -Xmx4G -Xss512K 
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=/deploy_service_heap.log -verbose:gc -Xloggc:/deploy_service_gc.log

我们使用jdk1.8自带的jvisualvm工具监控查看运行情况,如图:

TIPS:对于并发类型的GC来说,jvm默认开启了内存自动适配策略参数是UseAdaptiveSizePolicy。使用-XX:-UseAdaptiveSizePolicy来关闭jvm的自动适配策略。

Survivior空间调优

通常的网文会说-XX:SurvivorRatio这个参数默认为8,意思是eden:s1:s2 = 8:1:1。这个比值到底要不要改,修改后效果就一定好吗?答案是否定的。举例说明:如果改成6(6:2:2)或者4(4:3:3)增加了survivor空间的同时缩减了eden区空间,这会导致eden区由于内存分配不够ygc的频次增加,每ygc一次,存活的对象就会进入survivor,但实际情况是survivor中的对象会很少,作者的应用程序qps200~500之间,持续时间为24小时,但survivor中的对象总量最大不超过70M,所以扩大survivor区空间是在浪费资源。
survivor区真正要调整的是TargetSurvivorRatio,MaxTenuringThreshold这两个参数。
TargetSurvivorRatio:表示目标survivor区存储率超过指定百分比时,会重新计算一次TenuringThreshold值。在生产中如果你遇到这种情况,说明需要调整TargetSurvivorRatio了。如图:

图中survivor区在每次ygc后都会重新计算一次threshold的值(默认最大为15),说明在survivor区中的对象并不都是经过15次以上的ygc才进入老年代。
TIPS:在并发类型的GC中,-XX:-UseAdaptiveSizePolicy保证新生代内存分配按照你指定的参数执行。-XX:TargetSurvivorRatio=80 -XX:MaxTenuringThreshold=15 让目标survivor使用空间达到指定的百分比才会重新计算threshold值。如此一来,survivor中大部分的对象会活到15岁以上才进入老年代。优点是:阻止新对象快速进入old区将其填满,导致old区空间使用率100%而触发full gc。
通过上述调整后,我们来看一下jvm运行的效果:

我们看到eden区实际内存分配为1.6G,survivor区为204.5M,并且survivor区内的对象全部都在第16次ygc后进入了old区,这才是我们想要的效果!

另外补充说明一下,ParallelGC优调当然远不止这些,比如还有一些协助参数:-XX:+PrintTenuringDistribution 这个在jvm启动后每次进行内存分配可以打印出详细的内存分配情况。

以上是作者通过实际生产监控发现的问题,并整理出一些容易被我们忽略的问题点。欢迎各位读者评论区留言进行经验分享和探讨。

Java实现LFU算法

LFU(Least Frequently Used)算法,即最少访问算法,根据访问缓存的历史频率来淘汰数据,核心思想是“如果数据在过去一段时间被访问的次数很少,那么将来被访问的概率也会很低”。

手撕LFU算法

package com.github.xuchengen.cache;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class LRUCache<K, V> {

    private Entry<K, V> head;
    private Entry<K, V> tail;
    private Map<K, Entry<K, V>> cache;
    private int capacity;
    private int size;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.cache = new HashMap<>();
        this.head = new Entry<>();
        this.tail = new Entry<>();

        head.next = tail;
        tail.pre = head;
    }

    public V get(K key) {
        Entry<K, V> entry = cache.get(key);
        if (Objects.isNull(entry)) {
            return null;
        }

        moveToHead(entry);
        return entry.value;
    }

    public void put(K key, V value) {
        Entry<K, V> entry = cache.get(key);
        if (Objects.nonNull(entry)) {
            entry.value = value;
            cache.put(key, entry);
            moveToHead(entry);
            return;
        }

        if (capacity <= size) {
            Entry<K, V> lastEntry = tail.pre;
            cache.remove(lastEntry.key);
            remove(lastEntry);
            size--;
        }

        Entry<K, V> newEntry = new Entry<>(key, value);
        cache.put(key, newEntry);
        add(newEntry);
        size++;
    }

    private void moveToHead(Entry<K, V> entry) {
        remove(entry);
        add(entry);
    }

    private void add(Entry<K, V> entry) {
        head.next.pre = entry;
        entry.next = head.next;

        head.next = entry;
        entry.pre = head;
    }

    private void remove(Entry<K, V> entry) {
        entry.pre.next = entry.next;
        entry.next.pre = entry.pre;
    }

    private static class Entry<K, V> {
        private Entry<K, V> pre;
        private Entry<K, V> next;
        private K key;
        private V value;

        public Entry() {
        }

        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    public static void main(String[] args) {
        LRUCache<Integer, Integer> cache = new LRUCache<>(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(2));
        cache.put(3, 3);
        System.out.println(cache.get(1));
        System.out.println(cache.get(2));
        System.out.println(cache.get(3));
    }
}

 

Java实现LRU算法

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t,当须淘汰一个页面时,选择现有页面中其t值最大的,即最近最少使用的页面予以淘汰。

基于LinkedHashMap实现LRU

package com.github.xuchengen.cache;

import java.util.LinkedHashMap;
import java.util.Map;

public class SimpleLRUCache<K, V> extends LinkedHashMap<K, V> {

    private int capacity;

    public SimpleLRUCache(int capacity) {
        super(16, 0.75F, true);
        this.capacity = capacity;
    }


    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return super.size() > capacity;
    }

    public static void main(String[] args) {
        SimpleLRUCache<Integer, Integer> cache = new SimpleLRUCache<>(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(2));
        cache.put(3, 3);
        System.out.println(cache.get(1));
        System.out.println(cache.get(2));
        System.out.println(cache.get(3));
    }
}

手撕LRU算法

package com.github.xuchengen.cache;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class LRUCache<K, V> {

    private Entry<K, V> head;
    private Entry<K, V> tail;
    private Map<K, Entry<K, V>> cache;
    private int capacity;
    private int size;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.cache = new HashMap<>();
        this.head = new Entry<>();
        this.tail = new Entry<>();

        head.next = tail;
        tail.pre = head;
    }

    public V get(K key) {
        Entry<K, V> entry = cache.get(key);
        if (Objects.isNull(entry)) {
            return null;
        }

        moveToHead(entry);
        return entry.value;
    }

    public void put(K key, V value) {
        Entry<K, V> entry = cache.get(key);
        if (Objects.nonNull(entry)) {
            entry.value = value;
            cache.put(key, entry);
            moveToHead(entry);
            return;
        }

        if (capacity <= size) {
            Entry<K, V> lastEntry = tail.pre;
            cache.remove(lastEntry.key);
            remove(lastEntry);
            size--;
        }

        Entry<K, V> newEntry = new Entry<>(key, value);
        cache.put(key, newEntry);
        add(newEntry);
        size++;
    }

    private void moveToHead(Entry<K, V> entry) {
        remove(entry);
        add(entry);
    }

    private void add(Entry<K, V> entry) {
        head.next.pre = entry;
        entry.next = head.next;

        head.next = entry;
        entry.pre = head;
    }

    private void remove(Entry<K, V> entry) {
        entry.pre.next = entry.next;
        entry.next.pre = entry.pre;
    }

    private static class Entry<K, V> {
        private Entry<K, V> pre;
        private Entry<K, V> next;
        private K key;
        private V value;

        public Entry() {
        }

        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    public static void main(String[] args) {
        LRUCache<Integer, Integer> cache = new LRUCache<>(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(2));
        cache.put(3, 3);
        System.out.println(cache.get(1));
        System.out.println(cache.get(2));
        System.out.println(cache.get(3));
    }
}

 

生产环境Java项目CPU飙升100%排查

Java线上项目CPU飙升100%排查步骤

# 使用TOP命令查看进程ID
top
 
# 使用top命令查看线程ID
top -H -p {进程ID}

# 转换线程ID为16进制便于后续搜索
printf '%x' {线程ID}

# 使用jstack命令导出文件
jstack {进程ID} > {项目名称_进程ID}.txt

# 最后使用16进制线程ID去txt文件搜索进行进一步分析排查

 

解决Mac高版本系统不能安装JDK6的问题

公司部分业务系统依然使用的JDK6,然后便有了下文。

苹果官方JDK6下载地址:https://support.apple.com/kb/dl1572?locale=zh_CN

苹果高版本Mac系统已经不允许直接运行dmg包安装JDK6,以下脚本请用Apple script运行,然后双击桌面的pkg包安装。

set theDMG to choose file with prompt "Please select javaforosx.dmg:" of type {"dmg"}
do shell script "hdiutil mount " & quoted form of POSIX path of theDMG
do shell script "pkgutil --expand /Volumes/Java\\ for\\ macOS\\ 2017-001/JavaForOSX.pkg ~/tmp"
do shell script "hdiutil unmount /Volumes/Java\\ for\\ macOS\\ 2017-001/"
do shell script "sed -i '' 's/return false/return true/g' ~/tmp/Distribution"
do shell script "pkgutil --flatten ~/tmp ~/Desktop/Java.pkg"
do shell script "rm -rf ~/tmp"
display dialog "Modified Java.pkg saved on desktop" buttons {"Ok"}

 

基于ShardingShphere-JDBC实现读写分离

基于ShardingShphere-JDBC读写分离的样版工程。通过该工程快速了解ShardingShphere-JDBC框架。

Github地址

项目框架

  • Spring Boot
  • Mybatis
  • tkMapper
  • PageHelper
  • HikariCP
  • MySQL
  • knife4j
  • ShardingShphere-JDBC

基础设施搭建

MySQL数据库一个master节点一个slave节点均部署在Docker容器中,服务器使用CentOS7

安装Docker

yum -y install docker

启动Docker服务

systemctl enable docker.service 
systemctl start docker.service

创建Docker macvlan网络

docker network create -d macvlan --subnet=<局域网网段> --gateway=<网关地址> -o parent=<物理网卡名称> <给该网络取一个漂亮的名称>

举个例子

docker network create -d macvlan --subnet=192.168.0.0/24 --gateway=192.168.210.0.1 -o parent=eth0 macvlan_net

部署MySQL Master节点

$PWD参数表示你当前命令行所处的路径,那我们cd到Docker的volumes路径下,来部署我们的master节点。

cd /var/lib/docker/volumes
docker run -itd \
 --name mysql-01 \
 --hostname mysql-01 \
 -v $PWD/mysql_01/conf:/etc/mysql/conf.d \
 -v $PWD/mysql_01/data:/var/lib/mysql \
 -e TZ=Asia/Shanghai \
 -e MYSQL_ROOT_PASSWORD=123456 \
 --net macvlan_net \
 --ip 192.168.0.40 \
 mysql:5.7 \
 --character-set-server=utf8mb4 \
 --collation-server=utf8mb4_unicode_ci

配置MySQL Master节点

进入$PWD/mysql_01/conf目录,创建或修改my.cnf文件。

vim /var/lib/docker/volumes/mysql_01/conf/my.cnf
[mysqld]
server-id=1
log-bin=master-bin
binlog-format=row
binlog-ignore-db=information_schema
binlog-ignore-db=mysql
binlog-ignore-db=performance_schema
binlog-ignore-db=sys
expire_logs_days=7

部署MySQL Slave节点

$PWD参数表示你当前命令行所处的路径,那我们cd到Docker的volumes路径下,来部署我们的slave节点。

cd /var/lib/docker/volumes
docker run -itd \
 --name mysql-02 \
 --hostname mysql-02 \
 -v $PWD/mysql_02/conf:/etc/mysql/conf.d \
 -v $PWD/mysql_02/data:/var/lib/mysql \
 -e TZ=Asia/Shanghai \
 -e MYSQL_ROOT_PASSWORD=123456 \
 --net macvlan_net \
 --ip 192.168.0.41 \
 mysql:5.7 \
 --character-set-server=utf8mb4 \
 --collation-server=utf8mb4_unicode_ci

配置MySQL Slave节点

进入$PWD/mysql_02/conf目录,创建或修改my.cnf文件。

vim /var/lib/docker/volumes/mysql_02/conf/my.cnf
[mysqld]
server-id=2
read-only=1

配置主从复制

使用mysql命令行工具连接master节点

mysql -uroot -p123456 -h'192.168.0.40'

查看当前master节点状态信息

show master status;

master节点状态信息回显

mysql&gt; show master status;
+-------------------+----------+--------------+-------------------------------------------------+-------------------+
| File              | Position | Binlog_Do_DB | Binlog_Ignore_DB                                | Executed_Gtid_Set |
+-------------------+----------+--------------+-------------------------------------------------+-------------------+
| master-bin.000001 |   154    |              | information_schema,mysql,performance_schema,sys |                   |
+-------------------+----------+--------------+-------------------------------------------------+-------------------+
1 row in set (0.00 sec)

使用mysql命令行工具连接slave节点

mysql -uroot -p123456 -h'192.168.0.40'

配置主从复制命令

  • master_log_filemaster节点回显信息中的File列的值。
  • master_log_posmaster节点回显信息中的Position列的值。
change master to
master_host='192.168.0.40',
master_user='root',
master_password='123456',
master_log_file='master-bin.000001',
master_log_pos=154;

启动主从复制命令

start slave;

查看当前slave节点状态信息

show slave status \G;

slave节点状态信息回显

mysql&gt; show slave status \G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.0.40
                  Master_User: root
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: master-bin.000001
          Read_Master_Log_Pos: 116304
               Relay_Log_File: mysql-02-relay-bin.000002
                Relay_Log_Pos: 113214
        Relay_Master_Log_File: master-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 116304
              Relay_Log_Space: 113424
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 1
                  Master_UUID: 36902060-410b-11ec-b5e5-0242c0a8d228
             Master_Info_File: /var/lib/mysql/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: 
            Executed_Gtid_Set: 
                Auto_Position: 0
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
1 row in set (0.00 sec)

关于SharindShphere-JDBC框架主键回显报错问题

本项目数据访问层使用tkMapper框架,主键自增在字段上使用@GeneratedValue(generator = "JDBC")注解, 当调用tkMapper自带的insertSelective方法会导致空指针异常。

异常堆栈

java.lang.NullPointerException: ResultSet should call next or has no more data.
    at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:897) ~[guava-29.0-jre.jar:na]
    at org.apache.shardingsphere.driver.jdbc.core.resultset.GeneratedKeysResultSet.checkStateForGetData(GeneratedKeysResultSet.java:243) ~[shardingsphere-jdbc-core-5.0.0.jar:5.0.0]
    at org.apache.shardingsphere.driver.jdbc.core.resultset.GeneratedKeysResultSet.getLong(GeneratedKeysResultSet.java:142) ~[shardingsphere-jdbc-core-5.0.0.jar:5.0.0]
    at org.apache.ibatis.type.LongTypeHandler.getNullableResult(LongTypeHandler.java:44) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.type.LongTypeHandler.getNullableResult(LongTypeHandler.java:26) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:94) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator$KeyAssigner.assign(Jdbc3KeyGenerator.java:270) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.lambda$assignKeysToParam$0(Jdbc3KeyGenerator.java:124) ~[mybatis-3.5.7.jar:3.5.7]
    at java.util.ArrayList.forEach(ArrayList.java:1259) ~[na:1.8.0_301]
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.assignKeysToParam(Jdbc3KeyGenerator.java:124) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.assignKeys(Jdbc3KeyGenerator.java:104) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.processBatch(Jdbc3KeyGenerator.java:85) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.processAfter(Jdbc3KeyGenerator.java:71) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:51) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.update(RoutingStatementHandler.java:74) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:50) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117) ~[mybatis-3.5.7.jar:3.5.7]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_301]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_301]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_301]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_301]
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:64) ~[mybatis-3.5.7.jar:3.5.7]
    at com.sun.proxy.$Proxy278.update(Unknown Source) ~[na:na]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:194) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:181) ~[mybatis-3.5.7.jar:3.5.7]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_301]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_301]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_301]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_301]
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:427) ~[mybatis-spring-2.0.6.jar:2.0.6]
    at com.sun.proxy.$Proxy230.insert(Unknown Source) ~[na:na]
    at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:272) ~[mybatis-spring-2.0.6.jar:2.0.6]
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86) ~[mybatis-3.5.7.jar:3.5.7]
    at com.sun.proxy.$Proxy236.insertSelective(Unknown Source) ~[na:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_301]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_301]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_301]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_301]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139) ~[spring-tx-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at com.sun.proxy.$Proxy238.insertSelective(Unknown Source) ~[na:na]
    at com.github.xuchengen.rws.biz.UserService.createUser(UserService.java:19) ~[classes/:na]
    at com.github.xuchengen.rws.biz.UserService$$FastClassBySpringCGLIB$$49f8d808.invoke(&lt;generated&gt;) ~[classes/:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:687) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at com.github.xuchengen.rws.biz.UserService$$EnhancerBySpringCGLIB$$488bcf4f.createUser(&lt;generated&gt;) ~[classes/:na]
    at com.github.xuchengen.rws.web.UserController.createUser(UserController.java:43) ~[classes/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_301]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_301]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_301]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_301]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:660) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:747) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_301]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_301]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_301]

异常分析

当我们调用tkMapper框架自带的insertSelective方法时生成的SQL语句如下:

INSERT INTO `t_user` ( `id`,`name`,`phone` ) VALUES( ?,?,? )

PreparedStatement绑定参数如下:

Parameters: null, 凡尔赛(String), 17811111113(String)

tkMapper框架对insertSelective方法的官方解释:

保存一个实体,null的属性不会保存,会使用数据库默认值,无默认值则使用null

问题就出在这里,自增主键的null会当作绑定参数传递。ShardingShphere框架在底层处理时将该null值作为插入的主键值暂存, 执行回填逻辑时将该值作为可回填主键的值最终导致了异常。

重点关注的方法

public InsertStatementContext(final Map&lt;String, ShardingSphereMetaData&gt; metaDataMap, final List&lt;Object&gt; parameters, final InsertStatement sqlStatement, final String defaultSchemaName) {
    super(sqlStatement);
    AtomicInteger parametersOffset = new AtomicInteger(0);
    insertValueContexts = getInsertValueContexts(parameters, parametersOffset);
    insertSelectContext = getInsertSelectContext(metaDataMap, parameters, parametersOffset, defaultSchemaName).orElse(null);
    onDuplicateKeyUpdateValueContext = getOnDuplicateKeyUpdateValueContext(parameters, parametersOffset).orElse(null);
    tablesContext = new TablesContext(getAllSimpleTableSegments());
    ShardingSphereSchema schema = getSchema(metaDataMap, defaultSchemaName);
    List&lt;String&gt; insertColumnNames = getInsertColumnNames();
    columnNames = useDefaultColumns() ? schema.getAllColumnNames(sqlStatement.getTable().getTableName().getIdentifier().getValue()) : insertColumnNames;
    generatedKeyContext = new GeneratedKeyContextEngine(sqlStatement, schema).createGenerateKeyContext(insertColumnNames, getAllValueExpressions(sqlStatement), parameters).orElse(null);
    this.schemaName = defaultSchemaName;
}
public Optional&lt;GeneratedKeyContext&gt; createGenerateKeyContext(final List&lt;String&gt; insertColumnNames, final List&lt;List&lt;ExpressionSegment&gt;&gt; valueExpressions, final List&lt;Object&gt; parameters) {
    String tableName = insertStatement.getTable().getTableName().getIdentifier().getValue();
    return findGenerateKeyColumn(tableName).map(optional -&gt; containsGenerateKey(insertColumnNames, optional)
            ? findGeneratedKey(insertColumnNames, valueExpressions, parameters, optional) : new GeneratedKeyContext(optional, true));
}
private GeneratedKeyContext findGeneratedKey(final List&lt;String&gt; insertColumnNames, final List&lt;List&lt;ExpressionSegment&gt;&gt; valueExpressions, 
                                                 final List&lt;Object&gt; parameters, final String generateKeyColumnName) {
    GeneratedKeyContext result = new GeneratedKeyContext(generateKeyColumnName, false);
    for (ExpressionSegment each : findGenerateKeyExpressions(insertColumnNames, valueExpressions, generateKeyColumnName)) {
        if (each instanceof ParameterMarkerExpressionSegment) {
            result.getGeneratedValues().add((Comparable&lt;?&gt;) parameters.get(((ParameterMarkerExpressionSegment) each).getParameterMarkerIndex()));
        } else if (each instanceof LiteralExpressionSegment) {
            result.getGeneratedValues().add((Comparable&lt;?&gt;) ((LiteralExpressionSegment) each).getLiterals());
        }
    }
    return result;
}

 

使用Java实现负载均衡算法

说到负载均衡我们首先会想到Nginx负载均衡策略,在Nginx中支持5种负载均衡策略他们分别是:轮询、加权轮询、ip_hash、fair、url_hash。

概念

负载均衡是将客户端请求访问,通过提前约定好的规则转发给各个server。其中有好几个种经典的算法,下面我们用Java实现这几种算法。

轮询算法

轮询算法按顺序把每个新的连接请求分配给下一个服务器,最终把所有请求平分给所有的服务器。

优点:绝对公平

缺点:无法根据服务器性能去分配,无法合理利用服务器资源。

package com.monkeyjava.learn.basic.robin;

import com.google.common.collect.Lists;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TestRound {

    private Integer  index = 0;
    private List<String> ips = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3");


    public String roundRobin(){
        String serverIp;
        synchronized(index){
            if (index >= ips.size()){
                index = 0;
            }
            serverIp= ips.get(index);
            //轮询+1
            index ++;
        }
        return serverIp;
    }

    public static void main(String[] args) {
        TestRound testRoundRobin =new TestRound();
        for (int i=0;i< 10 ;i++){
            String serverIp= testRoundRobin.roundRobin();
            System.out.println(serverIp);
        }
    }
}

输出结果:

192.168.1.1
192.168.1.2
192.168.1.3
192.168.1.1
192.168.1.2
192.168.1.3
192.168.1.1
192.168.1.2
192.168.1.3
192.168.1.1

加权轮询法

每个机器接受的连接数量按权重比例进行分配。

该算法是对普通轮询算法的改进,比如你可以设定:第三台机器的处理能力是第一台机器的两倍,那么负载均衡器会把两倍的连接数量分配给第3台机器,轮询可以将请求顺序按照权重分配到后端。

package com.monkeyjava.learn.basic.robin;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TestWeight {
    private Integer index = 0;
    static Map<String, Integer> ipMap=new HashMap<String, Integer>(16);
    static {
        // 1.map, key-ip,value-权重
        ipMap.put("192.168.1.1", 1);
        ipMap.put("192.168.1.2", 2);
        ipMap.put("192.168.1.3", 4);

    }

    public List<String> getServerIpByWeight() {
        List<String> ips = new ArrayList<String>(32);
        for (Map.Entry<String, Integer> entry : ipMap.entrySet()) {
            String ip = entry.getKey();
            Integer weight = entry.getValue();
            // 根据权重不同,放入list 中的数量等同于权重,轮询出的的次数等同于权重
            for (int ipCount =0; ipCount < weight; ipCount++) {
                ips.add(ip);
            }
        }
        return ips;
    }

    public String weightRobin(){
        List<String> ips = this.getServerIpByWeight();
        if (index >= ips.size()){
            index = 0;
        }
        String serverIp= ips.get(index);
        index ++;
        return  serverIp;
    }

    public static void main(String[] args) {
        TestWeight testWeightRobin=new TestWeight();
        for (int i =0;i< 10 ;i++){
            String server=testWeightRobin.weightRobin();
            System.out.println(server);
        }
    }
}

输出结果:

192.168.1.1
192.168.1.3
192.168.1.3
192.168.1.3
192.168.1.3
192.168.1.2
192.168.1.2
192.168.1.1
192.168.1.3
192.168.1.3

加权随机法

获取带有权重的随机数字,随机这种东西,不能看绝对,只能看相对,我们不用index 控制下标进行轮询,只用random 进行随机取ip,即实现算法。

package com.monkeyjava.learn.basic.robin;

import java.util.*;

public class TestRandomWeight {

    static Map<String, Integer> ipMap=new HashMap<String, Integer>(16);
    static {
        // 1.map, key-ip,value-权重
        ipMap.put("192.168.1.1", 1);
        ipMap.put("192.168.1.2", 2);
        ipMap.put("192.168.1.3", 4);

    }

    public List<String> getServerIpByWeight() {
        List<String> ips = new ArrayList<String>(32);
        for (Map.Entry<String, Integer> entry : ipMap.entrySet()) {
            String ip = entry.getKey();
            Integer weight = entry.getValue();
            // 根据权重不同,放入list 中的数量等同于权重,轮询出的的次数等同于权重
            for (int ipCount =0; ipCount < weight; ipCount++) {
                ips.add(ip);
            }
        }
        return ips;
    }

    public String randomWeightRobin(){
        List<String> ips = this.getServerIpByWeight();
        //循环随机数
        Random random=new Random();
        int index =random.nextInt(ips.size());
        String serverIp = ips.get(index);
        return  serverIp;
    }

    public static void main(String[] args) {
        TestRandomWeight testRandomWeightRobin=new TestRandomWeight();
        for (int i =0;i< 10 ;i++){
            String server= testRandomWeightRobin.randomWeightRobin();
            System.out.println(server);
        }
    }
}

输出结果:

192.168.1.3
192.168.1.3
192.168.1.2
192.168.1.1
192.168.1.2
192.168.1.1
192.168.1.3
192.168.1.2
192.168.1.2
192.168.1.3

随机法

负载均衡方法随机的把负载分配到各个可用的服务器上,通过随机数生成算法选取一个服务器,这种实现算法最简单,随之调用次数增大,这种算法可以达到没台服务器的请求量接近于平均。

package com.monkeyjava.learn.basic.robin;

import com.google.common.collect.Lists;

import java.util.List;
import java.util.Random;

public class TestRandom {


    private List<String> ips = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3");


    public String randomRobin(){
        //随机数
        Random random=new Random();
        int index =random.nextInt(ips.size());
        String serverIp= ips.get(index);
        return  serverIp;

    }

    public static void main(String[] args) {
        TestRandom testRandomdRobin =new TestRandom();
        for (int i=0;i< 10 ;i++){
            String serverIp= testRandomdRobin.randomRobin();
            System.out.println(serverIp);
        }
    }
}

输出结果:

192.168.1.3
192.168.1.3
192.168.1.1
192.168.1.2
192.168.1.1
192.168.1.3
192.168.1.2
192.168.1.3
192.168.1.3
192.168.1.2

IP_Hash算法

hash(ip)%N算法,通过一种散列算法把客户端来源IP根据散列取模算法将请求分配到不同的服务器上。

优点:保证了相同客户端IP地址将会被哈希到同一台后端服务器,直到后端服务器列表变更。根据此特性可以在服务消费者与服务提供者之间建立有状态的session会话。

缺点: 如果服务器进行了下线操作,源IP路由的服务器IP就会变成另外一台,如果服务器没有做session 共享话,会造成session丢失。

package com.monkeyjava.learn.basic.robin;

import com.google.common.collect.Lists;

import java.util.List;

public class TestIpHash {


    private List<String> ips = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3");


    public String ipHashRobin(String clientIp){
        int hashCode=clientIp.hashCode();
        int serverListsize=ips.size();
        int index = hashCode%serverListsize;
        String serverIp= ips.get(index);
        return  serverIp;

    }

    public static void main(String[] args) {
        TestIpHash testIpHash =new TestIpHash();
        String servername= testIpHash.ipHashRobin("192.168.88.2");
        System.out.println(servername);
    }
}

输出结果:

192.168.1.3

 

 

SpringBoot通用日志解决方案

金融项目中对于业务较为敏感我们通常需要将用户的操作形成一个结构化的数据并进行持久化。

结构化日志需要的字段:

操作员信息、客户端IP地址、请求地址、控制器名称、控制器方法名称、HTTP请求类型、HTTP请求参数。

问题描述:

在获取请求参数时必然会读取request.getInputStream。由于流只允许读一次,后续读取必然会导致异常。

解决方案:

在SpringBoot框架中给我们提供了一个基于Filter的简单通用日志——CommonsRequestLoggingFilter,这个日志仅仅只实现了日志文件的输出远远达不到我们的设计目标。

通过阅读源码我发现了ContentCachingRequestWrapper这个类能够解决HttpServletRequest inputStream只能读取一次的问题,但是这个类有缺陷(前提必须是doFilter之前不能使用request.getInputStream()方法)。

配置Filter让后续的请求可以正常request.getInputStream

package com.bbc.ibank.sys.app.filter;

import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 请求上下文缓存过滤器<br>
 * 作者:徐承恩<br>
 * 邮箱:xuchengen@gmail.com<br>
 * 日期:2020/10/12 2:55 下午<br>
 */
public class ContentCachingRequestFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
        filterChain.doFilter(wrapper, response);
    }
}

拦截器

package com.bbc.ibank.sys.app.interceptor;

import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.netfinworks.vfsso.client.authapi.VfSsoUser;
import com.bbc.ibank.dal.mapper.LogDOMapper;
import com.bbc.ibank.dal.model.LogDO;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Objects;

/**
 * 通用日志拦截器<br>
 * 作者:徐承恩<br>
 * 邮箱:xuchengen@gmail.com<br>
 * 日期:2020/10/12 10:16 上午<br>
 */
public class LogInterceptor implements HandlerInterceptor {

    private static final Log log = LogFactory.get(LogInterceptor.class);

    @Resource(name = "logDOMapper")
    private LogDOMapper logDOMapper;

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception e) {
        try {
            // 当前登录操作员信息
            String userInfo = JSONUtil.toJsonStr(VfSsoUser.get());

            // 控制器名称
            String controllerName = null;
            // 方法名称
            String actionName = null;

            if (handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                controllerName = handlerMethod.getBean().getClass().getSimpleName();
                actionName = handlerMethod.getMethod().getName();
            }

            // 客户端IP
            String clientIP = ServletUtil.getClientIP(request);
            // 请求地址
            String requestUrl = request.getRequestURL().toString();
            // 请求方法类型
            String method = request.getMethod();
            // 请求参数
            String params = null;
            if (ServletUtil.isGetMethod(request)) {
                params = JSONUtil.toJsonStr(ServletUtil.getParams(request));
            } else if (ServletUtil.isPostMethod(request)) {
                if (ContentType.FORM_URLENCODED.getValue().equals(request.getContentType())) {
                    params = JSONUtil.toJsonStr(ServletUtil.getParams(request));
                } else if (ContentType.JSON.getValue().equals(request.getContentType())) {
                    ContentCachingRequestWrapper nativeRequest =
                            WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
                    if (Objects.nonNull(nativeRequest)) {
                        params = new String(nativeRequest.getContentAsByteArray(), StandardCharsets.UTF_8.name());
                    }
                }
            }

            LogDO logDO = new LogDO();
            logDO.setController(controllerName);
            logDO.setAction(actionName);
            logDO.setUrl(requestUrl);
            logDO.setMethod(method);
            logDO.setIp(clientIP);
            logDO.setCreateTime(new Date());
            logDO.setParams(params);
            logDO.setUserInfo(userInfo);
            logDOMapper.insertSelective(logDO);

        } catch (Exception exception) {
            log.error("通用日志异常:", e);
        }
    }
}

注册Filter和拦截器

package com.bbc.ibank.sys.app.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import cn.hutool.setting.Setting;
import com.netfinworks.vfsso.client.filter.VfSsoCasFilter;
import com.bbc.ibank.sys.app.annotation.IgnoreLoginCheck;
import com.bbc.ibank.sys.app.constant.AppConst;
import com.bbc.ibank.sys.app.constant.SymbolConst;
import com.bbc.ibank.sys.app.filter.ContentCachingRequestFilter;
import com.bbc.ibank.sys.app.interceptor.*;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 过滤器配置<br>
 * 作者:徐承恩<br>
 * 邮箱:xuchengen@gmail.com<br>
 * 日期:2020/5/11 10:56 上午<br>
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private static final Log log = LogFactory.get(WebConfig.class);

    @Value(value = "${profile}")
    private String profile;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 执行顺序就是添加的顺序

        // 通用日志拦截器
        registry.addInterceptor(logInterceptor())
                .addPathPatterns(AppConst.INTERCEPTOR_API_BASE_PATH);
    }

    @Bean
    public LogInterceptor logInterceptor() {
        return new LogInterceptor();
    }

    @Bean
    public FilterRegistrationBean<ContentCachingRequestFilter> contentCacheingRequestFilter() {
        FilterRegistrationBean<ContentCachingRequestFilter> registration =
                new FilterRegistrationBean<>(new ContentCachingRequestFilter());
        registration.addUrlPatterns(AppConst.FILTER_API_BASE_PATH);
        registration.setName(AppConst.CONTENT_CACHING_REQUEST_FILTER_NAME);
        registration.setOrder(1);
        return registration;
    }
}

 

InvalidKeyException: Illegal key size异常解决方案

测试环境的出款定时任务大量抛出InvalidKeyException: Illegal key size异常,根据以往经验初步判断是加密出现问题需要安装JCE相关的包。

产生错误原因

为了数据代码在传输过程中的安全,很多时候我们都会将要传输的数据进行加密,然后等对方拿到后再解密使用。我们在使用AES加解密的时候,在遇到128位密钥加解密的时候,没有进行什么特殊处理;然而,在使用256位密钥加解密的时候,如果不进行特殊处理的话,往往会出现这个异常

java.security.InvalidKeyException: Illegal key size

com.itrus.cryptorole.CryptoException: org.bouncycastle.cms.CMSException: key invalid in message

为什么会产生这样的错误?

我们做Java开发,或是Android开发,都会先在电脑上安装JDK(Java Development Kit) 并配置环境变量,JDK也就是 Java 语言的软件开发工具包,JDK中包含有JRE(Java Runtime Environment,即:Java运行环境),JRE中包括Java虚拟机(Java Virtual Machine)、Java核心类库和支持文件,而我们今天要说的主角就在Java的核心类库中。在Java的核心类库中有一个JCE(Java Cryptography Extension),JCE是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现,所以这个是实现加密解密的重要类库。

在我们安装的JRE目录下有这样一个文件夹:%JAVE_HOME%\jre\lib\security(%JAVE_HOME%是自己电脑的Java路径,一版默认是:C:\Program Files\Java,具体看自己当时安装JDK和JRE时选择的路径是什么),其中包含有两个.jar文件:“local_policy.jar ”和“US_export_policy.jar”,也就是我们平时说的jar包,再通俗一点说就是Java中包含的类库(Sun公司的程序大牛封装的类库,供使用Java开发的程序员使用),这两个jar包就是我们JCE中的核心类库了。JRE中自带的“local_policy.jar ”和“US_export_policy.jar”是支持128位密钥的加密算法,而当我们要使用256位密钥算法的时候,已经超出它的范围,无法支持,所以才会报:“java.security.InvalidKeyException: Illegal key size or default parameters”的异常。那么我们怎么解决呢?

如何解决

去官方下载JCE无限制权限策略文件。

JDK5 JCE下载

JDK6 JCE下载

JDK7 JCE下载

JDK8 JCE下载

下载后解压,可以看到local_policy.jarUS_export_policy.jar以及readme.txt

如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件。

如果安装了JDK,还要将两个jar文件也放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件。

Java的JIT即时编译及其优化

Oracle的Hotspot JVM实现,是目前OpenJDK使用的主流JVM,它采用解释与编译混合执行的模式,其JIT技术采用分层编译,极大地提升了Java的执行速度。

Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行的特别频繁时,会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器,不是Java虚拟机内必须的部分)。

要了解HotSpot虚拟机内的即时编译器的运作过程,要解决几个问题:

一、为何HotSpot虚拟机要使用解释器和编译器并存的架构?
二、为何HotSpot虚拟机要实现两个不同的即时编译器?
三、程序何时使用解释器执行?何时使用编译器执行?
四、哪些程序代码会被编译成本地代码?如何编译?
五、如何从外部观察即时编译器的编译过程和编译结果?

解释器和编译器

二者的优势:当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,越来越多的代码被编译成本地代码,可以获取更好的执行效率。解释器比较节约内存,编译器的效率比较高。解释器还可以作为编译器激进优化操作的“逃生门”,当激进优化的假设不成立,就退回到解释状态继续执行。

HotSpot内置了两个编译器,分别是Client Compiler和Server Complier,或者简称为C1和C2编译器。同时用到两个编译器的分层编译(Tiered Compilation)策略,使用后,C1和C2同时工作,有些代码可能多次编译,用C1获取更高的编译速度,C2获取更好的编译质量:

第0层,程序解释执行,解释器不开启性能监视功能(Profiling),可触发第1层编译。
第1层,也称为C1编译,将字节码编译成本地代码,进行简单、可靠的优化,若有必要将加入性能监控的逻辑。
第2层,也称为C2编译,也是将字节码编译成为本地代码,但是会启动一些编译耗时较长的优化,甚至会根据性能监控进行一些不可靠的激进优化。

编译对象和触发条件

在运行过程中被即时编译器编译的“热点代码”有两类,即:

一、被多次调用的方法。
二、被多次执行的循环体。

对第一种情况,由于是方法调用触发的编译,因此编译器会以整个方法作为编译对象,即标准的JIT编译方式。后一种,虽然是循环体触发的编译动作,但编译器依然按照整个方法(而不是单独的循环体)作为编译对象。这种编译方式称为栈上替换(On Stack Replacement,简称为OSR编译)。

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot Spot Detection),目前有两种方法:

一、基于采样的热点探测:采用这样的方法的虚拟机会周期性的检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。其好处就是实现简单、高效,还可以很容易的获取方法调用关系(将调用栈展开即可),缺点是很难精确的确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响。
二、基于计数器的热点探测:为每一个方法(甚至是代码块)建立计数器,统计方法的执行次数,超过一定的阈值就认为是“热点方法”。缺点是实现起来更麻烦,需要为每个方法建立并维护计数器,并且不能直接获取到方法的调用关系,优点是它的统计结果相对来说更加精确和严谨。
HotSpot虚拟机使用第二种,它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,用于统计一个方法中循环体代码执行的次数)。

执行有三种模式:

一、解释执行。

二、JIT编译执行。

三、JIT编译与解释混合执行(主流JVM默认执行模式)。

混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。随着时间推进,JVM通过热点代码统计分析,识别高频的方法调用、循环体、公共模块等,基于强大的JIT动态编译技术,将热点代码转换成机器码,直接交给CPU执行。JIT的作用是将Java字节码动态地编译成可以直接发送给处理器指令执行的机器码。

注意事项

注意解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态可以承受的负载要大于冷机状态(刚启动时),如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无法承载流量而假死。在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,建议每个批次的机器数至多占到整个集群的1/8。

案例

某程序员在发布平台进行分批发布,在输入发布总批数时,误填成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机,此故障说明了JIT的存在。