netty+javassist轻量级http服务&RequestMapping框架

在做一些后台服务的时候,有时候需要一些轻量级的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 apiMap;
    // Controller实例,一些单例的集合
    private Map, Object> beanMap;
    // 各Controller方法参数名集合
    private Map paramNamesMap;

    private Logger logger = Logger.getLogger(getClass());

    /**
     * 初始化以上各属性
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        logger.info("binging REST meta");

        apiMap = new HashMap();
        beanMap = new HashMap, Object>();
        paramNamesMap = new HashMap();

        // Rest是自定义的一个注解类,用于标识http api方法,类似spring的@RequestMapping注解
        Map 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> 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 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 apiHelp() {
        // help方法,展现所支持的http方法,也就是api信息
        return bindingMeta.getApiMap();
    }

    @Rest(path = "/config")
    public Config listConfig() {
        // 自己封装一些配置参数信息
        return config;
    }
}
 
这样子框架就搭好了,用的时候调用 NettyHttpServer.start(),然后就可以在浏览器访问了, 对于非web的后台服务还是挺方便的。最后上张效果图

netty+javassist轻量级http服务&RequestMapping框架_第1张图片
 

你可能感兴趣的:(后端工具,netty,javascript,json)