Spring boot starter that provides auto-configuration for Netty clients and servers.
The minimal project could look like this:
Maven's pom.xml:
<dependencies>
...
<dependency>
<groupId>com.xxlabaza.utils</groupId>
<artifactId>netty-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
...
</dependencies>
Your single Main.java with an echo server handler:
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Main {
public static void main (String[] args) {
SpringApplication.run(Main.class, args);
}
@Bean
ChannelHandler echoChannelHandler () {
return new EchoChannelHandler();
}
@Sharable
class EchoChannelHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead (ChannelHandlerContext context, Object object) throws Exception {
context.writeAndFlush(object);
}
}
}
And application.yml (see NettyClientProperties and NettyServerProperties for the full properties lists):
spring:
application.name: echo-server
xxlabaza.netty.server:
# the value could be just port, like here,
# or a complete string like localhost:9090
bind: 9090
So, the code above creates and runs the Echo server on port 9090. We can see it during the start process (pay attention on NettyServersApplicationListener and NettyServer log records):
> mvn package; java -jar target/my-netty-server-1.0.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.5.RELEASE)
2020-03-05 21:38:09.776 INFO 19438 --- [ main] com.xxlabaza.test.my.netty.server.Main : Starting Main on IB-ALABAZIN-M with PID 19438 (/Users/alabazin/jp/my-netty-server/target/my-netty-server-1.0.0.jar started by alabazin in /Users/alabazin/jp/my-netty-server)
2020-03-05 21:38:09.783 INFO 19438 --- [ main] com.xxlabaza.test.my.netty.server.Main : No active profile set, falling back to default profiles: default
2020-03-05 21:38:10.466 INFO 19438 --- [ main] .n.c.s.l.NettyServersApplicationListener : starting NettyServer(bind=0.0.0.0/0.0.0.0:9090)
2020-03-05 21:38:10.519 INFO 19438 --- [ main] com.xxlabaza.utils.netty.NettyServer : started NettyServer(bind=0.0.0.0/0.0.0.0:9090)
2020-03-05 21:38:10.527 INFO 19438 --- [ main] com.xxlabaza.test.my.netty.server.Main : Started Main in 2.336 seconds (JVM running for 3.144)
Yep, that was easy, but let's imagine that we need a second ChannelHandler
for, let's say, logging. The following code illustrates that situation:
Main.java:
import static io.netty.handler.logging.LogLevel.INFO;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.handler.logging.LoggingHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
@SpringBootApplication
public class Main {
public static void main (String[] args) {
SpringApplication.run(Main.class, args);
}
@Bean
@Order(0)
ChannelHandler loggingChannelHandler () {
return new LoggingHandler(INFO);
}
@Bean
@Order(1)
ChannelHandler echoChannelHandler () {
return new EchoChannelHandler();
}
@Sharable
class EchoChannelHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead (ChannelHandlerContext context, Object object) throws Exception {
context.writeAndFlush(object);
}
}
}
Note, I used the @Order annotation on top of the beans, because I wanted to specify their order in a channel initializer pipeline.
After the starting and receiving a message we can see the following picture:
> mvn package; java -jar target/my-netty-server-1.0.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.5.RELEASE)
2020-03-05 21:44:48.600 INFO 19874 --- [ main] com.xxlabaza.test.my.netty.server.Main : Starting Main on IB-ALABAZIN-M with PID 19874 (/Users/alabazin/jp/my-netty-server/target/my-netty-server-1.0.0.jar started by alabazin in /Users/alabazin/jp/my-netty-server)
2020-03-05 21:44:48.604 INFO 19874 --- [ main] com.xxlabaza.test.my.netty.server.Main : No active profile set, falling back to default profiles: default
2020-03-05 21:44:49.314 INFO 19874 --- [ main] .n.c.s.l.NettyServersApplicationListener : starting NettyServer(bind=0.0.0.0/0.0.0.0:9090)
2020-03-05 21:44:49.388 INFO 19874 --- [ main] com.xxlabaza.utils.netty.NettyServer : started NettyServer(bind=0.0.0.0/0.0.0.0:9090)
2020-03-05 21:44:49.400 INFO 19874 --- [ main] com.xxlabaza.test.my.netty.server.Main : Started Main in 1.37 seconds (JVM running for 2.277)
2020-03-05 21:46:42.707 INFO 19874 --- [orkers-pool-3-2] io.netty.handler.logging.LoggingHandler : [id: 0x1fa457ae, L:/127.0.0.1:9090 - R:/127.0.0.1:65422] REGISTERED
2020-03-05 21:46:42.708 INFO 19874 --- [orkers-pool-3-2] io.netty.handler.logging.LoggingHandler : [id: 0x1fa457ae, L:/127.0.0.1:9090 - R:/127.0.0.1:65422] ACTIVE
2020-03-05 21:46:44.280 INFO 19874 --- [orkers-pool-3-2] io.netty.handler.logging.LoggingHandler : [id: 0x1fa457ae, L:/127.0.0.1:9090 - R:/127.0.0.1:65422] READ: 6B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 70 6f 70 61 0d 0a |popa.. |
+--------+-------------------------------------------------+----------------+
2020-03-05 21:46:44.281 INFO 19874 --- [orkers-pool-3-2] io.netty.handler.logging.LoggingHandler : [id: 0x1fa457ae, L:/127.0.0.1:9090 - R:/127.0.0.1:65422] WRITE: 6B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 70 6f 70 61 0d 0a |popa.. |
+--------+-------------------------------------------------+----------------+
2020-03-05 21:46:44.281 INFO 19874 --- [orkers-pool-3-2] io.netty.handler.logging.LoggingHandler : [id: 0x1fa457ae, L:/127.0.0.1:9090 - R:/127.0.0.1:65422] FLUSH
2020-03-05 21:46:44.283 INFO 19874 --- [orkers-pool-3-2] io.netty.handler.logging.LoggingHandler : [id: 0x1fa457ae, L:/127.0.0.1:9090 - R:/127.0.0.1:65422] READ COMPLETE
Now you can see the logs of the inbound and outbound messages in the console.
Unfortunately, not all ChannelHandler
s are stateless and can have a @Share annotation on top of the class. Sometimes we need a state inside a ChannelHandler
, and we would like to have separate instances for each new connection. For that purposes, you can use ChannelHandlerProvider
, which supplies the new instances of a specified ChannelHandler
, like in this example:
...
import static java.lang.Integer.MAX_VALUE;
import com.xxlabaza.utils.netty.handler.ChannelHandlerProvider;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import java.util.function.Supplier;
...
@Bean
@Order(2)
ChannelHandler lengthFieldBasedFrameDecoder () {
Supplier<ChannelHandler> supplier = () -> new LengthFieldBasedFrameDecoder(MAX_VALUE, 0, 2);
return ChannelHandlerProvider.from(supplier);
}
...
In the code above, we created a ChannelHandler
supplier and put it in the ChannelHandlerProvider
instance, which will provide the new instances for each new connection in our channel initializer.
Ok. Looks awesome, but what about a client? Let's add one:
In application.yml I added a section with the client's settings:
spring:
application.name: echo-server
xxlabaza.netty:
server:
bind: 9090
client:
# the value, same as server.bindm could be just port, like here,
# or a complete string like localhost:9090
connect: 9090
Main.java now contains two ChannelInitializer
s declarations (NettyClientChannelInitializer for client and NettyServerChannelInitializer for server) to determine which Spring context's beans to use for the client and which for the server:
import static io.netty.handler.logging.LogLevel.INFO;
import com.xxlabaza.utils.netty.config.client.builder.NettyClientChannelInitializer;
import com.xxlabaza.utils.netty.config.server.builder.NettyServerChannelInitializer;
import com.xxlabaza.utils.netty.handler.ChannelHandlerInitializerPipeline;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.handler.logging.LoggingHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Main {
public static void main (String[] args) {
SpringApplication.run(Main.class, args);
}
@Bean
NettyClientChannelInitializer clientChannelInitializer () {
return ChannelHandlerInitializerPipeline.clientPipeline(
emptyChannelHandler()
);
}
@Bean
NettyServerChannelInitializer serverChannelInitializer () {
return ChannelHandlerInitializerPipeline.serverPipeline(
loggingChannelHandler(),
echoChannelHandler()
);
}
@Bean
ChannelHandler loggingChannelHandler () {
return new LoggingHandler(INFO);
}
@Bean
ChannelHandler echoChannelHandler () {
return new EchoChannelHandler();
}
@Bean
ChannelHandler emptyChannelHandler () {
return new EmptyChannelHandler();
}
@Sharable
class EchoChannelHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead (ChannelHandlerContext context, Object object) throws Exception {
context.writeAndFlush(object);
}
}
@Sharable
class EmptyChannelHandler extends ChannelInboundHandlerAdapter {
}
}
The client has a single EmptyChannelHandler
(doesn't do anything) and connects to the server automatically (thanks for xxlabaza.netty.client.auto-connect property in true
, by default). We can see that behavior with the help of the loggingChannelHandler
:
> mvn package; java -jar target/my-netty-server-1.0.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.5.RELEASE)
2020-03-05 23:17:03.901 INFO 26205 --- [ main] com.xxlabaza.test.my.netty.server.Main : Starting Main on IB-ALABAZIN-M with PID 26205 (/Users/alabazin/jp/my-netty-server/target/my-netty-server-1.0.0.jar started by alabazin in /Users/alabazin/jp/my-netty-server)
2020-03-05 23:17:03.904 INFO 26205 --- [ main] com.xxlabaza.test.my.netty.server.Main : No active profile set, falling back to default profiles: default
2020-03-05 23:17:04.677 INFO 26205 --- [ main] .n.c.s.l.NettyServersApplicationListener : starting NettyServer(bind=0.0.0.0/0.0.0.0:9090)
2020-03-05 23:17:04.751 INFO 26205 --- [ main] com.xxlabaza.utils.netty.NettyServer : started NettyServer(bind=0.0.0.0/0.0.0.0:9090)
2020-03-05 23:17:04.758 INFO 26205 --- [ main] u.n.c.c.l.NettyClientApplicationListener : starting NettyClient(remote=0.0.0.0/0.0.0.0:9090)
2020-03-05 23:17:04.773 INFO 26205 --- [ main] com.xxlabaza.utils.netty.NettyClient : started NettyClient(remote=0.0.0.0/0.0.0.0:9090)
2020-03-05 23:17:04.782 INFO 26205 --- [ main] com.xxlabaza.test.my.netty.server.Main : Started Main in 1.408 seconds (JVM running for 2.606)
2020-03-05 23:17:04.799 INFO 26205 --- [ pool-1-1] io.netty.handler.logging.LoggingHandler : [id: 0x2ab2a057, L:/10.116.52.78:9090 - R:/10.116.52.78:50381] REGISTERED
2020-03-05 23:17:04.800 INFO 26205 --- [ pool-1-1] io.netty.handler.logging.LoggingHandler : [id: 0x2ab2a057, L:/10.116.52.78:9090 - R:/10.116.52.78:50381] ACTIVE
Hey, but what if I want to have several clients or servers in the same Spring application context? Well, there are two special config classes for that: NettyServerConfig
and NettyClientConfig
. You can easily use them for creating so many clients and servers as you want:
Let's add one more client and server to the application.yml:
spring:
application.name: echo-server
xxlabaza.netty:
server:
bind: 9990
client:
connect: 9990
my.long.prefix.server:
bind: 9901
my.long.prefix.client:
connect: 9901
auto-connect: false
Then, we add according configurations to Main.java:
import static com.xxlabaza.utils.netty.handler.ChannelHandlerInitializerPipeline.clientPipeline;
import static com.xxlabaza.utils.netty.handler.ChannelHandlerInitializerPipeline.pipelineOf;
import static com.xxlabaza.utils.netty.handler.ChannelHandlerInitializerPipeline.serverPipeline;
import static io.netty.handler.logging.LogLevel.INFO;
import com.xxlabaza.utils.netty.config.client.NettyClientConfig;
import com.xxlabaza.utils.netty.config.client.builder.NettyClientChannelInitializer;
import com.xxlabaza.utils.netty.config.server.NettyServerConfig;
import com.xxlabaza.utils.netty.config.server.builder.NettyServerChannelInitializer;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.handler.logging.LoggingHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Main {
public static void main (String[] args) {
SpringApplication.run(Main.class, args);
}
@Bean
NettyServerConfig secondServer () {
return NettyServerConfig.builder()
.propertiesPrefix("my.long.prefix.server")
.channelInitializer(pipelineOf(
echoChannelHandler()
))
.build();
}
@Bean
NettyClientConfig secondClient () {
return NettyClientConfig.builder()
.propertiesPrefix("my.long.prefix.client")
.build();
}
@Bean
NettyClientChannelInitializer clientChannelInitializer () {
return clientPipeline(
emptyChannelHandler()
);
}
@Bean
NettyServerChannelInitializer serverChannelInitializer () {
return serverPipeline(
loggingChannelHandler(),
echoChannelHandler()
);
}
@Bean
ChannelHandler loggingChannelHandler () {
return new LoggingHandler(INFO);
}
@Bean
ChannelHandler echoChannelHandler () {
return new EchoChannelHandler();
}
@Bean
ChannelHandler emptyChannelHandler () {
return new EmptyChannelHandler();
}
@Sharable
class EchoChannelHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead (ChannelHandlerContext context, Object object) throws Exception {
context.writeAndFlush(object);
}
}
@Sharable
class EmptyChannelHandler extends ChannelInboundHandlerAdapter {
}
}
What do we have here?
- The default client and server, because we specified the xxlabaza.netty. prefix in the application.yml;
- a secondServer instance, which takes its configuration from properties with the prefix my.long.prefix.server and has a manually instantiated channel initializer (if you would like, you can customize the other parts of the
NettyServerConfig
); - a secondClient bean. It builds from the properties with my.long.prefix.client prefix.
So, the two servers will automatically start with the application context, the default client too (see NettyClientProperties and NettyServerProperties default values accordingly), but the second client needs to be connected manually (NettyClient.connect
or NettyClient.send
):
> mvn package; java -jar target/my-netty-server-1.0.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.5.RELEASE)
2020-03-06 02:23:36.263 INFO 30531 --- [ main] com.xxlabaza.test.my.netty.server.Main : Starting Main on IB-ALABAZIN-M with PID 30531 (/Users/alabazin/jp/my-netty-server/target/my-netty-server-1.0.0.jar started by alabazin in /Users/alabazin/jp/my-netty-server)
2020-03-06 02:23:36.266 INFO 30531 --- [ main] com.xxlabaza.test.my.netty.server.Main : No active profile set, falling back to default profiles: default
2020-03-06 02:23:37.052 INFO 30531 --- [ main] .n.c.s.l.NettyServersApplicationListener : starting NettyServer(bind=0.0.0.0/0.0.0.0:9990)
2020-03-06 02:23:37.103 INFO 30531 --- [ main] com.xxlabaza.utils.netty.NettyServer : started NettyServer(bind=0.0.0.0/0.0.0.0:9990)
2020-03-06 02:23:37.103 INFO 30531 --- [ main] .n.c.s.l.NettyServersApplicationListener : starting NettyServer(bind=0.0.0.0/0.0.0.0:9901)
2020-03-06 02:23:37.104 INFO 30531 --- [ main] com.xxlabaza.utils.netty.NettyServer : started NettyServer(bind=0.0.0.0/0.0.0.0:9901)
2020-03-06 02:23:37.107 INFO 30531 --- [ main] u.n.c.c.l.NettyClientApplicationListener : starting NettyClient(remote=0.0.0.0/0.0.0.0:9990)
2020-03-06 02:23:37.128 INFO 30531 --- [ main] com.xxlabaza.utils.netty.NettyClient : started NettyClient(remote=0.0.0.0/0.0.0.0:9990)
2020-03-06 02:23:37.133 INFO 30531 --- [ main] com.xxlabaza.test.my.netty.server.Main : Started Main in 1.263 seconds (JVM running for 2.339)
2020-03-06 02:23:37.143 INFO 30531 --- [orkers-pool-7-2] io.netty.handler.logging.LoggingHandler : [id: 0x1442fa3d, L:/192.168.236.7:9990 - R:/192.168.236.7:51918] REGISTERED
2020-03-06 02:23:37.144 INFO 30531 --- [orkers-pool-7-2] io.netty.handler.logging.LoggingHandler : [id: 0x1442fa3d, L:/192.168.236.7:9990 - R:/192.168.236.7:51918] ACTIVE
Also, you may want to just configure a Bootstrap
or a ServerBootstrap
instance (add additional options or, for example, use a shared ByteBufAllocator
) before using them, so for that, you can use NettyClientBootstrapConfigurer or NettyServerBootstrapConfigurer interfaces like this:
import com.xxlabaza.utils.netty.config.server.builder.NettyServerBootstrapConfigurer;
import io.netty.bootstrap.ServerBootstrap;
import org.springframework.stereotype.Component;
@Component
class MyCustomServerBootstrapConfigurer implements NettyServerBootstrapConfigurer {
@Override
public void configure (ServerBootstrap serverBootstrap) {
// do your awesome configuration here
}
}
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
For building the project you need only a Java compiler.
IMPORTANT: the project requires Java version starting from 8
And, of course, you need to clone the project from GitHub:
$> git clone https://github.com/xxlabaza/netty-spring-boot-starter
$> cd netty-spring-boot-starter
For building routine automation, I am using maven.
To build the project, do the following:
$> ./mvnw compile
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.970 s
[INFO] Finished at: 2020-03-05T21:23:42+03:00
[INFO] ------------------------------------------------------------------------
To run the project's test, do the following:
$> ./mvnw test
...
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 25, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.383 s
[INFO] Finished at: 2020-03-05T21:24:42+03:00
[INFO] ------------------------------------------------------------------------
Also, if you do package
or install
goals, the tests launch automatically.
To deploy the project in Maven Central, use the following command:
$> ./mvnw \
-DskipAllTests=true \
-Dspotbugs.skip=true \
-Dpmd.skip=true \
-Dcheckstyle.skip \
-Dmaven.javadoc.skip=false \
--settings .settings.xml \
deploy -B
It maybe usefull to import gpg
's secret keys and ownertrust from somewhere:
$> echo "${GPG_SECRET_KEYS}" | base64 --decode | "${GPG_EXECUTABLE}" --batch --passphrase "${GPG_PASSPHRASE}" --import
...
$> echo "${GPG_OWNERTRUST}" | base64 --decode | "${GPG_EXECUTABLE}" --batch --passphrase "${GPG_PASSPHRASE}" --import-ownertrust
...
-
Java - is a systems and applications programming language
-
Lombok - is a java library that spicing up your java
-
Junit - is a simple framework to write repeatable tests
-
AssertJ - AssertJ provides a rich set of assertions, truly helpful error messages, improves test code readability
-
Maven - is a software project management and comprehension tool
To see what has changed in recent versions of the project, see the changelog file.
Please read contributing file for details on my code of conduct, and the process for submitting pull requests to me.
We use SemVer for versioning. For the versions available, see the tags on this repository.
- Artem Labazin - creator and the main developer
This project is licensed under the Apache License 2.0 License - see the license file for details