Hutool Sftp 负载均衡踩坑记

踩坑背景

最近有个项目使用到了Sftp,部署的时候发现了一个奇怪的现象,当sftp通过ip直接连接的时候,程序是一切正常的,在使用运维给的LB地址时,老是会出现下面这个报错

cn.hutool.extra.ssh.JschRuntimeException: JschException: Session.connect: java.net.SocketException: Connection reset
    at cn.hutool.extra.ssh.JschUtil.openSession(JschUtil.java:116)
    at cn.hutool.extra.ssh.JschUtil.openSession(JschUtil.java:97)
    at cn.hutool.extra.ssh.JschSessionPool.lambda$getSession$64b21fc$1(JschSessionPool.java:45)
    at cn.hutool.core.lang.SimpleCache.get(SimpleCache.java:112)
    at cn.hutool.extra.ssh.JschUtil.getSession(JschUtil.java:55)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:166)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:93)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:80)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:70)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:56)
    at 

报错的同时,在服务器上尝试使用命令行LB地址连接Sftp却是一切正常的!!!同时经过排查发现,当程序刚启动尝试第一次连接的时候并没有出现这个奇怪的问题,正当我看这个奇怪的堆栈百思不得其解的时候,突然注意到了一个奇怪的打印

at cn.hutool.core.lang.SimpleCache.get(SimpleCache.java:112)

经过排查发现,hutool的Sftp工具类是默认开启了一个JschSessionPool的连接池,默认的直接使用 new Sftp(…) 或者 JschUtil.getSession(…) 都会把新建立的连接放到 JschSessionPool 这个连接池中,而恰恰就是这个连接池导致的问题!!!

修改方案

如果你也是使用的hutool Sftp工具类且涉及一些比较复杂的网络场景,建议跳过Hutool的Sftp自带的连接池逻辑,改成直接使用 JschUtil.createSession(…) 手动建立连接并且手动释放,这种场景反而更好做一些细粒度的控制

教训总结

在使用一个工具类前,必须要大致了解其底层实现逻辑以及相关issue,避免老是踩前辈们踩过的坑!!!!!!!!!!

Java 线程池 阻塞提交任务

场景

提交任务由单线程提交到线程池多线程处理,在线程池达到处理上线时可以在提交的线程阻塞等待。

方案

1.最常见的方案就是直接设置线程池的拒绝策略为 CallerRunsPolicy,当触发拒绝策略时,会将该任务直接在提交任务所在的线程直接运行该任务。这个方案有个小问题,当如果提交的任务负载很重导致提交任务的线程长时间阻塞,就会造成线程池的饥饿。

2.较可行但是有点恶心的方案,自定义阻塞策略在触发拒绝时获取任务队列阻塞提交。

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    try {
        if (!executor.isShutdown()) {
            executor.getQueue().put(r);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RejectedExecutionException("interrupted", e);
    }
}

这个方案也有问题:

如果你只有一个线程提交任务,而且任务的执行时间不可控,这个方案是我找到的算靠谱的了。

千万别这样用,你永远不知道别人会咋用你的线程池,一不小心就上当了!!!

3.较一般可行不那么恶心方案,自定义任务队列,直接让offer、and方法也阻塞

public class LimitedQueue<E> extends LinkedBlockingQueue<E> 
{
    public LimitedQueue(int maxSize)
    {
        super(maxSize);
    }

    @Override
    public boolean offer(E e)
    {
        // turn offer() and add() into a blocking calls (unless interrupted)
        try {
            put(e);
            return true;
        } catch(InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
        return false;
    }

}

这个方案其实挺完美的,但是唯一的问题就是线程池永远只会有coreSize个线程,在任务队列达到上限时就直接阻塞了=.=丧失了线程池的伸缩能力。

4.优雅比较可行的方案,自定义实现线程池,用信号量控制同时进入的线程,这个方案代码很完美,但是操蛋的是无法精确控制线程数量。

public class BoundedExecutor extends ThreadPoolExecutor{

    private final Semaphore semaphore;

    public BoundedExecutor(int bound) {
        super(bound, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        semaphore = new Semaphore(bound);
    }

    /**Submits task to execution pool, but blocks while number of running threads 
     * has reached the bound limit
     */
    public <T> Future<T> submitButBlockIfFull(final Callable<T> task) throws InterruptedException{

        semaphore.acquire();            
        return submit(task);                    
    }


    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);

        semaphore.release();
    }
}
//这个方案也类似 他们问题也是一样的,就是任务执行完后线程并不是立马可用的,但semaphore释放了
class BlockingExecutor implements Executor {

    final Semaphore semaphore;
    final Executor delegate;

    private BlockingExecutor(final int concurrentTasksLimit, final Executor delegate) {
        semaphore = new Semaphore(concurrentTasksLimit);
        this.delegate = delegate;
    }

    @Override
    public void execute(final Runnable command) {
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
            return;
        }

        final Runnable wrapped = () -> {
            try {
                command.run();
            } finally {
                semaphore.release();
            }
        };

        delegate.execute(wrapped);

    }
}

这个方案的问题就是

  • afterExecute这个方法不是线程执行完任务最后做的事情,也就是说线程执行完afterExecute还会要执行一些任务才能返回线程池,但是这个时候我们已经执行了semaphore.release(),任务进来以后发现没有线程可用又得创建一个线程!!
  • 这个方案只能实现coreSize==maxSize,如果你尝试将任务队列长度修改和信号量长度修改,你会发现由于问题1,你总会莫名其妙的就触发了拒绝策略了
  • 线程池里的线程总会比bound要多,而且如果你的任务很快完成(非常快那种),有可能创建非常多的线程。

Linux上使用Selenium运行有头浏览器

通常我们服务器vps是没有安装gui显示的,但是为了避免使用无头浏览器(很容易被识别),我们可以用Xvfb。

Xvfb

Xvfb是一个实现 X11 显示服务器协议的显示服务器。该程序将允许您以“无头”模式运行任何应用程序。基本上,这个程序不会在物理屏幕上输出 GUI,而是创建一个虚拟帧缓冲区并在那里“显示”UI。

安装

sudo apt-get install xvfb

使用

先启动一个xvfb服务 指定id,然后我们启动项目时也指定这个id运行

 
export DISPLAY=:7 指定变量
Xvfb -ac $DISPLAY -screen 0 1280x1024x8 //比如这样 启动一个服务 指定虚拟id是7  
//然后直接运行就可以 会根据环境变量找到xvfb服务的
 java -jar xxxx.jar

可以尝试这个脚本使用 https://gist.github.com/tsl0922/ab8d370a85653c4354ad
最好的方式就是启动一个xvfb服务 这样就可以直接使用有头浏览器了
创建 一个脚本 xvfb.sh 内容如下
#!/bin/bash

XVFB=/usr/bin/Xvfb
XVFBARGS="$DISPLAY -ac -screen 0 1024x768x16"
PIDFILE=${HOME}/xvfb_${DISPLAY:1}.pid
case "$1" in
  start)
    echo -n "Starting virtual X frame buffer: Xvfb"
    /sbin/start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile --background --exec $XVFB -- $XVFBARGS
    echo "."
    ;;
  stop)
    echo -n "Stopping virtual X frame buffer: Xvfb"
    /sbin/start-stop-daemon --stop --quiet --pidfile $PIDFILE
    echo "."
    ;;
  restart)
    $0 stop
    $0 start
    ;;
  *)
  echo "Usage: /etc/init.d/xvfb {start|stop|restart}"
  exit 1
esac
exit 0

启动方式 
bash xvfb.sh start

SpringBoot使用Mybatis逆向工程小记

1.新建立SpringBoot项目(当然在原有的项目添加也行),添加依赖和build插件,主要是下面这一段。

        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.7</version>
            <configuration>
                <configurationFile>src/main/resources/config.xml</configurationFile>
                <overwrite>true</overwrite>
                <includeCompileDependencies>true</includeCompileDependencies>
            </configuration>
        </plugin>

这是我的新springboot项目添加后的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.4</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.mybatis.generator</groupId>
				<artifactId>mybatis-generator-maven-plugin</artifactId>
				<version>1.3.7</version>
				<configuration>
					<configurationFile>src/main/resources/config.xml</configurationFile>
					<overwrite>true</overwrite>
					<includeCompileDependencies>true</includeCompileDependencies>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

2.配置模板,来自https://juejin.cn/post/6844903982582743048这里有超详细的教程

<?xml version="1.0" encoding="UTF-8" ?>
<!--mybatis的代码生成器相关配置-->
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!-- 引入配置文件 -->
    <properties resource="application-dev.properties"/>

    <!-- 一个数据库一个context,context的子元素必须按照它给出的顺序
        property*,plugin*,commentGenerator?,jdbcConnection,javaTypeResolver?,
        javaModelGenerator,sqlMapGenerator?,javaClientGenerator?,table+
    -->
    <context id="myContext" targetRuntime="MyBatis3" defaultModelType="flat">

        <!-- 这个插件给生成的Java模型对象增加了equals和hashCode方法 -->
        <!--<plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/>-->

        <!-- 注释 -->
        <commentGenerator>
            <!-- 是否不生成注释 -->
            <property name="suppressAllComments" value="true"/>
            <!-- 不希望生成的注释中包含时间戳 -->
            <!--<property name="suppressDate" value="true"/>-->
            <!-- 添加 db 表中字段的注释,只有suppressAllComments为false时才生效-->
            <!--<property name="addRemarkComments" value="true"/>-->
        </commentGenerator>


        <!-- jdbc连接 -->
        <jdbcConnection driverClass="${spring.datasource.driverClassName}"
                        connectionURL="${spring.datasource.url}"
                        userId="${spring.datasource.username}"
                        password="${spring.datasource.password}">
            <!--高版本的 mysql-connector-java 需要设置 nullCatalogMeansCurrent=true-->
            <property name="nullCatalogMeansCurrent" value="true"/>
        </jdbcConnection>

        <!-- 类型转换 -->
        <javaTypeResolver>
            <!--是否使用bigDecimal,默认false。
                false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer
                true,把JDBC DECIMAL 和 NUMERIC 类型解析为java.math.BigDecimal-->
            <property name="forceBigDecimals" value="true"/>
            <!--默认false
                false,将所有 JDBC 的时间类型解析为 java.util.Date
                true,将 JDBC 的时间类型按如下规则解析
                    DATE	                -> java.time.LocalDate
                    TIME	                -> java.time.LocalTime
                    TIMESTAMP               -> java.time.LocalDateTime
                    TIME_WITH_TIMEZONE  	-> java.time.OffsetTime
                    TIMESTAMP_WITH_TIMEZONE	-> java.time.OffsetDateTime
                -->
            <!--<property name="useJSR310Types" value="false"/>-->
        </javaTypeResolver>

        <!-- 生成实体类地址 -->
        <javaModelGenerator targetPackage="com.wqlm.boot.user.po" targetProject="src/main/java">
            <!-- 是否让 schema 作为包的后缀,默认为false -->
            <!--<property name="enableSubPackages" value="false"/>-->
            <!-- 是否针对string类型的字段在set方法中进行修剪,默认false -->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>


        <!-- 生成Mapper.xml文件 -->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <!--<property name="enableSubPackages" value="false"/>-->
        </sqlMapGenerator>

        <!-- 生成 XxxMapper.java 接口-->
        <javaClientGenerator targetPackage="com.wqlm.boot.user.dao" targetProject="src/main/java" type="XMLMAPPER">
            <!--<property name="enableSubPackages" value="false"/>-->
        </javaClientGenerator>


        <!-- schema为数据库名,oracle需要配置,mysql不需要配置。
             tableName为对应的数据库表名
             domainObjectName 是要生成的实体类名(可以不指定,默认按帕斯卡命名法将表名转换成类名)
             enableXXXByExample 默认为 true, 为 true 会生成一个对应Example帮助类,帮助你进行条件查询,不想要可以设为false
             -->
        <table schema="" tableName="user" domainObjectName="User"
               enableCountByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
               enableUpdateByExample="false" selectByExampleQueryId="false">
            <!--是否使用实际列名,默认为false-->
            <!--<property name="useActualColumnNames" value="false" />-->
        </table>
    </context>
</generatorConfiguration>

3.运行

命令行执行 mvn mybatis-generator:generate -e 即可运行,如果是idea的话可以直接在maven插件中双击该插件运行.

如果提示找不到jdbc对应的驱动类,请检查是否将mysql-connector的scope设置为runtime了

两数相加

问题描述

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

本彩笔的解法 纯粹的按部就班 浪费很多内存空间 唯一的优点就是速度还行 感觉有很多三元运算符是没必要的,以后再看看。

时间复杂度:O(max(m, n))

空间复杂度: O(max(m,n))

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode tempNode = new ListNode(0);
        ListNode firstNode = tempNode;
        int nextPlus = 0;
        while(!(l1 == null && l2 == null)){
        	int sum = ((l1==null)?0:(l1.val))+((l2==null)?0:l2.val) + nextPlus;
            nextPlus = 0;
        	if (sum >= 10) {
        		nextPlus = sum/10;
        		sum -= nextPlus*10;
        	}
        	tempNode.next = new ListNode(sum);
        	tempNode = tempNode.next;
        	l1 = l1==null? null : l1.next;
        	l2 = l2==null? null : l2.next;
        }
        if (nextPlus > 0) {
        	tempNode.next = new ListNode(nextPlus);
        }
        return firstNode.next;
    }
}

大佬们的解法

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode pre = new ListNode(0);
        ListNode cur = pre;
        int carry = 0;
        while(l1 != null || l2 != null) {
            int x = l1 == null ? 0 : l1.val;
            int y = l2 == null ? 0 : l2.val;
            int sum = x + y + carry;
            
            carry = sum / 10;
            sum = sum % 10;
            cur.next = new ListNode(sum);

            cur = cur.next;
            if(l1 != null)
                l1 = l1.next;
            if(l2 != null)
                l2 = l2.next;
        }
        if(carry == 1) {
            cur.next = new ListNode(carry);
        }
        return pre.next;
    }
}

作者:guanpengchn
链接:https://leetcode-cn.com/problems/add-two-numbers/solution/hua-jie-suan-fa-2-liang-shu-xiang-jia-by-guanpengc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Java流笔记之对象序列化

java.io.Serializable

java.io.Externalizable

序列化机制可以将实现序列化的对象转换成字节序列,使得对象能脱离java程序存在.如果要使一个对象是可序列化的,需要实现Serializable接口或者Externalizable接口之一.

使用Serializable标记接口实现对象的序列化很简单,只需要目标对象类实现Serializable接口即可,无需实现其方法.如果希望让对象的一些实例变量不被序列化,可以使用transient修饰符修饰.

1.将student对象序列化并储存在磁盘中并读取还原成对象

import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.ClassNotFoundException;

public  class SerializeTest{
	public static void main(String[] args) {
		try(
			ObjectOutputStream oop = new ObjectOutputStream(new FileOutputStream("log.txt"));
			ObjectInputStream oip = new ObjectInputStream(new FileInputStream("log.txt"));
		){
			Student s1 = new Student(3,"李华",1998);
			oop.writeObject(s1);
			Student s = (Student)oip.readObject();
			System.out.println(s.age + "  " + s.name + " " + s.year);
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}
class Student implements Serializable{
	int age;
	transient String name;
	int year;
	Student(int age, String name, int year){
		this.age = age;
		this.name = name;
		this.year = year;
	}
}
运行结果:(name用transient修饰,并未被序列化写入到磁盘)
3  null 1998
[Finished in 0.9s]

使用transient修饰的实例成员会被排除在序列化机制外,导致在反序列化时无法获得该实例成员的值.除了transient修饰符之外,Java还提供了自定义序列化机制,通过自动序列化机制,可以使程序自定义控制序列化流程.自定义序列化包含三个方法:

private void writeObject(ObjectOutputStream out) //负责写入对象 默认会调用out.defaultWriteObject()方法
private void readObject(ObjectInputStream in) //负责读取恢复对象 默认会调用in.defaultReadObject()方法
private void readObjectNoDate() //当序列流不完整时,负责初始化反序列化对象

 **注意**

  1. 如果使用序列化向一个文件写入了多个对象,那么读取时是按照写入顺序读取.
  2. 若可序列化对象拥有多个父类,那么这些父类要么有无参的构造器,要么也是序列化的,否则就会抛出InvalidClassException异常.另外,如果父类是不可序列化但有无参数的构造器,则父类中定义的成员变量值不会序列化到二进制流中.
  3. 若类的成员变量不是基本类型或者String,而是其他的引用类型,则这个引用类型也要是可序列化,否则该类无法序列化.(因为在序列化对象时,对象的所有成员变量也会被序列化).
  4. JAVA序列化机制采用了特殊的算法
    • 所有保存到磁盘的对象拥有一个特殊的序列化编号
    • 在程序进行序列化对象之前会检查该对象是否被序列化过,只有在该对象在(本次java虚拟机)未被序列化过时才会将该对象转换成字节系列.
    • 若该对象已经被序列化过,则程序会直接输出该对象的序列化编号.
    • **注意**在对可变对象进行序列化时一定要注意,如果在序列化可变对象后再改变该对象并且进行序列化,程序只会输出该对象改变前的序列化编号,并不会将改变过的对象重新序列化输出.

Java流笔记之RandomAccessFile

java.io.RandomAccessFile

RandomAccessFile是java的一个文件内容访问类,特点是支持”随机访问”,但只能对文件进行操作.
构造方法:

RandomAccessFile(File file/String fileName, String mode): mode为读写模式,分别有:
"r":只读,若写会抛出IOException
"rw":读写,若文件不存在则会创建
"rwd":读写,内容更新同步写到底层设备
"rws":读写,内容和元数据更新同步写到底层设备

常用方法:

long getFilePointer():返回文件中指针的位置
void seek(long index):将指针定位到index位置
void readXXX() void writeXXX() 读写

1.读取文件

import java.io.RandomAccessFile;
import java.io.IOException;
import java.io.FileNotFoundException;
public class RandomAccessFileTest{
	    public static void main(String[] args) {
	    try(
		    RandomAccessFile raf = new             
            RandomAccessFile("RandomAccessFileTest.java","rw");
		    ){
		    System.out.println("文件指针初始位置 :     "+raf.getFilePointer());
		    byte[] buffer = new byte[32];
		    int hasRead;
		    while ((hasRead = raf.read(buffer)) > 0) {
		    	    System.out.print(new String(buffer,0,hasRead));
		    }
	    }catch (IOException e) {
	    	e.printStackTrace();
	    }
    }
}

2.追加内容

import java.io.RandomAccessFile;
import java.io.IOException;
import java.io.FileNotFoundException;
public class RandomAccessFileTest{
	public static void main(String[] args) {
		try(
			RandomAccessFile raf = new RandomAccessFile("RandomAccessFileTest.java","rw");
		){
			System.out.println("文件字节数 : " + raf.length());
			raf.seek(raf.length());
			System.out.println("当前文件指针位置: " + raf.getFilePointer());
			raf.write("//这是添加的内容".getBytes());

		}catch (IOException e) {
			e.printStackTrace();
		}
	}
}//这是添加的内容

3.修改内容

**注意:直接修改文件指针内容会直接覆盖内容**
import java.io.RandomAccessFile;
import java.io.IOException;
import java.io.FileNotFoundException;//这是添加的新内容//这是添加的新内容
public class RandomAccessFileTest{
	public static void main(String[] args) {
		try(
			RandomAccessFile raf = new RandomAccessFile("RandomAccessFileTest.java","rw");
		){
			System.out.println(raf.getFilePointer());
			raf.seek(101);
			//将剩余内容缓存到content
			byte[] buffer = new byte[1024];
			int hasRead;
			String content = "";
			while ( (hasRead = raf.read(buffer)) > 0) {
				content += new String(buffer,0,hasRead);
			}
			System.out.println(content);
			raf.seek(101);
			//开始修改内容
			raf.write("//这是添加的新内容".getBytes());
			raf.write(content.getBytes());
		}catch (IOException e) {
			e.printStackTrace();
		}
	}
}//这是添加的内容