V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Aliberter
V2EX  ›  Java

求助! springboot 如何获取 url 上的参数,@PathVariable 复用问题

  •  
  •   Aliberter · 143 天前 · 2021 次点击
    这是一个创建于 143 天前的主题,其中的信息可能已经有所发展或是发生改变。

    现在工作中有这样一个需求,需要我把 c#的一个项目里的接口都转成 java 实现。这些接口都有共同的请求规则:/{controller}/{action}/{apiVersion}/{userId}/{clientName},举例子比如: http://127.0.0.1:8080/home/index/6.0.0/0/Any. 前面两个参数是 controller 名和方法名,这个我都可以在 @RequestMapping 里写死,但是后面的 apiVersion 、userId 、clientName 这些参数,我不可能在每个 controller 方法的注解上都写上占位符然后用 @PathVariable 获取吧,太 low 了,后期想统一维护都没法维护,而且这些参数要求如果 url 里没有的话要赋默认值。所以问问大佬们,springboot 中如何有没有更好的实现方式呢?无论是拦截器、aop,想来想去都没有太明确的思路。

    原 c#项目里是用路由实现的,这样配置后都会自动去找 controller 里的方法,并且给方法入参赋(默认)值:

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{apiVersion}/{userId}/{clientName}",
        defaults: new
        {
        controller = "Home",
        action = "Index",
        apiVersion = "6.0.0",
        userId = "0",
        clientName = ClientNames.Any
        }
    });
    

    我现在写出来只能是这样的,但我不能 200 多个接口都这样写吧:

    @RestController
    @RequestMapping("/home")
    public class HomePageController {
    
        @RequestMapping("/index/{apiVersion}/{userId}/{clientName}")
        public IndexResponse index(@PathVariable("apiVersion") String apiVersion,
                                   @PathVariable("userId") String userId,
                                   @PathVariable("clientName") String clientName) {
            return null;
        }
    }
    

    真有大佬能提供解决思路,我愿意有偿哈~多谢了

    24 条回复    2021-05-28 07:45:43 +08:00
    taogen
        1
    taogen   143 天前
    我觉得这样写可以啊,一个 action 不就是一个方法吗。难道 C# 中不用写 200 多个方法(接口)?还是你觉得每个接口都加一个 @RequestMapping 很麻烦?
    agzou
        2
    agzou   143 天前
    切面加自定义注解实现
    Aliberter
        3
    Aliberter   143 天前
    @taogen 不是,我是觉得每一个方法都要写那三个 @PathVariable 麻烦,本来就是共性的东西,所以想问问怎么实现比较优雅
    actar
        4
    actar   143 天前   ❤️ 5
    可以通过转发实现

    @RestController
    public class DefaultController {

    @RequestMapping("/{controller}/{action}/{apiVersion}/{userId}/{clientName}")
    public void index(@PathVariable String controller,
    @PathVariable String action,
    @PathVariable String apiVersion,
    @PathVariable String userId,
    @PathVariable String clientName, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    request.setAttribute("apiVersion", apiVersion);
    request.setAttribute("userId", userId);
    request.setAttribute("clientName", clientName);

    request.getRequestDispatcher(String.format("/%s/%s", controller, action)).forward(request, response);
    }

    @RequestMapping("/home/index")
    public String hello(HttpServletRequest request) {
    System.out.println(request.getAttribute("apiVersion"));
    System.out.println(request.getAttribute("userId"));
    System.out.println(request.getAttribute("clientName"));
    return "Hello World!";
    }

    }
    TicSmtc
        5
    TicSmtc   143 天前
    自己解析 url 然后反射?
    TicSmtc
        6
    TicSmtc   143 天前
    4 楼说的这个法子貌似挺好
    qwe520liao
        7
    qwe520liao   143 天前   ❤️ 2
    这里的问题就是把一些原本更适合放在 Header 中的参数放到了 URL 上。

    如果楼主确实需要一种解决方案,我个人的做法可能就是写一个 Filter,然后对 Request 进行包装( HttpServletRequestWrapper )并重写 getRequestURI()方法,相当于 rewrite,把这些 URL 路径参数转移到一个 ThreadLocal 上(或者 Header,总之让它存到另一个地方),然后就可以比较干净的来写 Controller 了。
    taogen
        8
    taogen   143 天前   ❤️ 1
    @Aliberter #3
    我觉得想办法去掉 @PathVariable 没必要,可能有很优雅的方法做到。但是会增加代码的复杂度,增加了一层 HTTP URL 到 controller URL 的转换关系。

    另外,我觉得可以这样实现,我没有验证,只是提供一个思路。
    1 )写一个 filter 。
    2 )在 filter 中 forward 请求,把 URL 中的参数放到 request 中。
    3 )写一个实体类 BaseParam 封装 apiVersion, userId, 和 clientName,controller 接口中用 @ModelAttribute BaseParam baseParam 接收参数。
    rd554259440
        9
    rd554259440   143 天前
    楼上答的不是想要的吧..........用对象接收,把参数都放对象里,不就可以只写一个到处使用了........
    xiangyuecn
        10
    xiangyuecn   143 天前   ❤️ 2
    脱离框架来思考,所有功能都异常简单,非常容易移植

    不然去研究 xx 框架有没有 xx 功能,如果以前会,那还好,不会?学习成本比自己手撸一个框架还高

    就你这个事,按我的脑回路 优先想到的就是写一个静态的类,每个参数都提供一个静态 get 方法,直接取当前请求上下文中的 url,提取对应变量和默认值,几十行代码半小时搞定,研究 spring 半天不一定搞得定,毕竟人家写的东西代码又多又看不懂😅
    agzou
        11
    agzou   143 天前   ❤️ 1
    @Aspect
    @Component
    @RestController
    public class Demo {
    @RequestMapping()
    public void test() {
    //获取参数
    ParamsHolder.getApiVersion();
    }

    @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void pointCut() {

    }

    @Around("pointCut()")
    public Object process(ProceedingJoinPoint pjp) throws Throwable {
    try {
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest req = servletRequestAttributes.getRequest();
    ParamsHolder.setApiVersion(getApiVersion(req));
    return pjp.proceed();
    } finally {
    ParamsHolder.removeApiVersion();
    }

    }

    private String getApiVersion(HttpServletRequest request) {
    // TODO: 2021/5/26 获取需要的参数
    return null;
    }

    public static class ParamsHolder {
    private static final ThreadLocal<String> API_VERSION = new ThreadLocal<>();

    public static String getApiVersion() {
    return API_VERSION.get();
    }

    private static void removeApiVersion() {
    API_VERSION.remove();
    }

    private static void setApiVersion(String apiVersion) {
    API_VERSION.set(apiVersion);
    }
    }
    }
    huifer
        12
    huifer   143 天前
    自己写 url 解析写 aop,写拦截器你在自己做类型转换等你写完这个就相当于实现了 spring-mvc 中的路由解析只是没有注解.
    huifer
        13
    huifer   143 天前   ❤️ 1
    @Aliberter
    解决方案为开启一个独立的 servlet,具体在 springboot 中注入方式如下:

    ```
    @Component
    @ComponentScan("com.example.demo.*")
    public class Beans {
    @Autowired
    private ApplicationContext context;

    @Bean
    public ServletRegistrationBean viewRedisServlet() {
    ServletRegistrationBean<Servlet> servletServletRegistrationBean = new ServletRegistrationBean<>();
    CustomerServlet servlet = new CustomerServlet();
    servlet.setContext(context);

    servletServletRegistrationBean.setServlet(servlet);
    return servletServletRegistrationBean;
    }
    }
    ```

    第二步编写 servlet 具体代码如下:

    ```
    @WebServlet
    public class CustomerServlet extends HttpServlet {
    Gson gson = new Gson();
    private ApplicationContext context;

    public ApplicationContext getContext() {
    return context;
    }

    public void setContext(ApplicationContext context) {
    this.context = context;
    }

    @SneakyThrows @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    // 从请求头中获取一个标记,用于确认是需要进行处理的

    String c = req.getHeader("is_c");
    if (Boolean.valueOf(c)) {

    String contextPath = req.getContextPath();
    String servletPath = req.getServletPath();

    // /{controller}/{action}/{apiVersion}/{userId}/{clientName}
    String requestURI = req.getRequestURI();
    String[] split = requestURI.split("/");

    String controller = null;
    String action = null;
    String apiVersion = null;
    String userId = null;
    String clientName = null;
    if (split.length == 6) {
    controller = split[1];
    action = split[2];
    apiVersion = split[3];
    userId = split[4];
    clientName = split[5];
    }

    // 通过 spring 上下文去搜索 controller + actiron 对应的方法

    ApplicationContext tuUse = this.context;
    if (StringUtils.hasText(controller)) {
    // 找到实例
    Object bean = tuUse.getBean(controller);
    // 找到执行方法
    Method[] methods = bean.getClass().getDeclaredMethods();

    Method toCall = null;
    for (Method method : methods) {
    boolean equals = method.getName().equals(action);
    if (equals) {
    toCall = method;
    break;
    }
    }
    // 获取方法参数类型, 你需要做转换
    Class<?>[] types = toCall.getParameterTypes();

    // 转换后进行参数使用调用方法
    Object invoke = toCall.invoke(bean, apiVersion, userId, clientName);

    resp.setContentType("application/json; charset=UTF-8");
    resp.getWriter().write(gson.toJson(invoke));
    }
    System.out.println(contextPath);
    }
    }

    }
    ```

    上述代代码处理流程:

    1. 判断是需要进行处理的
    2. 将 url 中的 /{controller}/{action}/{apiVersion}/{userId}/{clientName}参数提取
    3. 通过成员变量 context 在 spring 中根据名字获取 bean 实例,名字是 controller,通过 spring 中 Component 注解的 value 进行赋值
    4. 在 bean 实例种搜索 action 对应的方法,这里要求方法名称和 action 强对应。
    5. 将上一步得到的方法提取方法参数,将 url 参数进行类型转换。
    6. 反射执行
    7. response 返回





    其他代码如下:

    ```
    @Data
    public class IndexResponse {
    private int code;
    private Object data;
    }
    ```



    ```
    @Service(value = "home")
    public class HomePageController {

    public IndexResponse index(
    String apiVersion,
    String userId,
    String clientName) {

    IndexResponse response = new IndexResponse();
    response.setCode(1);
    response.setData(apiVersion + "-" +
    userId + "-" +
    clientName);
    return response;
    }

    }
    ```



    测试用例如下:

    GET http://localhost:8080/home/index/6.0.0/0/Any.

    HTTP/1.1 200
    Content-Type: application/json;charset=UTF-8
    Content-Length: 32
    Date: Wed, 26 May 2021 05:14:26 GMT
    Keep-Alive: timeout=60
    Connection: keep-alive

    {
    "code": 1,
    "data": "6.0.0-0-Any."
    }
    ikas
        14
    ikas   143 天前   ❤️ 4
    ......................................
    了解下?
    @PostMapping("/owners/{ownerId}/pets/{petId}/edit")
    public String processSubmit(@ModelAttribute Pet pet) {
    // method logic...
    }
    ztechstack
        15
    ztechstack   143 天前
    Map<String, String> pathVariableRequestMap = (Map<String, String>) nativeWebRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
    FreeEx
        16
    FreeEx   143 天前
    这个接口设计就很拉跨,安卓看了沉默,iOS 看了流泪,前端出来骂街。
    ztechstack
        17
    ztechstack   143 天前   ❤️ 1
    详细说一下

    自定义一个注解,实现 HandlerMethodArgumentResolver 的 Bean,在 @EnableWebMvc 中配置并注入这个 Bean 。
    然后在 resolveArgument 使用
    Map<String, String> pathVariableRequestMap = (Map<String, String>) nativeWebRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
    获取所有 url 参数,进行通用处理,如果接口需要这些数据的,可以返回一个类,里边包含这些通用参数。

    不需要参数的,可以在方法上加入这个注解。需要参数的,在方法参数上加入注解与返回类。
    Aliberter
        18
    Aliberter   143 天前
    @actar 多谢大佬,采用了你的方法,如有需要留个支付宝我赞助杯咖啡喝,以后还要多请教
    Aliberter
        19
    Aliberter   143 天前
    @agzou 多谢大佬,我学习学习
    Aliberter
        20
    Aliberter   143 天前
    @huifer 多谢大佬!!学习下
    actar
        21
    actar   143 天前
    MjEwMDY3MTgxMUBxcS5jb20=
    这是支付宝,也是邮箱。
    有问题可以多多交流。
    @Aliberter
    jorneyr
        22
    jorneyr   143 天前
    这不就是另一个 ServletDispatcher 吗?用拦截器然后再处理吧。
    bringyou
        23
    bringyou   142 天前
    可以用 #14 的 model attribute
    如果路径上的值跟变量名一样,可以省略 @PathVariable 的括号
    MarioLuo
        24
    MarioLuo   141 天前 via Android   ❤️ 1
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2165 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 12:49 · PVG 20:49 · LAX 05:49 · JFK 08:49
    ♥ Do have faith in what you're doing.