项目地址:https://github.com/xiaogou446/jsonboot
使用branch:feature/buildPath
命令行:git checkout feature/buildPath
设置@PathVariable注解
在SpringMVC中,我们能了解到 @PathVariable注解的作用,比如设置的路由为"user/{name}",外部传入的path为"/user/qinghuo",那么在name参数用 @PathVariable标注时,会自动映射到{name}对应的"qinghuo",则将"qinghuo"赋值给name。
先设置 @PathVariable注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface PathVariable {
String value();
}
回想一下我们前几节实现请求路由的功能,是通过 @RestController 和 @GetMapping的路由值拼接来映射外部传入的请求path。但是如果使用 @PathVaribale注解,那么这种单纯的映射就无法实现了,我们需要换一种实现方式。
我们可以采用正则表达式的方式来建立路由(user/{name}) 和 path(user/qinghuo)之间的关系。比如我们设置路由(user/{name}),那么我们将{name}设置为正则的匹配任意中文、英文字符、下划线和数字的正则表达式。如路由(user/{name})的正则表达式就是"^/user/[\u4e00-\u9fa5_a-zA-Z0-9]+/?$" 我们暂定为formatUrl。这样我们就能设置一个Map,key为formatUrl, value为路由(user/{name})。 在外部path(user/qinghuo)进行请求时,通过formatUrl与path进行正则匹配,如果能够匹配,代表path(user/qinghuo)和路由(user/{name})是一致的。
所以先修改一下原先url : Method 的map,定义两个map,一个是formatUrl : path,另一个是formatUrl :Method。
/**
* Get方法映射路径 用于@GetMapping路由 存放getFormatUrl : method
*/
public static Map<String, Method> getMethodMapping = new HashMap<>();
/**
* Post方法映射 用于@PostMapping路由 存放postFormatUrl : method
*/
public static Map<String, Method> postMethodMapping = new HashMap<>();
/**
* Get方法 存放getFormatUrl : 原url path
*/
public static Map<String, String> getUrlMapping = new HashMap<>();
/**
* Post方法 存放postFormatUrl : 原url path
*/
public static Map<String, String> postUrlMapping = new HashMap<>();
设置正则表达式替换的方法,用来对path进行正则的替换。
/**
* 定义替换{xx}为匹配中文 英文字母 下划线的正则
* 如 /user/{age} -> ^/user/[\u4e00-\u9fa5_a-zA-Z0-9]+/?$
*
* @param url 拼装路径
* @return 能够匹配{xxx}的正则
*/
public static String formatUrl(String url) {
String originPattern = url.replaceAll("(\\{\\w+})", "[\\\\u4e00-\\\\u9fa5_a-zA-Z0-9]+");
String pattern = "^" + originPattern + "/?$";
return pattern.replaceAll("/+", "/");
}
重新定义一下之前加载路由的方法,将formatUrl添加进去。组成两个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);
String url = baseUri + getMapping.value();
//获取正则 formatUrl
String formatUrl = UrlUtil.formatUrl(url);
getMethodMapping.put(formatUrl, method);
getUrlMapping.put(formatUrl, url);
//如果有getMapping在上面了,则不进行postMapping的使用
continue;
}
if (method.isAnnotationPresent(PostMapping.class)){
PostMapping postMapping = method.getAnnotation(PostMapping.class);
String url = baseUri + postMapping.value();
String formatUrl = UrlUtil.formatUrl(url);
postMethodMapping.put(formatUrl, method);
postUrlMapping.put(formatUrl, url);
}
}
}
到这就将系统初始化的工作完成了,组建了两个map一个formatUrl:path,一个formatUrl:Method,就等外部请求访问即可。
解决了Path的路由映射问题,第二个问题就是如何实现 @PathVariable 注解了。在我们获取到了Path(user/qinghuo)和路由(user/{name})时,可以进行一个整合,通过"/“进行分割,再一一对应,就变成"user: user”,“name: qinghuo”,再整合到一个map中。在处理参数如果有带有 @PathVaribale注解时,就在这个map中通过 @PathVariable的value “name"获取到"qinghuo”。同时我们也需要一个实体存储这个映射到map。
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class MethodDetail {
/**
* 方法类型
*/
private Method method;
/**
* url参数映射 如 /user/{name} 对应 /user/xx user:user name:xx
*/
private Map<String, String> urlParameterMappings;
/**
* 查询参数映射 外部查询 如 name=xx&age=18
*/
private Map<String, String> queryParameterMappings;
/**
* post请求到json数据
*/
private String json;
}
通过MethodDetail,在处理Get请求或者Post请求时,仅需返回MethodDetail,就能代表所有的数据,方便后续的调用。以GetRequestHandler为例:
String uri = fullHttpRequest.uri();
//获取参数map
Map<String, String> queryParamMap = UrlUtil.getQueryParam(uri);
//获取请求path
String path = UrlUtil.convertUriToPath(uri);
ApplicationContext applicationContext = ApplicationContext.getInstance();
//生成methodDetail
MethodDetail methodDetail = applicationContext.getMethodDetail(path, HttpMethod.GET);
if (methodDetail == null || methodDetail.getMethod() == null){
return null;
}
log.info("request path: {}, method: {}", path, methodDetail.getMethod().getName());
methodDetail.setQueryParameterMappings(queryParamMap);
applicationContext.getMethodDetail(path, HttpMethod.GET); 方法如下,也就是生成MethodDetail的方法。
优先通过Get的请求方式或者Post的请求方式使用不同的map。通过遍历getMethodMapping中的每一个formatUrl,对path进行正则的匹配,如果有匹配到一致的path,就将对应数据存储到MethodDetail中,并生成有关@PathVariable的参数对应map。
/**
* 获取请求的MethodDetail
*
* @param requestPath 请求path 如 /user/xxx
* @param httpMethod 请求的方法类型 如 get、post
* @return MethodDetail
*/
public MethodDetail getMethodDetail(String requestPath, HttpMethod httpMethod){
MethodDetail methodDetail = null;
if (HttpMethod.GET.equals(httpMethod)){
methodDetail = buildMethodDetail(requestPath, getMethodMapping, getUrlMapping, getMethodDetailMapping);
}
if (HttpMethod.POST.equals(httpMethod)){
methodDetail = buildMethodDetail(requestPath, postMethodMapping, postUrlMapping, postMethodDetailMapping);
}
return methodDetail;
}
/**
* 生成MethodDetail
*
* @param requestPath 请求路径path
* @param getMethodMapping 存放方法的mapping映射 formatUrl:method
* @param getUrlMapping formatUrl:url
* @param getMethodDetailMapping formatUrl:methodDetail
* @return MethodDetail
*/
private MethodDetail buildMethodDetail(String requestPath, Map<String, Method> getMethodMapping
, Map<String, String> getUrlMapping, Map<String, MethodDetail> getMethodDetailMapping){
MethodDetail methodDetail = new MethodDetail();
//遍历formatUrl与Method
getMethodMapping.forEach((key, value) -> {
//使用之前定义的正则,来判断是否是一个path
Pattern pattern = Pattern.compile(key);
if (pattern.matcher(requestPath).find()){
methodDetail.setMethod(value);
//获取到注解拼成到url
String url = getUrlMapping.get(key);
//生成@PathVariable有用的映射map
Map<String, String> urlParameterMappings = UrlUtil.getUrlParameterMappings(requestPath, url);
methodDetail.setUrlParameterMappings(urlParameterMappings);
getMethodDetailMapping.put(key, methodDetail);
}
});
return methodDetail;
}
当前返回的MethodDetail就相当于是该请求相关的所有数据的大杂烩。有需要的数据直接从MethodDetail中获取即可。后遍历每个参数,如果有参数上标注 @PathVariable注解,则从相关的Map中取出对应的value即可。
PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);
//获取每个参数的类型
Class<?> type = parameter.getType();
//获取pathVariable的value 这里@PathVariable可以设置成无参,用默认的参数值,写文章的时候才发现...
String mappingKey = pathVariable.value();
//以PathVariable的值为key,取出对应的path上的值
String value = methodDetail.getUrlParameterMappings().get(mappingKey);
//反射赋值
return ObjectUtil.convertToClass(type, value);
结构整合
上节也有提到,当我们在处理Post请求的时候,期间也会使用到Get请求的处理注解,如果在Post请求中再写一个Get请求的方法,会导致代码的高度耦合,修改不便等问题。
通过查看Get请求和Post请求的实现方式不难发现,都是先遍历每个参数,对每个参数进行赋值,最后再调用方法实现。假如我们将每个注解的实现方式都提取出去,在遍历每个参数时对已有的几个注解类型进行一个判断,如果是其中某个注解,则返回该注解的注解实现方式,那么代码也会显得更加清晰美观。
定义一个统一参数处理类的接口
public interface ParameterResolver {
/**
* 调用注解的接口
*
* @param methodDetail 调用方法的methodDetail
* @param parameter 使用的参数
* @return 处理后的结果
*/
Object resolve(MethodDetail methodDetail, Parameter parameter);
}
再定义几个实现该接口的类,代表每个注解的实现方式,这里就放两个注解的代码实现,就不都放了,具体可以直接看项目里的代码。
//@PathVariable注解实现
public class PathVariableParameterResolver implements ParameterResolver {
@Override
public Object resolve(MethodDetail methodDetail, Parameter parameter) {
PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);
//获取每个参数的类型
Class<?> type = parameter.getType();
String mappingKey = pathVariable.value();
String value = methodDetail.getUrlParameterMappings().get(mappingKey);
return ObjectUtil.convertToClass(type, value);
}
}
//默认无注解的参数实现方式
public class DefaultParameterResolver implements ParameterResolver {
@Override
public Object resolve(MethodDetail methodDetail, Parameter parameter) {
//获取每个参数的类型
Class<?> type = parameter.getType();
//如果没有注解,则直接进行名称对应查找
String paramValue = methodDetail.getQueryParameterMappings().get(parameter.getName());
return ObjectUtil.convertToClass(type, paramValue);
}
}
这样每个注解的实现类就定义完成了,再去定义一个简单工厂,根据当前参数上的注解来获取对应的解析类。
public class ParameterResolverFactory {
/**
* 根据parameter的类型 获取对应的执行方式
*
* @param parameter 参数
* @return 参数执行器
*/
public static ParameterResolver get(Parameter parameter){
//当前的注解类型是@RequestParam时
if (parameter.isAnnotationPresent(RequestParam.class)){
return new RequestParamParameterResolver();
}
//当前的注解类型是@RequestBody时
if (parameter.isAnnotationPresent(RequestBody.class)){
return new RequestBodyParameterResolver();
}
//当前的注解类型是@PathVariable时
if (parameter.isAnnotationPresent(PathVariable.class)){
return new PathVariableParameterResolver();
}
//都没有或者没有注解时返回默认的处理方式
return new DefaultParameterResolver();
}
}
在GetRequestHandler和PostRequestHandler中,遍历每个参数,根据参数上的注解去工厂中取出对应的处理实现。调用实现类获取参数数据。
//获取到该方法到参数
Parameter[] parameters = methodDetail.getMethod().getParameters();
List<Object> params = new ArrayList<>();
for (Parameter parameter : parameters){
//在参数解析工厂中取出对应的注解处理类型
ParameterResolver parameterResolver = ParameterResolverFactory.get(parameter);
//获取参数
Object result = parameterResolver.resolve(methodDetail, parameter);
params.add(result);
}
测试
到这儿SpringMVC的相关注解实现就差不多了,可以根据自己的需求拓展,下一节开始实现IOC容器。