Стандартная Java EE перенаправляет на внутренние ресурсы что-то вроде этого:
public class MyServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse resp) {
req.getRequestDispatcher("/WEB-INF/page/my.jsp").forward(req, resp);
}
}
По общему признанию, нет никакого разделения между кодом сервлета и технологией представления, даже с местоположением JSP.
Spring MVC вводит понятие ViewResolver
. Контроллер просто обрабатывает логические имена, сопоставление между логическим именем и фактическим ресурсом обрабатывается с помощью ViewResolver
. Более того, контроллеры полностью независимы от распознавателей: достаточно просто зарегистрировать последние в контексте Spring.
Вот очень простой контроллер, обратите внимание, что нет никаких подсказок относительно конечного местоположения ресурса.
@Controller
public class MyController {
@RequestMapping("/logical")
public String displayLogicalResource() {
return "my";
}
}
Более того, здесь нет ничего, что касается природы ресурса; это может быть JSP, HTML, Tiles, Excel, что угодно. У каждого есть стратегия местоположения, основанная на выделенномViewResolver
. Наиболее используемый распознаватель является InternalResourceViewResolver
; это означало перенаправление на внутренние ресурсы, в большинстве случаев, JSP. Инициализируется так:
@Bean
public ViewResolver htmlViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/page/");
resolver.setSuffix(".jsp");
return resolver;
}
Принимая во внимание этот распознаватель представлений, доступный в контексте Spring, "my"
будет пытаться разрешить логическое имя с помощью "/WEB-INF/page/my.jsp"
пути. Если ресурс существует, хорошо, иначе Spring MVC вернет 404.
А что если у меня разные папки с JSP? Я ожидаю, что смогу настроить два разных преобразователя представления, один с определенным префиксом, другой с другим. Я также ожидаю, что они будут проверены в определенном порядке и откатятся от первого до последнего. Spring MVC предлагает несколько распознавателей с детерминированным порядком , с большой оговоркой: это не относится к InternalResourceViewResolver
!
Цитата Spring MVC Javadoc:
При объединении ViewResolvers, InternalResourceViewResolver всегда должен быть последним, поскольку он будет пытаться разрешить любое имя представления, независимо от того, существует ли базовый ресурс на самом деле.
Это означает, что я не могу настроить два InternalResourceViewResolver
в моем контексте, или, точнее, могу, но первый завершит процесс поиска. Причина (а также фактический код) заключается в том, что распознаватель получает дескриптор RequestDispatcher, настроенный с использованием пути к ресурсу. Только намного позже диспетчер отправляется, только чтобы обнаружить, что его не существует.
Для меня это неприемлемо, так как мой вариант использования является обычным делом. Кроме того, о настройке только "/WEB-INF"
для префикса и возврате остальной части path ( "/page/my"
) не может быть и речи, поскольку в конечном итоге она не отвечает цели отделения логического имени от расположения ресурса. Хуже всего то, что я видел код контроллера, такой как следующий, чтобы справиться с этим ограничением:
return getViews().get("my");
// The controller has a Map view property with "my" as key and the complete path as the "value"
Я думаю, что должен быть еще какой-то Spring-ish способ добиться этого, и я пришел к тому, что я считаю элегантным решением в виде ViewResolver, который проверяет, существует ли ресурс.
public class ChainableUrlBasedViewResolver extends UrlBasedViewResolver {
public ChainableUrlBasedViewResolver() {
setViewClass(InternalResourceView.class);
}
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
String url = getPrefix() + viewName + getSuffix();
InputStream stream = getServletContext().getResourceAsStream(url);
if (stream == null) {
return new NonExistentView();
}
return super.buildView(viewName);
}
private static class NonExistentView extends AbstractUrlBasedView {
@Override
protected boolean isUrlRequired() {
return false;
}
@Override
public boolean checkResource(Locale locale) throws Exception {
return false;
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
// Purposely empty, it should never get called
}
}
}
Моя первая попытка была попытка вернуться null
в buildView()
метод. К сожалению, позже в коде было добавлено несколько NPE. Следовательно, метод возвращает представление о том, что a. говорит вызывающей стороне, что базовый ресурс не существует b. не позволяет проверять его URL-адрес (в некоторый момент происходит сбой, если он не установлен).
Я очень доволен этим решением, так как оно позволяет мне настраивать свой контекст следующим образом:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "ch.frankel.blog.spring.viewresolver.controller")
public class WebConfig {
@Bean
public ViewResolver htmlViewResolver() {
UrlBasedViewResolver resolver = new ChainableUrlBasedViewResolver();
resolver.setPrefix("/WEB-INF/page/");
resolver.setSuffix(".jsp");
resolver.setOrder(0);
return resolver;
}
@Bean
public ViewResolver jspViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/jsp/");
resolver.setSuffix(".jsp");
resolver.setOrder(1);
return resolver;
}
}
Теперь я очень хорошо разбираюсь в философии Spring: я полностью отделен, и я использую упорядочение именных резольверов Spring. Единственным недостатком является то, что один ресурс может скрывать другой, имея одно и то же логическое имя, указывающее на разные ресурсы при разных разрешениях представления. Так как это уже имеет место с несколькими решателями представления, я готов принять риск.
Демонстрационный проект можно найти здесь в формате IntelliJ IDEA / Maven.