用APT解耦Http Server的客户端请求

做过几个需要http server的android项目。 可能由于客户端请求种类比较少。所以都是用if-else分发和处理客户端request。现在做一个框架,使得http server更容易使用。打算用注解的方式解耦, 做到单一,开放封闭这两条原则。

先看一下if-else的原始方式:

public class HttpServer{
...   
 @Override
    public Response serve(IHTTPSession session) {
        try {
            String path = session.getUri();
            if (path.startsWith("/info/")) {
                return DeviceManager.getInstance().onRequest(session);
            }


            if (path.startsWith("/filedrop/")) {
                return FileManager.getInstance().onRequest(session);
            }

      
            return createResponse(Constants.Response.Invalid_Param, "Device id is null");
        } catch (Exception e) {
            return createResponse(Constants.Response.Invalid_Param, "check your json or other param");
        }
    }
...
}

"info", "filedrop",是功能模块,如果多一个模块就要再加一个if。而且模块里面也分方法,又是一大堆if-else。比如,在FileManager里面:

    @Override
    public NanoHTTPD.Response onRequest(NanoHTTPD.IHTTPSession session) {
        String path = session.getUri();

        if (path.startsWith("/file/v1/create")) {

        } else if (path.startsWith("/file/v1/upload")) {

        } else if (path.startsWith("/file/v1/end")) {
     
        } else if (path.startsWith("/file/v1/pause")) {
       
        } else if (path.startsWith("/file/v1/resume")) {
         
        } else {
            return createResponse(Constants.Response.Function_Not_Exist, "Function not exist:" + path);
        }
    }

 那么如何消除这些if-else呢?我们完全可以借鉴ARouter的方式来解决这个问题。

下面是我们已经很熟悉的方式。

@Route(path = "/app/MainActivity")
public class MainActivity extends Activty{


}

那么我们仿照它做成这样:

@HttpService(path="/filedrop/v1/upload")
public class ChunkUpload implements HttpServer.RequestHandler {
    private FileManager manager = getInstance();

    @Override
    public NanoHTTPD.Response onRequest(NanoHTTPD.IHTTPSession session) {
   
    }
}

效果就是:当客户端发送upload请求的时候,直接到onRequest()方法里面来了。接下来,如果还要增加客户端调用的方法,也是这样做就好了。 这样就实现开闭原则了。

那么实现这样的效果我们需要做什么呢?

1. @HttpService

首先自然是创建一个注解,命名为@HttpService。


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface HttpService {
    String path();
}

这是一个针对class的注解((ElementType.TYPE)),而且是一个编译时注解
@Retention(RetentionPolicy.CLASS)。

2. 改造HttpServer的分发方法

    @Override
    public NanoHTTPD.Response serve(IHTTPSession session) {
        String path = session.getUri();
        if (TextUtils.isEmpty(path))
            return createResponse(Constants.Response.Function_Not_Exist, "Function not exist:" + path);

        RequestHandler service = ServiceHouse.services.get(path);
        if (service == null) {// There's no instance of this service
            Class<? extends RequestHandler> serviceMeta = ServiceHouse.servicesMetas.get(path);
            RequestHandler instance;
            try {
                instance = serviceMeta.getConstructor().newInstance();
                ServiceHouse.services.put(serviceMeta, instance);
                service = instance;
            } catch (Exception e) {
                return createResponse(Constants.Response.Function_Not_Exist, "Function not exist:" + path);
            }
        }

        return service.onRequest(session);
    }

这是改造后的分发方法。先根据uri,也就是注解的path值,找到处理类的class, 比如上面的ChunkUpload类。然后通过反射生成ChunkUpload类对象,然后调用onRequest()方法,最后缓存这个类对象,方便下次使用。

做到此处,HttpServer的serve()方法以后再也不需要由于增加模块而改变代码!

3. ServiceHouse 缓存

前面提到,处理类的class是从ServiceHouse中获取的,那么ServiceHouse里面还存储了什么? 怎么存储的?

public class ServiceHouse {
  static Map<String, Class<? extends IServiceGroup>> servicesIndex = new HashMap<>();
  static Map<String,  Class<? extends HttpServer.RequestHandler>> servicesMetas = new HashMap<>();
  static Map<Class, HttpServer.RequestHandler> services = new HashMap<>();
}

ServiceHouse这个类,完全就是ARouter的Warehouse类。

再来看一下自动生成的类,然后在说一下ServiceHouse里面存了什么。

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY HTTP SERVER. */
public class HttpService$$Group$$info implements IServiceGroup {
  @Override
  public void loadInto(Map<String, Class<? extends HttpServer.RequestHandler>> loader) {
    loader.put("/info/v1/exchange", ExchangeInfo.class);
  }
}
/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY HTTP SERVER. */
public class HttpService$$Group$$Root implements IServiceRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IServiceGroup>> groups) {
    groups.put("filedrop", com.nero.fileserver.apt.HttpService$$Group$$filedrop.class);
    groups.put("info", com.nero.fileserver.apt.HttpService$$Group$$info.class);
  }
}

 

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY HTTP SERVER. */
public class HttpService$$Group$$filedrop implements IServiceGroup {
  @Override
  public void loadInto(Map<String, Class<? extends HttpServer.RequestHandler>> loader) {
    loader.put("/filedrop/v1/upload", ChunkUpload.class);
    loader.put("/filedrop/v1/create", CreateUpload.class);
    loader.put("/filedrop/v1/end", UploadEnd.class);
    loader.put("/filedrop/v1/pause", UploadPause.class);
    loader.put("/filedrop/v1/resume", UploadResume.class);
  }
}

上面是所有自动生成的类。

ServiceHouse中的servicesIndex 存储了所有的HttpService$Group$moduleName, 每个module必然包含很多方法。

servicesMetas 中缓存着,path值所对应的方法类的class,用于反射生成对象。

services中缓存着,曾经反射生成过的方法类对象。

4.加载 ServiceHouse 缓存

    private HttpServer() {
        super(PORT);

        try {
            IServiceRoot serviceRoot = (IServiceRoot) Class.forName("com.nero.fileserver.apt.HttpService$$Group$$Root").newInstance();
            serviceRoot.loadInto(ServiceHouse.servicesIndex);
            for (Map.Entry<String, Class<? extends IServiceGroup>> entry : ServiceHouse.servicesIndex.entrySet()) {
                IServiceGroup serviceGroup = entry.getValue().getConstructor().newInstance();
                serviceGroup.loadInto(ServiceHouse.servicesMetas);
            }
        } catch (Exception e) {
        }
    }

从自动生成的HttpService$Group$Root类开始入手,完成加载。

5.如何自动生成代码。

和ARouter一样,使用注解处理器在编译阶段生成代码。生成代码的工具是javapoet。相对代码比较简单就用javapoet。如果很长很长,那就用java自带的方式,直接拼接字符串。

下面是注解处理器的类:

@AutoService(Processor.class)
public class HttpServerProcessor extends BaseProcessor {
    private TypeMirror iRequestHandler = null;
    private HashMap<String, ArrayList<Element>> groupAndMembers = new HashMap<>();
    private HashMap<String, String> groups = new HashMap<>();


    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        iRequestHandler = elementUtils.getTypeElement(RequestHandler).asType();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(HttpService.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (TypeElement typeElement : set) {
            Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(typeElement);
            try {
                parseHttpServices(elementsAnnotatedWith);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return true;
    }

    private void parseHttpServices(Set<? extends Element> elements) throws IOException {
        if (elements == null)
            return;

        for (Element element : elements) {
            if (!verify(element)) {
                throw new IllegalArgumentException("The http service must implement RequestHandler");
            }

            HttpService annotation = element.getAnnotation(HttpService.class);
            if (annotation == null) {
                return;
            }

            if (!pathFormatVerify(annotation.path())) {
                return;
            }

            String[] segments = annotation.path().split("/");
            String group = segments[1];
            String serviceName = segments[2] + "/" + segments[3];

            ArrayList<Element> groupElements = groupAndMembers.get(group);
            if (groupElements == null) {
                groupElements = new ArrayList<>();
                groupAndMembers.put(group, groupElements);
            }

            groupElements.add(element);
        }

        //create group.--------------------------------------

//            public class Service$$Group$$info implements IServiceGroup {
//                public void loadInto(Map<String, Class<? extends HttpServer.RequestHandler>> loader){
//                    loader.put("/info/v1/exchange", ExchangeInfo.class);
//                }
//            }

        ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ParameterizedTypeName.get(ClassName.get(Class.class),
                        WildcardTypeName.subtypeOf(ClassName.get(iRequestHandler))));

        // Build input param name.
        ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "loader").build();

        TypeElement type_IServiceGroup = elementUtils.getTypeElement(SERVICE_GROUP_INTERFACE);

        // Generate
        if (null != groupAndMembers && groupAndMembers.size() > 0) {

            for (Map.Entry<String, ArrayList<Element>> entry : groupAndMembers.entrySet()) {
                // Build method body
                ArrayList<Element> value = entry.getValue();
                if (value != null) {
                    // Build method : 'loadInto'
                    MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                            .addAnnotation(Override.class)
                            .addModifiers(PUBLIC)
                            .addParameter(groupParamSpec);

                    for (Element element : value) {
                        HttpService annotation = element.getAnnotation(HttpService.class);
                        loadIntoMethodOfGroupBuilder.addStatement("loader.put(" +"\""+ annotation.path() +"\""+ ", $T.class)", ClassName.get((TypeElement) element));
                    }

                    // Write to disk(Write file even interceptors is empty.),Service$$Group$$ModuleName
                    JavaFile.builder(Consts.PACKAGE,
                            TypeSpec.classBuilder(HTTP_SERVICE_GROUP_PREFIXES + entry.getKey())
                                    .addModifiers(PUBLIC)
                                    .addJavadoc(WARNING_TIPS)
                                    .addMethod(loadIntoMethodOfGroupBuilder.build())
                                    .addSuperinterface(ClassName.get(type_IServiceGroup))
                                    .build()
                    ).build().writeTo(mFiler);

                    groups.put(entry.getKey(), Consts.PACKAGE + "." + HTTP_SERVICE_GROUP_PREFIXES + entry.getKey());
                }
            }
        }

//create group root.--------------------------------------

//        public class Service$$Group$$Root implements IServiceRoot {
//            public void loadInto(Map<String, Class<? extends IServiceGroup>> groups){
//                groups.put("info", Service$$Group$$info.class);
//                groups.put("filedrop", Service$$Group$$filedrop.class);
//            }
//        }


        TypeElement type_IServiceRoot = elementUtils.getTypeElement(SERVICE_ROOT_INTERFACE);

        /**
         *  Build input type, format as :
         *
         *  ```Map<Integer, Class<? extends IServiceGroup>>```
         */
        ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ParameterizedTypeName.get(
                        ClassName.get(Class.class),
                        WildcardTypeName.subtypeOf(ClassName.get(type_IServiceGroup))
                )
        );

        // Build input param name.
        ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "groups").build();

        // Build method : 'loadInto'
        MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                .addAnnotation(Override.class)
                .addModifiers(PUBLIC)
                .addParameter(rootParamSpec);

        // Generate
        if (null != groupAndMembers && groupAndMembers.size() > 0) {
            for (Map.Entry<String, ArrayList<Element>> entry : groupAndMembers.entrySet()) {
                // Build method body
                loadIntoMethodOfRootBuilder.addStatement("groups.put($S, $N.class)",entry.getKey(), groups.get(entry.getKey()));
            }
        }

        // Write to disk(Write file even interceptors is empty.),Service$$Group$$Root
        JavaFile.builder(Consts.PACKAGE,
                TypeSpec.classBuilder(HTTP_SERVICE_GROUP_ROOT_NAME)
                        .addModifiers(PUBLIC)
                        .addJavadoc(WARNING_TIPS)
                        .addMethod(loadIntoMethodOfRootBuilder.build())
                        .addSuperinterface(ClassName.get(type_IServiceRoot))
                        .build()
        ).build().writeTo(mFiler);
    }

    /**
     * the path format must start with "/", then group name, version, and service method name
     *
     * @param path
     * @return
     */
    private boolean pathFormatVerify(String path) {
        if (StringUtils.isEmpty(path) || !path.startsWith("/")) {
            throw new IllegalArgumentException("HttpService path must start with /");
        }
        String[] segments = path.split("/");
        if (segments.length != 4) {
            throw new IllegalArgumentException("HttpService path must start with /");
        }

        return true;
    }

    /**
     * Verify HttpService meta
     *
     * @param element Http Service taw type
     * @return verify result
     */
    private boolean verify(Element element) {
        HttpService httpService = element.getAnnotation(HttpService.class);
        // It must be implement the interface RequestHandler and marked with annotation HttpService.
        return null != httpService && ((TypeElement) element).getInterfaces().contains(iRequestHandler);
    }
}

以上是所有要做的事。

相对于ARouter,我们还有什么可以借鉴的呢?

首先是缓存。现在模块和方法比较少,如果多起来了,可以使用LRU方式缓存。

其次是拦截器。权限验证,格式验证等等,都可以放这里。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值