(四)使用Netty结合springmvc

2021-04-08  本文已影响0人  guessguess

之前用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";
    }
}
运行结果图
上一篇下一篇

猜你喜欢

热点阅读