Статьи

Resolvers просмотра цепочек URL в Spring MVC

Стандартная 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.