(四)使用Netty结合springmvc

之前用netty做了一个简陋的web容器,算是一个demo。
但是在实际应用里面,肯定都是用类似spring,springmvc做管理容器的。
网上有一些跟Netty相关的mvc框架,但是没发现像springmvc那么主流的,所以这里就结合Spring,springmvc,netty做一个web容器。

依赖

依赖比较简单,其实就是普通的spring-starter 还有feign的组件,还有eureka,熔断器,方便后续使用,集成在里面,也有一些Spring-web相关的依赖需要用到,但是feign的组件里面已经提供了

<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>
  <groupId>com.gee</groupId>
  <artifactId>netty-springmvc-demo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath />
        <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
        <docker.registry>191.0.0.158/ibcloud</docker.registry>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- spring-cloud-dependencies start -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- spring-cloud-dependencies end -->
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- netty组件 -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
        </dependency>
        <!-- 时间工具类 start -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- spring start -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 单元测试 start -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 服务发现 start -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 服务发现 end -->
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
            <!-- 指定maven编译的jdk的版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

yml配置文件

server:
  port: 9413

spring:
  application:
    name: netty-spring-mvc-demo
  profiles:
    active:
    - local
      
netty: 
  server: 
    port: ${nettyport:9527}
    host: ${nettyhost:127.0.0.1}
    
eureka:
  instance:
    lease-renewal-interval-in-seconds: 15
    prefer-ip-address: true
  client:
    serviceUrl:
      defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:}

项目启动类

package com.gee;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.openfeign.EnableFeignClients;

import com.gee.netty.NettyServer;



@SpringBootApplication
@EnableConfigurationProperties
@EnableFeignClients
public class NettyWebApplication implements CommandLineRunner{
    public static void main(String[] args) {
        SpringApplication.run(NettyWebApplication.class, args);
    }
    
    @Autowired
    private NettyServer nettyServer;
    
    @Override
    public void run(String... args) throws Exception {
        nettyServer.start();
    }
}

这里也没有太特殊的地方,其实就是实现了CommandLineRunner,可以在容器启动后,启动Netty服务端。

构建Netty服务

1.先构建最基本的Netty服务配置

这个配置的话,大家可以自己写死,或者是配置在配置文件里面

@Configuration
@ConfigurationProperties(prefix = "netty.server")
@Data
public class NettyServerConfig {
    private Integer port;
    private String host;
}

2.构建自定义的DispatcherServlet用于处理请求, 处理请求的核心。

为什么要弄一个类,去继承DispatcherServlet。
答案比较简单,因为DispatcherServlet中的doService方法是protected的,所以只能继承子类去调用。
DispatcherServlet是Springmvc处理请求的核心。
另外springmvc本身也是主流的组件,自己去写一个servlet太费劲了。
另外我们还看到,构建自定义的DispatcherServlet的时候,需要传入一个web容器,因为servlet初始化的时候,需要传入一个容器。下面再讲一下容器是如何构建的。

package com.gee.netty.handler;


import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class CustomDispatcherServlet extends DispatcherServlet{
    private static final long serialVersionUID = -5960477391619626506L;
    
    public CustomDispatcherServlet(AnnotationConfigWebApplicationContext webApplicationContext)
            throws ServletException {
        super(webApplicationContext);
        this.init(webApplicationContext.getServletConfig());
    }

    public void doService(HttpServletRequest request, HttpServletResponse response) {
        try {
            super.doService(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.构建web容器。用于servlet的初始化。

因为Servlet本身,单例就满足需求了,所以直接注入到容器里面去。
先弄一个配置类,用于扫描Mvc的组件, 在构建完容器后,将其注册到容器中,随后进行刷新。

@Configuration
@ComponentScan("com.gee.web.controller")
public class WebAppConfig {

}
package com.gee.netty;

import javax.servlet.ServletException;

import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.web.SpringBootMockServletContext;
import org.springframework.context.annotation.Bean;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import com.gee.netty.handler.CustomDispatcherServlet;
import com.gee.netty.handler.HttpHandler;
import com.gee.web.WebAppConfig;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class NettyServer {
    
    @Autowired
    private NettyServerConfig serverConfig;
        
    @Bean
    public CustomDispatcherServlet customDispatcherServlet() {
        AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
        webApplicationContext.register(WebAppConfig.class);
        webApplicationContext.refresh();
        SpringBootMockServletContext sevletContext = new SpringBootMockServletContext("/");
        sevletContext.setServletContextName("mokServlet");
        MockServletConfig sevletConfig = new MockServletConfig(sevletContext);
        webApplicationContext.setServletConfig(sevletConfig);
        webApplicationContext.setServletContext(sevletContext);
        try {
            创建完容器后,将容器传入到自定义的servlet中
            return new CustomDispatcherServlet(webApplicationContext);
        } catch (ServletException e) {
            throw new BeanCreationException("创建CustomDispatcherServlet的时候失败");
        }
    }
    
    public void start() {
        // 用于处理连接事件
        EventLoopGroup boss = new NioEventLoopGroup();
        // 用于处理读写事件
        EventLoopGroup work = new NioEventLoopGroup();
        ServerBootstrap sb = new ServerBootstrap();
        sb.group(boss, work);
        // 设置处理的通道类型
        sb.channel(NioServerSocketChannel.class);
        sb.option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
        // 设置对于客户端连接的处理器
        sb.childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                //对http请求进行解码
                ch.pipeline().addLast(new HttpRequestDecoder());
                //对响应进行编码
                ch.pipeline().addLast(new HttpResponseEncoder());
                ch.pipeline().addLast(new HttpObjectAggregator(65536));
                //这个handler是自定义的Handler,用于处理请求的。
                ch.pipeline().addLast(new HttpHandler());

            }
        });
        ChannelFuture cf = null;
        try {
            cf = sb.bind(serverConfig.getHost(), serverConfig.getPort()).sync();
            if(cf.isDone()) {
                log.info("服务端启动成功");
            }
            cf.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("服务端启动或者关闭时出现异常");
        } finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
        }
    }
}

4.构建自定义的处理器,用于处理channel的读写相关。

由于这里我们只需要响应,所以比较简单。

package com.gee.netty.handler;


import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;

import org.apache.commons.lang.StringUtils;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import com.gee.utils.ApplicationContextUtils;


public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest>{

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullRequest) throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setMethod(fullRequest.method().name());
        request.setContentType(fullRequest.headers().get("Content-Type"));
        request.setRequestURI(fullRequest.uri());

        MockHttpServletResponse response = new MockHttpServletResponse();
        通过工具类从容器中获取CustomDispatcherServlet单例,用于处理请求,后面再讲容器的工具类的构建。=======================================================
        ApplicationContextUtils.getBean(CustomDispatcherServlet.class).doService(request, response);
        HttpResponseStatus status = HttpResponseStatus.valueOf(response.getStatus());
        String result = response.getContentAsString();
        result = StringUtils.isEmpty(result) ? "" : result;

        FullHttpResponse fullResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
                Unpooled.copiedBuffer(result, CharsetUtil.UTF_8));
        fullResponse.headers().set("Content-Type", "text/json;charset=UTF-8");
        fullResponse.headers().set("Access-Control-Allow-Origin", "*");
        fullResponse.headers().set("Access-Control-Allow-Headers",
                "Content-Type,Content-Length, Authorization, Accept,X-Requested-With,X-File-Name");
        fullResponse.headers().set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
        fullResponse.headers().set("Content-Length", Integer.valueOf(fullResponse.content().readableBytes()));
        fullResponse.headers().set("Connection", "keep-alive");
        ChannelFuture writeFuture = ctx.writeAndFlush(fullResponse);
        writeFuture.addListener(ChannelFutureListener.CLOSE);
    }
}

5工具类

package com.gee.utils;


import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.stereotype.Component;


@Component
public class ApplicationContextUtils implements ApplicationContextAware{
    //用于封装成员变量
    private ApplicationContext applicationContext;
    //封装一个静态变量用于静态方法的调用
    private static ApplicationContext STATIC_APPLICATION_CONTEXT;
    
    //为什么实现这个接口,
    //ApplicationContextAwarePostProcessor会判断类是否是ApplicationContextAware的子类
    //如是,则会将容器作为成员变量,通过SET方法封装到成员变量
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        STATIC_APPLICATION_CONTEXT = applicationContext;
    }
    
    public static <T> T getBean(Class<T> classT) {
        if(null == STATIC_APPLICATION_CONTEXT) {
            return null;
        }
        return STATIC_APPLICATION_CONTEXT.getBean(classT);
    }
    
    public static Object getBean(String beanName) {
        if(null == STATIC_APPLICATION_CONTEXT) {
            return null;
        }
        return STATIC_APPLICATION_CONTEXT.getBean(beanName);
    }
    
       /**
     * 获取接口的所有实现类
    **/
    public static <T> List<T> getBeanList(Class<T> classT) {
        String beanNames[] = STATIC_APPLICATION_CONTEXT.getBeanNamesForType(classT);
        if(null == beanNames) {
            return null;
        }
        List<T> beanList = new ArrayList<T>();
        Object bean;
        for(String beanName : beanNames) {
            bean = getBean(beanName);
            if(null != bean) {
                beanList.add((T)bean);
            }
        }
        return beanList;
    }
    
    public static void publishEvent(ApplicationEvent event) {
        STATIC_APPLICATION_CONTEXT.publishEvent(event);
    }
    
    public static <T> T getBean(String beanName, Class<T> claz) {
        return STATIC_APPLICATION_CONTEXT.getBean(beanName, claz);
    }
    
    public static Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> annotationType) {
        return STATIC_APPLICATION_CONTEXT.getBeansWithAnnotation(annotationType);
    }
    
    public static Class getType(String beanName) {
        return STATIC_APPLICATION_CONTEXT.getType(beanName);
    }
    
    public static String getActiveProfile() {
        return STATIC_APPLICATION_CONTEXT.getEnvironment().getActiveProfiles()[0];
    }
    
    public static List<String> getBeanNams() {
        return Arrays.asList(STATIC_APPLICATION_CONTEXT.getBeanDefinitionNames());
    }
}

6.写一个controller用于测试

package com.gee.web.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/hello")
    public String get() {
        return "helloworld";
    }
}
运行结果图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容