问题描述:在Tomcat中热部署Springboot应用时,报如下错误:
java.net.BindException: Address already in use
at sun.nio.ch.Net.bind0(Native Method)
at sun.nio.ch.Net.bind(Net.java:444)
at sun.nio.ch.Net.bind(Net.java:436)
at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:214)
at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:74)
问题原因:应用销毁时,未关闭netty server和释放端口。
现有解决办法:
在网上百度来的全是通过kill -9 干掉进程的方式,这个方法过于简单粗暴,且大大增加了运维成本。
做为一个优秀的码农+运维(自加鸡腿一根:)怎么能允许这种事情发生呢?
让我们来优雅的解决这个问题。↓
解决思路:监控springboot容器销毁事件,在此事件中先关闭netty。然后再加载时就不会有端口冲突了。
首先,将ServerChannel和EventLoopGroup定义为全局静态变量,在接收到关闭事件时,关闭channel,EventLoopGroup即可。
话不多说,直接上代码:
一,NettyServer:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class NettyServerMain {
private static EventLoopGroup bossGroup = null;
private static EventLoopGroup workGroup = null;
private static Channel serverChannel = null;
public static void main(String[] args) {
// write your code here
}
public void bind(String host, int port) throws Exception {
log.info("Netty local host = " + host + " and port = " + port);
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup);
b.channel(NioServerSocketChannel.class);
b.option(ChannelOption.SO_BACKLOG, 1024);
b.option(ChannelOption.SO_REUSEADDR, true);
// b.option(ChannelOption.SO_KEEPALIVE, true);
b.childHandler(new ChildChannelHandler());
// 绑定端口
ChannelFuture channelFuture = b.bind(host, port).sync();
// 将ServerChannel保存下来
serverChannel = channelFuture.channel();
// 等待服务端监听端口关闭
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.error("Netty bind error");
e.printStackTrace();
} finally {
// 优雅的退出
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
/**
* 关闭netty server
*/
public void close() {
if (serverChannel != null) {
try {
serverChannel.close();
serverChannel = null;
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
} catch (Exception e) {
log.error("Close netty server failed.");
e.printStackTrace();
}
}
}
}
二,SpringBoot监听:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class MessageReceiver implements ApplicationListener<ContextRefreshedEvent> {
private ExecutorService webSocketSinglePool;
private volatile AtomicBoolean isInit = new AtomicBoolean(false);
private NettyServerMain nettyServerMain = null;
@PostConstruct
public void setup() {
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("webSocketSinglePool-%d").build();
webSocketSinglePool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
log.info("webSocketSinglePool init.");
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.info("=================onApplicationEvent: ContextRefreshedEvent 进入");
// 防止重复触发
if (!isInit.compareAndSet(false, true)) {
log.info("=================onApplicationEvent: ContextRefreshedEvent 是再次触发,忽略");
return;
}
if (event.getApplicationContext().getParent() == null) {
runNettyServer();
}
}
private void runNettyServer() {
log.info("=================runNettyServer ");
webSocketSinglePool.execute(() -> {
try {
runNetty();
} catch (Exception e) {
log.error("NettyServer listen and serve error.", e);
}
});
}
/**
* 关闭Netty Server
*/
private void destoryNettyServer() {
if (nettyServerMain != null) {
nettyServerMain.close();
nettyServerMain = null;
log.info("netty destroyed.");
}
}
@PreDestroy
public void cleanup() {
destoryNettyServer();
webSocketSinglePool.shutdown();
log.info("webSocketSinglePool destroyed.");
}
private void runNetty() {
try {
log.info("=================start to get Netty local host and port!");
nettyServerMain = new NettyServerMain();
nettyServerMain.bind("127.0.0.1", 8765);
} catch (Exception e) {
e.printStackTrace();
}
}
}
补充:
遗留问题,在热部署完成之后,虽然不再报端口占用的问题,但在Tomcat log中会看到有几个EventLoopGroup线程未关闭,会导致内存泄漏的问题。有没有这方面的大神,来一起解决一下 :)