(四)使用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";
}
}
运行结果图