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

2021-05-26 10:43:18 +08:00
 Aliberter

现在工作中有这样一个需求,需要我把 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;
    }
}

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

3526 次点击
所在节点    Java
24 条回复
taogen
2021-05-26 11:04:09 +08:00
我觉得这样写可以啊,一个 action 不就是一个方法吗。难道 C# 中不用写 200 多个方法(接口)?还是你觉得每个接口都加一个 @RequestMapping 很麻烦?
agzou
2021-05-26 11:06:02 +08:00
切面加自定义注解实现
Aliberter
2021-05-26 11:10:07 +08:00
@taogen 不是,我是觉得每一个方法都要写那三个 @PathVariable 麻烦,本来就是共性的东西,所以想问问怎么实现比较优雅
actar
2021-05-26 11:10:12 +08:00
可以通过转发实现

@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
2021-05-26 11:10:52 +08:00
自己解析 url 然后反射?
TicSmtc
2021-05-26 11:12:28 +08:00
4 楼说的这个法子貌似挺好
timethinker
2021-05-26 11:13:17 +08:00
这里的问题就是把一些原本更适合放在 Header 中的参数放到了 URL 上。

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

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

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

就你这个事,按我的脑回路 优先想到的就是写一个静态的类,每个参数都提供一个静态 get 方法,直接取当前请求上下文中的 url,提取对应变量和默认值,几十行代码半小时搞定,研究 spring 半天不一定搞得定,毕竟人家写的东西代码又多又看不懂😅
agzou
2021-05-26 11:30:21 +08:00
@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
2021-05-26 12:38:22 +08:00
自己写 url 解析写 aop,写拦截器你在自己做类型转换等你写完这个就相当于实现了 spring-mvc 中的路由解析只是没有注解.
huifer
2021-05-26 13:15:15 +08:00
@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
2021-05-26 13:26:19 +08:00
......................................
了解下?
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
// method logic...
}
zliea
2021-05-26 13:35:59 +08:00
Map<String, String> pathVariableRequestMap = (Map<String, String>) nativeWebRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
FreeEx
2021-05-26 14:09:53 +08:00
这个接口设计就很拉跨,安卓看了沉默,iOS 看了流泪,前端出来骂街。
zliea
2021-05-26 14:25:16 +08:00
详细说一下

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

不需要参数的,可以在方法上加入这个注解。需要参数的,在方法参数上加入注解与返回类。
Aliberter
2021-05-26 15:15:19 +08:00
@actar 多谢大佬,采用了你的方法,如有需要留个支付宝我赞助杯咖啡喝,以后还要多请教
Aliberter
2021-05-26 15:16:29 +08:00
@agzou 多谢大佬,我学习学习
Aliberter
2021-05-26 15:17:17 +08:00
@huifer 多谢大佬!!学习下

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/779260

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX