一起来写个SpringBoot[4] — — 实现@PathVariable注解

项目地址: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容器。

下一节:一起来写个SpringBoot[5] — — 实现一个简单的IOC

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值