springboot自带的http服务功能由于功能较多,导致打包后的jar包比较大,由于我们业务以及环境的特殊性因此决定自己实现一个http服务器,主要原理是由一个启动器去扫描系统包下的类注解、方法注解等,生成http接口容器,然后通过url等方式定位到接口,最后进行反射调用
最终的效果:
通过指定包名启动http服务、初始化配置信息以及容器信息
定义接口方式如下:
1、maven依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
<version>4.1.86.Final</version>
</dependency>
2、HttpServerStarter.class
package com.ctrip.hotel.octopus.crawler.http.starter;
import com.ctrip.hotel.octopus.commons.base.utils.JsonUtils;
import com.ctrip.hotel.octopus.commons.base.utils.RetryMonitor;
import com.ctrip.hotel.octopus.crawler.common.config.PropertiesLoader;
import com.ctrip.hotel.octopus.crawler.http.container.HttpServerContainer;
import com.ctrip.hotel.octopus.crawler.http.initializer.HttpInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.AdaptiveRecvByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.io.InputStream;
import java.util.Properties;
/**
* @author ZhaoXu
* @date 2024/1/10 19:09
*/
@Slf4j
public class HttpServerStarter {
private final EventLoopGroup bossGroup = new NioEventLoopGroup();
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
private final ServerBootstrap serverBootstrap = new ServerBootstrap();
private final String[] basePackages;
public HttpServerStarter(String[] basePackages) {
this.basePackages = basePackages;
}
public static void start(String... basePackages) {
// 失败自动重试
RetryMonitor.registry(() -> {
try {
HttpServerStarter httpServerStarter = new HttpServerStarter(basePackages);
httpServerStarter.init();
httpServerStarter.start();
} catch (Exception e) {
log.error("服务启动失败!", e);
throw new RuntimeException(e);
}
}, Integer.MAX_VALUE);
}
private void init() {
// 初始化http服务
initHttpServer();
// 初始化http容器
initHttpContainer(basePackages);
}
private void start() throws InterruptedException {
String port = PropertiesLoader.getProperty("server.port", "8080");
ChannelFuture channelFuture = serverBootstrap.bind("0.0.0.0", Integer.parseInt(port)).sync();
log.info("http服务启动成功,监听本地端口:" + port);
channelFuture.channel().closeFuture().sync();
}
private void initHttpContainer(String[] basePackages) {
log.info("初始化http容器");
HttpServerContainer httpServerContainer = new HttpServerContainer(basePackages);
httpServerContainer.start();
}
private void initHttpServer() {
log.info("初始化http服务信息");
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
//开启SO_KEEPALIVE
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
//配置receiveBuf使用混合型
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator());
//开启TCP_NODELAY
serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
//配置初始化处理器队列
serverBootstrap.childHandler(new HttpInitializer());
}
}
3、定义channel以及handler等配置类HttpInitializer
package com.ctrip.hotel.octopus.crawler.http.initializer;
import com.ctrip.hotel.octopus.crawler.http.handler.HttpCustomHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
/**
* @author ZhaoXu
* @date 2024/1/10 19:55
*/
@Slf4j
public class HttpInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("http编解码器", new HttpServerCodec());
pipeline.addLast("http大数据包处理器", new ChunkedWriteHandler());
pipeline.addLast("http报文聚合器", new HttpObjectAggregator(64 * 1024));
pipeline.addLast("自定义http请求分发器", new HttpCustomHandler());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("http服务发生异常!", cause);
ctx.close();
}
}
4、HttpCustomHandler
package com.ctrip.hotel.octopus.crawler.http.handler;
import com.ctrip.hotel.octopus.commons.base.enums.ErrorCode;
import com.ctrip.hotel.octopus.commons.base.exception.BaseException;
import com.ctrip.hotel.octopus.commons.base.model.BaseResponse;
import com.ctrip.hotel.octopus.commons.base.utils.JsonUtils;
import com.ctrip.hotel.octopus.crawler.common.config.ThreadPoolConfig;
import com.ctrip.hotel.octopus.crawler.http.container.HttpServerContainer;
import com.ctrip.hotel.octopus.crawler.http.enums.HttpRequestType;
import io.netty.buffer.Unpooled;
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.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author ZhaoXu
* @date 2024/1/10 19:52
*/
@Slf4j
public class HttpCustomHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("处理http请求服务异常!", cause);
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) {
ThreadPoolExecutor executor = ThreadPoolConfig.getExecutor();
fullHttpRequest.content().retain();
executor.execute(() -> {
try {
String name = fullHttpRequest.method().name();
HttpRequestType requestType = HttpRequestType.type2EnumMap.get(name);
// 解析请求类型
if (ObjectUtils.isEmpty(requestType)) {
sendResponse(channelHandlerContext, BaseResponse.fail("不支持的请求类型", -1));
return;
}
String url = fullHttpRequest.uri();
int indexOf = url.indexOf("?");
if (indexOf != -1) {
url = url.substring(0, indexOf);
}
// 定位接口位置
Method targetMethod = HttpServerContainer.getTargetMethod(url, requestType);
if (ObjectUtils.isEmpty(targetMethod)) {
sendResponse(channelHandlerContext, BaseResponse.fail("资源不存在!", 404));
return;
}
Object instance = HttpServerContainer.getTargetInstanceByMethod(targetMethod);
// 解析属性并注入至方法
Object[] realParams = ParameterHandler.resolveRequestParam(fullHttpRequest, targetMethod);
Object result = targetMethod.invoke(instance, realParams);
if (!(result instanceof BaseResponse)) {
result = BaseResponse.success(result);
}
sendResponse(channelHandlerContext, result);
} catch (Exception e) {
if (BaseException.class.isAssignableFrom(e.getCause().getClass())) {
BaseException baseException = (BaseException) e.getCause();
log.info("发生业务异常!", e);
sendResponse(channelHandlerContext, new BaseResponse<>(baseException.getCode(), baseException.getMessage(), e.getCause()));
} else {
log.error("发生系统异常!", e);
sendResponse(channelHandlerContext, new BaseResponse<>(500, e.getMessage(), e.getCause()));
}
}
});
}
private void sendResponse(ChannelHandlerContext channelHandlerContext, Object response) {
String json = JsonUtils.toJson(response);
FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(json.getBytes(StandardCharsets.UTF_8)));
fullHttpResponse.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
channelHandlerContext.writeAndFlush(fullHttpResponse).addListener(ChannelFutureListener.CLOSE);
}
}
5、http容器:HttpServerContainer
package com.ctrip.hotel.octopus.crawler.http.container;
import com.ctrip.hotel.octopus.commons.base.utils.ClassUtils;
import com.ctrip.hotel.octopus.crawler.http.annotation.RequestMapping;
import com.ctrip.hotel.octopus.crawler.http.enums.HttpRequestType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* @author ZhaoXu
* @date 2024/1/10 20:09
*/
@Slf4j
public class HttpServerContainer {
private final String[] basePackages;
/**
* 通过url + 请求方式定位某个方法
*/
private static final Map<Pair<String, HttpRequestType>, Method> URL_AND_TYPE2_METHOD = new ConcurrentHashMap<>();
/**
* 通过方法定位class
*/
private static final Map<Method, Object> METHOD_2_CLASS = new ConcurrentHashMap<>();
public HttpServerContainer(String... basePackages) {
if (ObjectUtils.isEmpty(basePackages)) {
throw new RuntimeException("包路径为空,无法解析!");
}
this.basePackages = basePackages;
}
public void start() {
// 扫描包路径下的所有类
Set<Class<?>> allClass = Arrays.stream(basePackages)
.map(ClassUtils::scanClass)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
// 自定义接口路由
for (Class<?> aClass : allClass) {
Method[] declaredMethods = aClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = declaredMethod.getAnnotation(RequestMapping.class);
String url = requestMapping.value();
HttpRequestType requestType = requestMapping.requestType();
if (ObjectUtils.anyNull(url, requestType)) {
log.error("容器接口解析异常,class;{},method:{}", aClass.getName(), declaredMethod.getName());
}
log.info("装载http接口,url:{},requestType:{},class;{},method:{}", url, requestType.getType(), aClass.getName(), declaredMethod.getName());
Pair<String, HttpRequestType> pair = Pair.of(url, requestType);
URL_AND_TYPE2_METHOD.put(pair, declaredMethod);
try {
METHOD_2_CLASS.put(declaredMethod, aClass.newInstance());
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
}
public static Method getTargetMethod(String url, HttpRequestType requestType) {
Pair<String, HttpRequestType> pair = Pair.of(url, requestType);
return URL_AND_TYPE2_METHOD.get(pair);
}
public static Object getTargetInstanceByMethod(Method method) {
return METHOD_2_CLASS.get(method);
}
}
6、参数解析器:ParameterHandler
package com.ctrip.hotel.octopus.crawler.http.handler;
import com.ctrip.hotel.octopus.commons.base.utils.JsonUtils;
import com.ctrip.hotel.octopus.crawler.http.annotation.RequestParam;
import com.ctrip.hotel.octopus.crawler.http.annotation.RequestBody;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
/**
* @author ZhaoXu
* @date 2024/1/10 21:40
*/
@Slf4j
public class ParameterHandler {
/**
* 解析请求参数
* @param fullHttpRequest
* @param targetMethod
* @return
*/
public static Object[] resolveRequestParam(FullHttpRequest fullHttpRequest, Method targetMethod) {
Parameter[] parameters = targetMethod.getParameters();
Object[] realParams = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
// 参数分为param和body类型
if (parameter.isAnnotationPresent(RequestBody.class)) {
Class<?> type = parameter.getType();
ByteBuf content = fullHttpRequest.content();
String jsonString = content.toString(StandardCharsets.UTF_8);
realParams[i] = JsonUtils.fromJson(jsonString, type);
} else if (parameter.isAnnotationPresent(RequestParam.class)) {
RequestParam annotation = parameter.getAnnotation(RequestParam.class);
String value = annotation.value();
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(fullHttpRequest.uri());
Map<String, List<String>> params = queryStringDecoder.parameters();
List<String> values = params.get(value);
if (ObjectUtils.isNotEmpty(values)) {
Class<?> type = parameter.getType();
if (type == String.class) {
realParams[i] = values.get(0);
}
if (type == Integer.class || type == int.class) {
realParams[i] = Integer.parseInt(values.get(0));
}
if (type == Double.class || type == double.class) {
realParams[i] = Double.parseDouble(values.get(0));
}
}
}
}
return realParams;
}
}