Контекст:
Данный:
/src/main/resources/source_dir/source_file.yamlПроблема:
http://localhost:8080/target_dir/target_file.yamlКаков идиоматический способ добиться этого в Spring Boot?
Я знаю, что есть способ сопоставить каталог пути к классам с веб-путем, настроив ResourceHandlerRegistry, но это не работает для одного ресурса, и вы не можете дать ресурсу другое имя:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/target_dir/**")
.addResourceLocations("classpath:/source_dir/");
}
В настоящее время я использую трюк с пересылкой в контроллере, чтобы файл можно было сопоставить, но это некрасиво:
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/target_dir/target_file.yaml")
.setViewName("forward:/source_dir/source_file.yaml");
}
Я ожидал, что код, подобный приведенному ниже, будет работать, но, согласно исходному коду Spring, он может обрабатывать только каталоги:
// is not working with Spring Boot
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/target_dir/target_file.yaml")
.addResourceLocations("classpath:/source_dir/source_file.yaml");
}
(1) добавьте static (2) пример: /src/main/resources/static/aaa/my-service-spec.yaml , (3) откройте «localhost:8080/aaa/my-service-spec.yaml»
@m-deinum Ссылка на один ресурс с помощью ResourceHandlerRegistry не поддерживается. Он ожидает каталог.
@ life888888 Life888888 Ресурс предоставлен другой библиотекой, путь к которой нельзя изменить. И вопрос не в трюках, а в идиоматическом способе Spring Boot для отображения одного ресурса.
@IgorMukhin Достаточно ли идиоматично вызов ResourceHandlerRegistration#addResourceLocations(Resource...) и реализация пользовательского ресурса типа SingleResource(String path, Sting location)?




Чтобы достичь того, что вы описываете, нам нужны следующие два шага:
1 - Чтобы добавить каталог в разрешенные места
По умолчанию Spring Boot настраивает отображение следующих каталогов:
classpath:/META-INF/resources/webjars/
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
Чтобы добавить еще один каталог пути к классам (в вашем случае /source_dir/), добавьте этот класс конфигурации:
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/source_dir/{filename:source_file.yaml}")
.addResourceLocations("classpath:/source_dir/");
}
}
Обратите внимание, что обработчик ресурсов предназначен только для одного конкретного ресурса. Другие файлы из каталога не будут доступны.
2 - Настроить переименование URL-адреса файлового ресурса.
Это можно сделать с помощью следующего кода, добавленного в приведенный выше класс конфигурации:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/target_dir/target_file.yaml")
.setViewName("forward:/source_dir/source_file.yaml");
}
С учетом вышеперечисленных изменений вы получите содержимое файла /source_dir/source_file.yaml, вызвав
GET localhost:8080/target_dir/target_file.yaml
Обратите внимание: вы уже публиковали подобный код, но по каким-то причинам он показался вам некрасивым. По моему мнению, это не так. Он использует только открытые и документированные методы обратного вызова интерфейса WebMvcConfigurer.
Я считаю это не идеальным по некоторым причинам: 1) он также предоставляет ресурс по внутреннему пути localhost:8080/source_dir/source_file.yaml. Это нежелательное поведение. 2) Конфигурация не самая простая для понимания. Кстати, это должно быть «forwand:», а не «redirect:», иначе вы раскрываете внутренний путь.
Спасибо за предложение синтаксиса пути: «/source_dir/{filename:source_file.yaml}». Я об этом не знал. Знаете ли вы, где я могу найти документы для этого?
JavaDoc addResourceHandler: поддерживаются такие шаблоны, как /static/** или /css/{filename:\\w+\\.css}.
Ниже приведена моя попытка использования метода ResourceHandlerRegistration#addResourceLocations(Resource...):
@Component
public class SingleResourceFactory implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public Resource singleResource(String path, String location) {
return (Resource) Proxy.newProxyInstance(
Resource.class.getClassLoader(),
new Class<?>[]{Resource.class},
new SingleResourceInvocationHandler(path, location));
}
private class SingleResourceInvocationHandler implements InvocationHandler {
private final String path;
private final String location;
public SingleResourceInvocationHandler(String path, String location) {
this.path = path;
this.location = location;
}
protected Resource getResource() {
// todo: do we need to cache result?
return resourceLoader.getResource(location);
}
protected Resource createRelative(Resource proxyObject, String relativePath) throws IOException {
if (path.equals(relativePath)) {
return proxyObject;
}
return NonExistingResource.INSTANCE;
}
@Override
public Object invoke(Object proxyObject, Method method, Object[] args) throws Throwable {
if (ReflectionUtils.isEqualsMethod(method)) {
return (proxyObject == args[0]);
} else if (ReflectionUtils.isHashCodeMethod(method)) {
return hashCode();
} else if (ReflectionUtils.isToStringMethod(method)) {
return "SingleResource proxy: " + path + " -> " + location;
}
if ("createRelative".equals(method.getName())) {
return createRelative((Resource) proxyObject, (String) args[0]);
}
return method.invoke(getResource(), args);
}
}
static class NonExistingResource extends AbstractResource {
static final Resource INSTANCE = new NonExistingResource();
@Override
public String getDescription() {
return "Non existing";
}
@Override
public InputStream getInputStream() throws IOException {
throw new UnsupportedEncodingException();
}
@Override
public boolean exists() {
return false;
}
@Override
public boolean isReadable() {
return false;
}
}
}
@Autowired
private SingleResourceFactory singleResourceFactory;
@Bean
@Order(Ordered.LOWEST_PRECEDENCE - 2)
public WebMvcConfigurer testResourceConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/test/1.txt")
.addResourceLocations(singleResourceFactory.singleResource("/test/1.txt", "classpath:/application.properties"));
}
};
}
Вероятно, самое простое решение — использовать метод контроллера, который возвращает ResponseEntity вместе с Resource:
@Controller
public class ResourceMappingController {
@GetMapping("/target_dir/target_file.yaml")
public ResponseEntity<Resource> get() {
return ResponseEntity.ok().body(
new ClassPathResource("/source_dir/source_file.yaml"));
}
}
Кредит: идея решения была предложена Ангелом Ионутом, но он удалил свой ответ по нераскрытой причине.
ОБНОВЛЯТЬ
Джош Лонг показал мне, что можно просто вернуть Resource с контроллера:
@Controller
public class ResourceMappingController {
private final Resource resource =
new ClassPathResource("/source_dir/source_file.yaml")
@GetMapping("/target_dir/target_file.yaml", produces = "text/yaml")
@ResponseBody
public Resource get() {
return resource;
}
}
Мы можем добавить новый SingleUrlHandlerMapping, который сопоставляет целевой путь с постоянным ресурсом пути к классам.
@Configuration
class ExampleConfiguration {
@Bean
public HandlerMapping handlerMapping() throws Exception {
var resource = new ClassPathResource("abc/xyz.html");
var path = "foo/bar.html";
var requestHandler = new ResourceHttpRequestHandler();
requestHandler.setResourceResolvers(List.of(new ConstantResourceResolver(resource)));
requestHandler.afterPropertiesSet();
var handlerMapping = new SimpleUrlHandlerMapping(Map.of(path, requestHandler));
handlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
return handlerMapping;
}
static class ConstantResourceResolver implements ResourceResolver {
private final Resource resource;
public ConstantResourceResolver(Resource resource) {
this.resource = resource;
}
@Override
public Resource resolveResource(HttpServletRequest request, String requestPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
return resource;
}
@Override
public String resolveUrlPath(String resourcePath, List<? extends Resource> locations,
ResourceResolverChain chain) {
return resourcePath;
}
}
}
Самый простой способ обслуживать один ресурс — использовать RestContolller
Пример
@RestController
class SingleYamlResourceController {
final Resource resource;
SingleYamlResourceController(@Value("${my.resourse:optional_default_path}") Resource resource) {
this.resource = resource;
}
// produces => "application/yaml" download via browser
// produces => "text/yaml" human readable in browser
@GetMapping(value = "target_dir/target_file.yaml", produces = "application/yaml")
public Resource yaml() {
return resource;
}
}
application.properties
my.resourse=classpath:/source_dir/target_file.yaml
Это позволяет вам настроить путь к вашему ресурсу.
Какой смысл внедрять ресурс как @Value, а не просто создавать его с помощью new ClassPathResource?
@ИгорьМухин спасибо за указание на это, в том числе application.properties
Конечно, вы можете указать один ресурс. Наличие шаблона не обязательно. Проблема в том, что местоположение ресурса должно быть каталогом (афаик). Хотя вы могли бы попытаться указать полный путь (насколько я вижу, код на самом деле не препятствует этому).