Today we had a bit of a challenge ahead of our team. The guys in charge of the network and security of our client asked us to provide a list of all URLs that your application accepts, including the parameters.
Since there are tens of controllers in our application that have tens of methods inside them it was not a manual task to do anymore. After a bit of looking around, I decided to implement it using ApplicationListener
. Any class implementing ApplicationListener
will listen to the corresponding event and when the event occurs, fires the onApplicationEvent
method.
For us, the event was ContextRefreshedEvent
. I wanted to have a list of all request mappings when the application starts up and also I wanted that list updated in case of a context refresh.
So I receive the ContextRefreshedEvent
, extract the ApplicationContext
, get all handler methods and the request mappings, and finally extract the patterns and method parameters. This is my base implementation :
@Log4j
@Component
public class EndpointsListener implements ApplicationListener<ContextRefreshedEvent> {
@SneakyThrows
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ApplicationContext applicationContext = event.getApplicationContext();
List<Mapping> mappings = new ArrayList<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : applicationContext
.getBean(RequestMappingHandlerMapping.class).getHandlerMethods().entrySet()) {
RequestMappingInfo requestMappingInfo = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
Mapping mapping = new Mapping();
mapping.setMethods(requestMappingInfo.getMethodsCondition().getMethods()
.stream().map(Enum::name).collect(Collectors.toSet()));
mapping.setPatterns(requestMappingInfo.getPatternsCondition().getPatterns());
Arrays.stream(handlerMethod.getMethodParameters()).forEach(methodParameter -> {
mapping.getParams().add(methodParameter.getParameter().getType().getSimpleName());
});
mappings.add(mapping);
}
mappings.sort(Comparator.comparing(o -> o.getPatterns().stream().findFirst().orElse("")));
log.info(new ObjectMapper().writeValueAsString(mappings));
}
@Data
public class Mapping {
private Set<String> patterns;
private Set<String> methods;
private List<String> params;
public List<String> getParams(){
if(params == null) {
params = new ArrayList<>();
}
return params;
}
}
}
Enter fullscreen mode Exit fullscreen mode
But there’s a problem: we have quite a lot of view controllers defined in our implementation of WebMvcConfigurer
. Let’s take a look :
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/new").setViewName("home");
registry.addViewController("/home").setViewName("welcome");
registry.addViewController("/help").setViewName("help");
registry.addViewController("/404").setViewName("error_404");
registry.addViewController("/403").setViewName("error_403");
registry.addViewController("/405").setViewName("error_405");
registry.addViewController("/500").setViewName("error_500");
// The list goes on for 57 more lines
}
Enter fullscreen mode Exit fullscreen mode
These mappings will not show up in the event listener (since there’s no method handler I guess). So I had to update my listener to this :
@Log4j
@Component
public class EndpointsListener implements ApplicationListener<ContextRefreshedEvent> {
private final HandlerMapping viewControllerHandlerMapping;
public EndpointsListener(HandlerMapping viewControllerHandlerMapping) {
this.viewControllerHandlerMapping = viewControllerHandlerMapping;
}
@SneakyThrows
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ApplicationContext applicationContext = event.getApplicationContext();
List<Mapping> mappings = new ArrayList<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : applicationContext
.getBean(RequestMappingHandlerMapping.class).getHandlerMethods().entrySet()) {
RequestMappingInfo requestMappingInfo = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
Mapping mapping = new Mapping();
mapping.setMethods(requestMappingInfo.getMethodsCondition().getMethods()
.stream().map(Enum::name).collect(Collectors.toSet()));
mapping.setPatterns(requestMappingInfo.getPatternsCondition().getPatterns());
Arrays.stream(handlerMethod.getMethodParameters()).forEach(methodParameter -> {
mapping.getParams().add(methodParameter.getParameter().getType().getSimpleName());
});
mappings.add(mapping);
}
((SimpleUrlHandlerMapping) viewControllerHandlerMapping).getHandlerMap().forEach((s, o) -> {
Mapping mapping = new Mapping();
mapping.setMethods(Collections.singleton("GET"));
mapping.setPatterns(Collections.singleton(s));
mappings.add(mapping);
});
mappings.sort(Comparator.comparing(o -> o.getPatterns().stream().findFirst().orElse("")));
log.info(new ObjectMapper().writeValueAsString(mappings));
}
@Data
public class Mapping {
private Set<String> patterns;
private Set<String> methods;
private List<String> params;
public List<String> getParams(){
if(params == null) {
params = new ArrayList<>();
}
return params;
}
}
}
Enter fullscreen mode Exit fullscreen mode
By this, I will have a log of all the request mappings and their request parameter types.
The code needs a bit of refactoring for sure:
- Method parameters like HttpServletRequest or HttpSession should be ignored.
- Parameter names are not logged.
- Required status of parameters is not logged.
- PathVariables should be logged separately.
The method handler could just give us parameter names like "arg0"
, "arg1"
and etc. But since our parameters are all annotated by @RequestParam
we can easily get their names by getting the parameter annotation. This way we could log if the parameter is required or not and if it has a default value. The final code looks like this :
/**
* A listener to listen on {@link ContextRefreshedEvent}.
*/
@Log4j
@Component
public class EndpointsListener implements ApplicationListener<ContextRefreshedEvent> {
/**
* Provides access to all view controllers defined in {@link SpringConfig}.
*/
private final HandlerMapping viewControllerHandlerMapping;
/**
* Constructor.
*
* @param viewControllerHandlerMapping View Controller Handler Mapping.
*/
public EndpointsListener(HandlerMapping viewControllerHandlerMapping) {
this.viewControllerHandlerMapping = viewControllerHandlerMapping;
}
/**
* On context refresh, get all request handler mappings and create a list.
* Also get the view controller mappings from {@link #viewControllerHandlerMapping} and add to the list.
*
* @param event A {@link ContextRefreshedEvent}.
*/
@SneakyThrows
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ApplicationContext applicationContext = event.getApplicationContext();
List<CustomMapping> mappings = new ArrayList<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : applicationContext
.getBean(RequestMappingHandlerMapping.class).getHandlerMethods().entrySet()) {
RequestMappingInfo requestMappingInfo = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
CustomMapping mapping = new CustomMapping();
mapping.setMethods(requestMappingInfo.getMethodsCondition().getMethods()
.stream().map(Enum::name).collect(Collectors.toSet()));
mapping.setPatterns(requestMappingInfo.getPatternsCondition().getPatterns());
Arrays.stream(handlerMethod.getMethodParameters()).forEach(methodParameter -> {
CustomParameter parameter = new CustomParameter();
Annotation[] parameterAnnotations = methodParameter.getParameterAnnotations();
Arrays.stream(parameterAnnotations).forEach(annotation -> {
if (annotation instanceof PathVariable) {
PathVariable pathVariable = (PathVariable) annotation;
mapping.getPathVariables()
.add(new CustomPathVariable(pathVariable.name(), pathVariable.required()));
} else if (annotation instanceof RequestParam) {
RequestParam requestParam = (RequestParam) annotation;
parameter.setName(requestParam.name());
String defaultValue = requestParam.defaultValue();
if (!defaultValue.equals(ValueConstants.DEFAULT_NONE)) {
parameter.setDefaultValue(defaultValue);
}
parameter.setRequired(requestParam.required());
parameter.setType(methodParameter.getParameter().getType().getSimpleName());
mapping.getParams().add(parameter);
}
});
});
mappings.add(mapping);
}
((SimpleUrlHandlerMapping) viewControllerHandlerMapping).getHandlerMap().forEach((s, o) -> {
CustomMapping mapping = new CustomMapping();
mapping.setMethods(Collections.singleton("GET"));
mapping.setPatterns(Collections.singleton(s));
mappings.add(mapping);
});
mappings.sort(Comparator.comparing(o -> o.getPatterns().stream().findFirst().orElse("")));
log.info(new ObjectMapper().writeValueAsString(mappings));
}
/**
* Custom mapping class.
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Data
public class CustomMapping {
private Set<String> patterns;
private Set<String> methods;
private List<CustomParameter> params = new ArrayList<>();
private List<CustomPathVariable> pathVariables = new ArrayList<>();
}
/**
* Custom Parameter class
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class CustomParameter {
private String name;
private String type;
private String defaultValue;
private Boolean required;
}
/**
* Custom path variable class.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@AllArgsConstructor
@Data
public class CustomPathVariable {
private String name;
private Boolean required;
}
}
Enter fullscreen mode Exit fullscreen mode
But what if they are changed? What if we add a new controller the day after we declare our URLs to the security guys? So instead of logging the results, I will keep them as a list and write a rest controller for accessing them.
The code is also available as a gist.
原文链接:Logging all request mappings and their parameters in Spring
暂无评论内容