netty是Java世界中高并发的框架,号称单台物理机能够支撑100万并发连接,是Java世界中高性能并发的不二之选。不过,跟spring-boot相比,其开发有点偏于底层,写起来没有spring-boot那么爽快,开发的效率不高。
我的一个项目中,有高并发的需求,单靠spring-boot自带的tomcat无法满足性能上的要求。因此,我选择netty,作为底层框架。为了能够提高开发效率,我尝试将spring-boot引入我的开发中。仔细想想,其实整个spring都是建立在IOC和AOP之上的,所以只要我引入spring-boot这两个最基础的组件,那么势必整个spring-boot的组件都能为我所用。

不过spring-web不晓得该咋引入,其它的组件都不成问题。不过从我的角度看,netty本身就是网络框架,基本没必要在引入一个spring-web

我的项目中使用maven做整个工程管理,以下是pom.xml,我只保留了spring-boot和netty的部分:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.test</groupId>
    <artifactId>netty.spring-boot</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>cdn-router</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring.version>4.3.10.RELEASE</spring.version>
        <build-tool.version>1.0.0</build-tool.version>
        <cdn-opentsdb.version>1.0.0-SNAPSHOT</cdn-opentsdb.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.13.Final</version>
        </dependency>
        <!-- log配置:Log4j2 + Slf4j -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <!--<version>2.8.2</version>-->
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <!--<version>2.8.2</version>-->
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <!--<version>1.7.25</version>-->
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <!--<version>2.8.2</version>-->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j-impl</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.31</version>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <!-- Import dependency management from Spring Boot -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>1.5.6.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <finalName>${finalName}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.test.CDNRouterServer</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>
</project>

接下来,我实现一个http消息的handler,并将其设置为IOC的bean,让spring-boot去管理它。

/**
 * @Author Derek.
 * @Date 2017/7/18 9:24.
 */
@Component
@Scope("prototype")
public class OpsHttpMessageHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
        System.out.println("OK");
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Channel Test".getBytes("utf-8")));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

注意:其中的@Scope(“prototype”),因为netty会为每个eventloop重新生成一个handler的处理链,因此默认情况下,线程间不会共享handler。这样做的好处可以避免临界区访问的问题,从而避免了线程冲突和切换,提高并发率。而spring-boot中默认情况下,bean是单例模式的,也就是说这个bean只会有一个实例,而显然不适合与netty对handler的默认假设。因此,我们将bean改成原型模式,即@Scope(“prototype”)。在这个状态下,每次引用这个bean的时候,都会创建一个实例。当然,netty中也支持共享的handler,这时候需要在handler中注上@Sharable,此时就可以使用spring-boot默认的单例的bean了。具体见下面的代码:

/**
 * @Author Derek.
 * @Date 2017/5/18 10:58.
 */
@Component
@Sharable
public class Http2MessageHandler extends ChannelDuplexHandler {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("OK");
        super.channelRead(ctx, msg);
    }
}

接下来,我将使用上面创建的handler创建http server,同样也设置为spring的bean。

@Component(value = "opsHttpServer")
public class OpsHttpServer implements Runnable {
    @Autowired
    final private HttpProperty httpProperty = null;

    @Autowired
    private ServerBootstrapFactory factory;

    @Autowired
    final private ApplicationContext applicationContext = null;

    final  private EventExecutorGroup pool = new DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors() * 2);

    private static final int MAX_CONTENT_LENGTH = 1024 * 100;


    @Override
    public void run() {
        ServerBootstrap tcpBootStrap = factory.newServerBootstrap(0);
        tcpBootStrap.handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new HttpServerCodec())
                                .addLast(new HttpContentDecompressor())
                                .addLast(new ChunkedWriteHandler())
                                .addLast(new HttpContentCompressor())
                                .addLast(new HttpObjectAggregator(MAX_CONTENT_LENGTH))
                                .addLast(pool, applicationContext.getBean("opsHttpMessageHandler", OpsHttpMessageHandler.class));
                    }
                });
        try {
            ChannelFuture cf = tcpBootStrap.bind(httpProperty.getBackport()).sync();
            cf.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        } finally {
            factory.shutdownGracefully(false);
        }
    }
}
@Component
@Scope("prototype")
public class ServerBootstrapFactory {
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    /**
     * New server bootstrap server bootstrap.
     *
     * @param ioThreadCount the io thread count
     * @return the server bootstrap
     */
    public ServerBootstrap newServerBootstrap(int ioThreadCount) {
        if (Epoll.isAvailable()) {
            return newEpollServerBootstrap(ioThreadCount);
        }

        return newNioServerBootstrap(ioThreadCount);
    }

    /**
     * Shutdown gracefully.
     *
     * @param shouldWait the should wait
     */
    public void shutdownGracefully(boolean shouldWait) {
        Future<?> workerFuture = workerGroup.shutdownGracefully();
        Future<?> bossFuture = bossGroup.shutdownGracefully();

        if (shouldWait) {
            workerFuture.awaitUninterruptibly();
            bossFuture.awaitUninterruptibly();
        }
    }

    private ServerBootstrap newNioServerBootstrap(int ioThreadCount) {
        if (ioThreadCount > 0) {
            bossGroup = new NioEventLoopGroup(ioThreadCount);
            workerGroup = new NioEventLoopGroup(ioThreadCount);
        } else {
            bossGroup = new NioEventLoopGroup();
            workerGroup = new NioEventLoopGroup();
        }

        return new ServerBootstrap().group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class);
    }

    private ServerBootstrap newEpollServerBootstrap(int ioThreadCount) {
        if (ioThreadCount > 0) {
            bossGroup = new EpollEventLoopGroup(ioThreadCount);
            workerGroup = new EpollEventLoopGroup(ioThreadCount);
        } else {
            bossGroup = new EpollEventLoopGroup();
            workerGroup = new EpollEventLoopGroup();
        }

        return new ServerBootstrap().group(bossGroup, workerGroup)
                .channel(EpollServerSocketChannel.class);
    }
}

请注意这一句代码,addLast(pool, applicationContext.getBean("opsHttpMessageHandler", OpsHttpMessageHandler.class))。我使用applicationContext来获取opsHttpMessageHandler这个bean,因为此处位于ChannelInitializer中,而ChannelInitializer本身并不是spring管理的bean,所以只能通过applicationContext来获取对应的bean。

上述代码中,我将server设置为线程,这样是为了能在一个程序中,同时监听多个端口。最后是整个程序的入口,使用ApplicationContext来获取server的bean,并使用ExecutorService 来启动server对应的线程。

@SpringBootApplication
public class CDNRouterServer {

    private static Logger logger = LoggerFactory.getLogger(CDNRouterServer.class);

    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = SpringApplication.run(CDNRouterServer.class, args);
        ExecutorService service = Executors.newCachedThreadPool();
        OpsHttpServer server = ctx.getBean("opsHttpServer", OpsHttpServer.class);
        service.execute(server);
        service.shutdown();
    }
}

引入Spring-boot后,我们就可以很方便的引入spring-data-jpa来做数据库的访问了,而不需要再手动得写JDBC的程序,极大简化了数据库的访问。