在做一些后台服务的时候,有时候需要一些轻量级的Http入口,以便通过浏览器就能实现便捷的后台功能,例如
1.监控服务运行状态,如服务存在性、版本、配置、性能等
2.手动触发一些功能入口(特别适合测试环境的冒烟测试)
3.支持一些紧急操作,例如手动清缓存,有时候排查问题有用
这些操作通常数量不多,也没什么并发,专门搭一套web框架(如tomcat+spring mvc)有点浪费,一点不封装又不方便。以下用netty+javassist实现一个简单的http服务框架,使得在使用上达到接近spring mvc的体验。这里还使用了spring-bean,但只是为了托管实例方便,如果愿意也可以完全不依赖spring
以下为主要代码实现。首先是server主类,标准的netty server端,没什么特殊的
package com.ximalaya.damus.protocol.rest;
import java.io.IOException;
import java.net.InetSocketAddress;
import org.apache.log4j.Logger;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.http.HttpContentCompressor;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import com.ximalaya.damus.protocol.exception.RestException;
import com.ximalaya.damus.protocol.rest.binding.BindingMeta;
import com.ximalaya.damus.protocol.rest.netty.SimpleHttpRequestHandler;
public class NettyHttpServer {
private ServerBootstrap bootstrap;
private final InetSocketAddress host;
@Autowired
private BindingMeta meta;
private Logger logger = Logger.getLogger(getClass());
public NettyHttpServer(String hostname, int port) {
host = new InetSocketAddress(hostname, port);
}
public void start() throws RestException, IOException {
bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory());
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("encoder", new HttpResponseEncoder());
pipeline.addLast("deflater", new HttpContentCompressor());
pipeline.addLast("handler", new SimpleHttpRequestHandler(meta));
return pipeline;
}
});
bootstrap.bind(host);
logger.info("Start NettyHttpServer at " + host);
}
public void close() {
logger.info("Close NettyHttpServer at " + host);
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap.releaseExternalResources();
}
}
}
下面是Handler,主要逻辑就是messageReceived()
package com.ximalaya.damus.protocol.rest.netty;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK;
import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import org.apache.log4j.Logger;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import com.ximalaya.damus.common.util.JsonUtils;
import com.ximalaya.damus.protocol.rest.binding.BindingMeta;
public class SimpleHttpRequestHandler extends SimpleChannelUpstreamHandler {
private Logger logger = Logger.getLogger(getClass());
private final BindingMeta meta;
public SimpleHttpRequestHandler(BindingMeta meta) {
this.meta = meta;
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
HttpRequest request = (HttpRequest) e.getMessage();
String uri = request.getUri();
logger.info("Rest Request:" + uri);
if (!"/favicon.ico".equals(uri)) { // 过滤静态资源,因为不需要前端页面
Object ret = meta.invoke(request); // 触发controller逻辑
writeResponse(e.getChannel(), ret != null ? ret : "done"); // 返回
}
}
// 这个方法将返回值以Json方式返回,类似实现Spring的@ResponseBody注解
private void writeResponse(Channel channel, Object obj) {
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
response.setStatus(HttpResponseStatus.OK);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json");
String resMsg = JsonUtils.toJsonString(obj); // 序列化方法,不用展开了吧
response.setContent(ChannelBuffers.wrappedBuffer(resMsg.getBytes()));
final ChannelFuture future = channel.write(response);
future.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
logger.info("channel closed:" + e.getChannel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
logger.error("rest error", e.getCause());
// 如果抛异常,就把异常信息返回出去
writeResponse(e.getChannel(), "ERROR: " + e.getCause().getMessage());
}
}
接下来就是BindingMeta,这是主要逻辑的实现类,包括Controller信息的收集,RequestMapping字段信息解析,已经Controller触发逻辑。这个类用spring托管只是方便起见
package com.ximalaya.damus.protocol.rest.binding;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javassist.ClassPool;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.LocalVariableAttribute;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import com.ximalaya.damus.common.util.JsonUtils;
import com.ximalaya.damus.protocol.exception.RestException;
import com.ximalaya.damus.protocol.rest.annotation.Rest;
@Component
public class BindingMeta implements ApplicationContextAware {
// Controller方法元数据,每个元素对应一个http请求
private Map<String, Method> apiMap;
// Controller实例,一些单例的集合
private Map<Class<?>, Object> beanMap;
// 各Controller方法参数名集合
private Map<String, String[]> paramNamesMap;
private Logger logger = Logger.getLogger(getClass());
/**
* 初始化以上各属性
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
logger.info("binging REST meta");
apiMap = new HashMap<String, Method>();
beanMap = new HashMap<Class<?>, Object>();
paramNamesMap = new HashMap<String, String[]>();
// Rest是自定义的一个注解类,用于标识http api方法,类似spring的@RequestMapping注解
Map<String, Object> allBeans = applicationContext.getBeansWithAnnotation(Rest.class);
for (Object instance : allBeans.values()) {
// 生成Controller类的实例
Class<?> clz = instance.getClass();
beanMap.put(clz, instance);
// 解析成员方法,也就是各http api
parseMethods(clz);
}
}
private void parseMethods(Class<?> clz) {
for (Method method : clz.getDeclaredMethods()) {
Rest rest = method.getAnnotation(Rest.class);
// 遍历所有带@Rest注解的方法
if (rest != null) {
logger.info("Binding url " + rest.path() + " to " + method);
apiMap.put(rest.path(), method);
// 解析参数名,也就是http请求所带的参数名
String[] paramNames = parseParamNames(method);
paramNamesMap.put(rest.path(), paramNames);
}
}
}
// 这个方法要解析某个方法所有参数的声明名称,这里需要用到javassist的高级反射特性(普通的反射机制是拿不到方法参数声明名称的,只能拿到类型)
private String[] parseParamNames(Method method) {
try {
CtMethod cm = ClassPool.getDefault().get(method.getDeclaringClass().getName())
.getDeclaredMethod(method.getName());
LocalVariableAttribute attr = (LocalVariableAttribute) cm.getMethodInfo()
.getCodeAttribute().getAttribute(LocalVariableAttribute.tag);
String[] paramNames = new String[cm.getParameterTypes().length];
int offset = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
for (int i = 0; i < cm.getParameterTypes().length; i++) {
paramNames[i] = attr.variableName(i + offset);
}
return paramNames;
} catch (NotFoundException e) {
logger.error("parseParamNames Error", e);
return new String[] {};
}
}
// 这个就是Handler调用的入口,也就是将HttpRequest映射到对应的方法并映射各参数
public Object invoke(HttpRequest request) throws RestException {
String uri = request.getUri();
int index = uri.indexOf('?');
// 获取请求路径,映射到Controller的方法
String path = index >= 0 ? uri.substring(0, index) : uri;
Method method = apiMap.get(path);
if (method == null) { // 没有注册该方法,直接抛异常
throw new RestException("No method binded for request " + path);
}
try {
Class<?>[] argClzs = method.getParameterTypes(); // 参数类型
Object[] args = new Object[argClzs.length]; // 这里放实际的参数值
String[] paramNames = paramNamesMap.get(path); // 参数名
// 这个是netty封装的url参数解析工具,当然也可以自己split(",")
Map<String, List<String>> requestParams = new QueryStringDecoder(uri).getParameters();
// 逐个把请求参数解析成对应方法参数的类型
for (int i = 0; i < argClzs.length; i++) {
Class<?> argClz = argClzs[i];
String paramName = paramNames[i];
if (!requestParams.containsKey(paramName)
|| CollectionUtils.isEmpty(requestParams.get(paramName))) {
// 没有找到对应参数,则默认取null。愿意的话也可以定义类似@Required或者@DefaultValue之类的注解来设置自定义默认值
args[i] = null;
continue;
}
// 如果带了这个参数,则根据不同的目标类型来解析,先是一些基础类型,这里列的不全,有需要的话可以自己加其他,例如Date
String param = requestParams.get(paramNames[i]).get(0);
if (param == null) {
args[i] = null;
} else if (argClz == HttpRequest.class) {
args[i] = request;
} else if (argClz == long.class || argClz == Long.class) {
args[i] = Long.valueOf(param);
} else if (argClz == int.class || argClz == Integer.class) {
args[i] = Integer.valueOf(param);
} else if (argClz == boolean.class || argClz == Boolean.class) {
args[i] = Boolean.valueOf(param);
} else if (argClz == String.class) {
args[i] = param;
} else {
// 复合类型的话,默认按照Json方式解析。不过这种场景一般也不需要特别复杂的参数
try {
args[i] = JsonUtils.fromJsonString(argClz, param);
} catch (Exception e) {
args[i] = null;
}
}
}
// 最后反射调用方法
Object instance = beanMap.get(method.getDeclaringClass());
return method.invoke(instance, args);
} catch (Exception e) {
throw new RestException(e);
}
}
public Map<String, Method> getApiMap() {
return apiMap;
}
}
@Rest注解类
package com.ximalaya.damus.protocol.rest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Rest {
String path() default "";
}
最后是Controller方法,以下是一个最简单的demo,可以看做是服务的封面。也可以自己实现其他的Controller,代码风格与spring mvc类似
package com.ximalaya.damus.actuary.rest;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import com.ximalaya.damus.protocol.rest.annotation.Rest;
import com.ximalaya.damus.protocol.rest.binding.BindingMeta;
@Component
@Rest
public class IndexController implements ApplicationContextAware {
@Autowired
private Config config;
@Autowired
private BindingMeta bindingMeta;
@Rest(path = "/")
public String index() {
return "My-Project v0.0.1 www.ximalaya.com";
}
@Rest(path = "/help")
public Map<String, Method> apiHelp() {
// help方法,展现所支持的http方法,也就是api信息
return bindingMeta.getApiMap();
}
@Rest(path = "/config")
public Config listConfig() {
// 自己封装一些配置参数信息
return config;
}
}
这样子框架就搭好了,用的时候调用
NettyHttpServer.start(),然后就可以在浏览器访问了,
对于非web的后台服务还是挺方便的。最后上张效果图
![](https://i-blog.csdnimg.cn/blog_migrate/10d02da69ddcb9688ace3bd97a8e6a70.png)