项目地址:https://github.com/xiaogou446/jsonboot
本节延续第一节的内容,branch:feature/annotatedClassScanner
命令行:git checkout feature/annotatedClassScanner
设置请求路由
上节写到请求从Netty建立到Http服务器中进入,并通过Handler请求处理器分别处理Get请求与Post请求,可是Get请求有很多,对应处理的Get方法也有很多,我们需要找到对应请求的请求处理器,再对请求进行处理。
参照SpringBoot中定义路由的注解,由于我们是只返回json的数据,那么直接定义@RestController注解、@GetMapping 和 PostMapping注解进行路由。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface RestController {
String value() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface GetMapping {
String value() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface PostMapping {
String value() default "";
}
通过Reflections定义一个扫描特定路径下该注解类的功能,通过该方法,我们能直到对应路径下比如com.df路径下有 @RestController注解标注的类有哪些。
/**
* 实现路径内注解扫描功能
*
* @param packageName 需要扫描的包路径
* @param annotation 需要在包路径内寻找类的注解
* @return 包路径内包含该注解的类
*/
public Set<Class<?>> scan(String packageName, Class<? extends Annotation> annotation){
//初始化工具类 指定包名和注解对应的类型
Reflections reflections = new Reflections(packageName, new TypeAnnotationsScanner());
//获取某个包下对应功能的注解类
Set<Class<?>> annotationClass = reflections.getTypesAnnotatedWith(annotation, true);
log.info("annotationClass : {}, size: {}", annotationClass, annotationClass.size());
return annotationClass;
}
定义两个Map,用来存储对应的方法路径,比如@RestController("/user") + @GetMapping("/findName") 那么路径为 /user/findName ,map的key为路径,value为Method
/**
* Get方法映射路径
*/
public static Map<String, Method> getMethodMapping = new HashMap<>();
/**
* Post方法映射
*/
public static Map<String, Method> postMethodMapping = new HashMap<>();
那么思路就明确了,先通过反射包扫描到 @RestController所标注的类,记录类路由,后遍历每个类中的方法,如果标有 @GetMapping 或 @PostMapping,就将其中的value值记录,与@RestController中的值拼接,存到对应的Map中。
/**
* 根据注解进行访问路径的拼接
*
* @param packageName 需要进行扫描的包名
*/
public void loadRoutes(String packageName){
AnnotatedClassScanner annotatedScanner = new AnnotatedClassScanner();
Set<Class<?>> scan = annotatedScanner.scan(packageName, RestController.class);
for (Class<?> aClass : scan){
// 从扫描的类中获取该注解信息
RestController restController = aClass.getAnnotation(RestController.class);
String baseUri = restController.value();
Method[] methods = aClass.getMethods();
//获取方法映射
loadMethodRoutes(baseUri, methods);
classMapping.put(baseUri, aClass);
}
System.out.println(classMapping);
System.out.println(getMethodMapping);
System.out.println(postMethodMapping);
}
/**
* 添加方法注解的路径映射
*
* @param baseUri 从类注解中解析的基础uri
* @param methods 该类中的方法
*/
private void loadMethodRoutes(String baseUri, Method[] methods){
for (Method method : methods){
if (method.isAnnotationPresent(GetMapping.class)){
GetMapping getMapping = method.getAnnotation(GetMapping.class);
getMethodMapping.put(baseUri + getMapping.value(), method);
//如果有getMapping在上面了,则不进行postMapping的使用
continue;
}
if (method.isAnnotationPresent(PostMapping.class)){
PostMapping postMapping = method.getAnnotation(PostMapping.class);
postMethodMapping.put(baseUri + postMapping.value(), method);
}
}
}
处理Get请求
上述定义好路由后,也就可以根据对应的请求uri,在GetMapping中找到对应的处理方法,通过反射进行执行。
在处理Get的请求中,同时也会用到 @RequestParam 注解,@RequestParam注解可以做到参数映射的效果,即 @RequestParam的value值可以与外部传入的key对应获取值。
/**
* 用于标注传入参数映射
*
* @author qinghuo
* @since 2021/03/22 9:34
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface RequestParam {
String value();
}
在处理Get请求时,也会解析请求的uri,获取请求uri上的参数,转化为queryParamMap,后将解析的path为key,在之前定义的GetMapping中获取到对应的处理方法。
获取到方法后就是对参数的处理,我们需要定义好每个参数的值与类型,才能进行反射调用方法。遍历每个方法的每一个参数,获取到参数的类型,为后续的参数转换类型埋下伏笔。判断每个参数上是否标有 @RequestParam注解,如果有则获取value值作为key,不然就按默认的参数名称为key,在GetMapping中取到对应参数的值。将值记录后反射调用方法获取执行结果。
@Slf4j
public class GetRequestHandler implements RequestHandler {
@Override
public Object handler(FullHttpRequest fullHttpRequest) {
QueryStringDecoder queryDecoder = new QueryStringDecoder(fullHttpRequest.uri(), Charsets.toCharset(CharEncoding.UTF_8));
//获取参数列表
Map<String, String> queryParamMap = UrlUtils.getQueryParam(queryDecoder);
String path = queryDecoder.path();
Method method = Route.getMethodMapping.get(path);
if (method == null){
return null;
}
log.info("request path: {}, method: {}", path, method.getName());
//获取到该方法到参数
Parameter[] parameters = method.getParameters();
List<Object> params = new ArrayList<>();
for (Parameter parameter : parameters){
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
//获取每个参数的类型
Class<?> type = parameter.getType();
//定义最终参数
String paramValue;
//有注解的情况
if (requestParam != null){
//获取注解设置的value值
String paramKey = requestParam.value();
//以注解设置的value值为key去参数map中取出值
paramValue = queryParamMap.get(paramKey);
}else{
//如果没有注解,则直接进行名称对应查找
paramValue = queryParamMap.get(parameter.getName());
}
params.add(ObjectUtils.convertToClass(type, paramValue));
}
return ReflectionUtil.executeMethod(method, params.toArray());
}
}
其实这里会出现一个问题!在我们获取到参数并存入List params时需要进行一个参数的转换,从uri上切取下的参数是String类型的,而我们最终需要的却是其他类型,这必须进行一个参数的转换。一开始没有找到什么好的方式…只能自己使用一个比较愚蠢的方法,后来看了大佬的实现才明白还有这种操作。
/**
* 转换String类型对象为目标类型对象
*
* @param targetClass 需要转换的目标类型
* @param content String文本对象
* @return 转换后的对象
*/
public static Object convertToClass(Class<?> targetClass, String content){
PropertyEditor editor = PropertyEditorManager.findEditor(targetClass);
editor.setAsText(content);
return editor.getValue();
}
/**
* 用于将String对象转换为需要的类型对象(已废弃,使用PropertyEditorManager代替)
*
* @param type 原类型
* @param str 需要转换的字符串
* @return 转换后的对象
*/
@Deprecated
public static Object getNumber(Class<?> type,String str) {
Class<?>[] paramsClasses = { str.getClass() };
Object[] params = { str };
Class<?> typeClass = ObjectUtils.convertBaseClass(type);
Object o = null;
try {
Constructor<?> c = typeClass.getConstructor(paramsClasses);
o = c.newInstance(params);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
return o;
}
/**
* 如果是基本数据类型就转换为其包装类 (已废弃)
*
* @param type 传入的类型
* @return 处理后的类型
*/
@Deprecated
public static Class<?> convertBaseClass(Class<?> type){
switch (type.getTypeName()){
case "int":
return Integer.class;
case "short":
return Short.class;
case "long":
return Long.class;
case "boolean":
return Boolean.class;
case "byte":
return Byte.class;
case "float":
return Float.class;
case "char":
return Character.class;
case "double":
return Double.class;
default:
return type;
}
}
测试
本章基本完成了请求路由的设置以及部分基本注解的实现。