做过几个需要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方式缓存。
其次是拦截器。权限验证,格式验证等等,都可以放这里。