Предоставление одного ресурса пути к классам для веб-доступа в Spring Boot

Контекст:

  • Весенняя загрузка/Ява

Данный:

  • ресурс пути к классам, например /src/main/resources/source_dir/source_file.yaml

Проблема:

  • этот файл должен быть доступен по URL-адресу 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");
    }

Конечно, вы можете указать один ресурс. Наличие шаблона не обязательно. Проблема в том, что местоположение ресурса должно быть каталогом (афаик). Хотя вы могли бы попытаться указать полный путь (насколько я вижу, код на самом деле не препятствует этому).

M. Deinum 19.06.2024 11:54

(1) добавьте static (2) пример: /src/main/resources/static/aaa/my-service-spec.yaml , (3) откройте «localhost:8080/aaa/my-service-spec.yaml»

life888888 19.06.2024 12:11

@m-deinum Ссылка на один ресурс с помощью ResourceHandlerRegistry не поддерживается. Он ожидает каталог.

Igor Mukhin 19.06.2024 16:37

@ life888888 Life888888 Ресурс предоставлен другой библиотекой, путь к которой нельзя изменить. И вопрос не в трюках, а в идиоматическом способе Spring Boot для отображения одного ресурса.

Igor Mukhin 19.06.2024 16:39

@IgorMukhin Достаточно ли идиоматично вызов ResourceHandlerRegistration#addResourceLocations(Resource...‌​) и реализация пользовательского ресурса типа SingleResource(String path, Sting location)?

Andrey B. Panfilov 23.06.2024 13:50
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
5
196
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Чтобы достичь того, что вы описываете, нам нужны следующие два шага:

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:», иначе вы раскрываете внутренний путь.

Igor Mukhin 24.06.2024 13:21

Спасибо за предложение синтаксиса пути: «/source_dir/{filename:source_file.yaml}». Я об этом не знал. Знаете ли вы, где я могу найти документы для этого?

Igor Mukhin 24.06.2024 13:22

JavaDoc addResourceHandler: поддерживаются такие шаблоны, как /static/** или /css/{filename:\\w+\\.css}.

Mar-Z 24.06.2024 14:04

Ниже приведена моя попытка использования метода 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?

Igor Mukhin 28.06.2024 11:25

@ИгорьМухин спасибо за указание на это, в том числе application.properties

Dirk Deyne 28.06.2024 15:58

Другие вопросы по теме

Похожие вопросы