Статьи

Реализация Spring MVC с CDI и Java EE 6 часть 1

Во второй статье о реализации Spring MVC в Java EE 6 мы возьмем метаданные, которые мы извлекли в первой части, и будем использовать их для вызова методов контроллера сопоставленных запросов в ответ на веб-запросы, а затем направим пользователя на веб-страницу в зависимости от результата. метода.

Мы будем реализовывать следующие фрагменты кода:

  • Напишите сервлет, который будет отправлять запросы в наш класс обработчика MVC.
  • В обработчике MVC мы возьмем веб-запрос и вызовем соответствующий метод контроллера.
  • Возьмите результат из сопоставленного с запросом метода и разрешите его в представление, в которое пользователь перенаправляется

Наш код сервлета принимает входящие запросы и делегирует их внедренному экземпляру MvcHandler.

@WebServlet(urlPatterns = "/demo/*")
public class DelegatingServlet extends HttpServlet {

    @Inject
    private MvcHandler mvcHandler;

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doHandleRequest(req, resp, RequestMethod.GET);
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doHandleRequest(req, resp, RequestMethod.POST);
    }

    private void doHandleRequest(HttpServletRequest request, HttpServletResponse response,RequestMethod requestMethod) {
        mvcHandler.handleRequest(request.getPathInfo(),requestMethod,request,response,getServletContext());
    }
}

Наш сервлет использует аннотацию @WebServlet, чтобы зарегистрировать сервлет для пути / demo / * url. Он внедряет экземпляр нашего класса MvcHandler и использует его для обработки запросов GET и POST. Когда мы вызываем обработчик MVC, мы должны передать несколько объектов, которые будут использоваться обработчиком. Забегая вперёд, мы увидим, что это будет расти, так как у нас есть методы контроллера, значения моделей, результаты и имена представлений для передачи, поэтому мы создадим новый объект с именем RequestContext, который будет держать все эти вещи, и мы можем передать все эти предметы как один объект. Это заставляет наши вызовы методов выглядеть лучше с меньшим количеством параметров, и нам не нужно постоянно добавлять параметры в методы, поскольку мы понимаем, что метод нуждается в новой части информации.

Вместо этого мы можем добавить атрибут в контекст запроса и сделать его доступным в любом месте конвейера. Работа с одним контекстом означает, что мы можем разбить обработчик до определенного набора шагов, при этом произведение каждого метода (то есть выборки значений модели) будет храниться в контексте и использоваться в следующем методе. Это также означает, что мы можем позже преобразовать его в интерфейс и / или абстрагировать некоторую информацию, доступную для обеспечения различных реализаций (например, версия портала). На данный момент нам просто нужна базовая версия с контекстом сервлета, объектами запроса и ответа. Нам также нужно сохранить контроллер, метод контроллера и результат этого метода.

public class RequestContext {

    private final ServletContext servletContext;
    private final HttpServletRequest request;
    private final HttpServletResponse response;
    private final RequestMethod requestMethod;
    private Object controller;
    private ControllerMethod method;
    private Object outcome;

    public RequestContext(ServletContext servletContext,
            HttpServletRequest request, HttpServletResponse response,
            RequestMethod requestMethod) {
        this.servletContext = servletContext;
        this.request = request;
        this.response = response;
        this.requestMethod = requestMethod;
    }

    //getters and setters omitted
}

Возвращаясь к нашему обработчику Mvc, код этого класса собирает все вместе и организует вещи, когда он получает вызовы от сервлета.

public class MvcHandler {

    @Inject
    private ControllerInfo controllerInfo;

    @Inject
    private ControllerMethodMatcher matcher;

    @Inject
    private BeanManager beanManager;    

    public void handleRequest(RequestContext context) {
        //find the method matching this request
        context.setMethod(matcher.findMatching(controllerInfo, context));
        if (context.getMethod() != null) {
            context.setController(locateController(context.getMethod().getControllerClass()));
            Object outcome = execute(controller,context.getMethod().getMethod(),null);
            context.setOutcome(outcome);
            handleResponse(context);
        } else {
            throw new RuntimeException("Unable to find method for " + context.getRequestMethod() + " request with url " + context.getPath());
        }
    }

    private void handleResponse(RequestContext context) {
        if (context.getOutcome() instanceof String) {
            String outcome = (String) context.getOutcome();
            String view = "/"+outcome+".jsp";
            context.forwardTo(view);
        }
    }

    private Object locateController(Class<?> controllerClass) {
          //returns a bean of type controllerClass from CDI
    }

    private Object execute(Object instance,Method javaMethod, Object[] params) {
       //executes the java method on this instance with these params
    }
}

Мы внедряем наш экземпляр ControllerInfo, который содержит все методы контроллера, и мы определили наш метод handleRequest (), который получает веб-запрос от сервлета и выполняет следующие шаги:

  • Найдите соответствующий метод контроллера для этого запроса.
  • Найдите контроллер
  • Выполните метод контроллера на этом экземпляре контроллера, сохранив результат, возвращенный методом.
  • Обработайте правильный ответ обратно пользователю.

Пока что мы просто предполагаем, что метод контроллера возвращает строку, которая указывает имя представления, которое мы пересылаем. Я добавил метод для обработки пересылки в контексте запроса.

Методы соответствия

ControllerMethodMatcher — это интерфейс, который можно использовать для поиска метода контроллера в списке методов контроллера, который соответствует информации входящего запроса, хранящейся в контексте запроса.

public interface ControllerMethodMatcher {
    ControllerMethod findMatching(ControllerInfo info,RequestContext context);
}

Наша стандартная реализация этого пока очень проста. Мы перебираем наш список методов контроллера и проверяем, соответствует ли тип запроса методу запроса входящего запроса. Если это так, то если путь URL начинается с префикса уровня контроллера и заканчивается суффиксом уровня метода, то это совпадение. Поскольку мы уже отсортировали методы контроллера по порядку более крупных выражений, мы знаем, что первое совпадение, с которым мы столкнулись, на данный момент является лучшим.

public class DefaultControllerMatcher implements ControllerMethodMatcher {

    public ControllerMethod findMatching(ControllerInfo info,RequestContext context) {
        for (ControllerMethod method : info.getControllerMethods()) {
            if (matches(method, context)) {
                return method;
            }
        }
        return null;
    }

    protected boolean matches(ControllerMethod methodToTest, RequestContext context) {
        String path = context.getPath();
        boolean result = methodToTest.matchesRequestMethod(context.getRequestMethod())
            && (methodToTest.getPrefix() == null || path.startsWith(methodToTest.getPrefix()))
            && (methodToTest.getSuffix() == null || path.endsWith(methodToTest.getSuffix()));
        return result;
    }
}

Мы можем переопределить этот класс и позже повторно реализовать метод совпадений, и, поскольку мы передадим RequestContext, у нас будет вся информация, доступная нам для определения наилучшего соответствия. Одним из примеров использования контекста запроса является передача параметров в URL (т. Е. People \ {id} \), которые необходимо будет учитывать в сопоставлении, а также, возможно, извлекать и сохранять в контексте запроса.

Выполнение метода контроллера

Сейчас мы можем просто использовать отражение для выполнения экземпляра метода контроллера. Значение параметра, переданное в, в настоящее время фиксируется как нулевое.

private Object execute(Object instance,Method javaMethod, Object[] params) {
    return javaMethod.invoke(instance,params);
}

Забегая вперед, нам нужно реализовать внедрение параметров, чтобы мы могли внедрять модели, запросы http и другие объекты в качестве параметров в методы нашего контроллера. У нас также будут другие типы методов, которые нам нужно будет вызывать с внедрением параметров. Например, аннотированные методы ModelAttribute будут вызываться для получения значений модели, и в них могут быть введены значения. Как бы то ни было, мы делаем это красиво и просто.

Видя это в действии

Наконец, мы можем собрать все это вместе, чтобы увидеть это в действии в веб-приложении. На данный момент наш код MVC находится в нашем веб-проекте, чтобы упростить его интеграцию. Позже мы можем извлечь код MVC в отдельный модуль для использования в качестве библиотеки в других проектах. Мы уже настроили контроллер и наш сервлет, теперь нам просто нужно создать пару страниц на основе строки, возвращаемой из сопоставленных методов. Если мы запустим приложение сейчас и перейдем по URL-адресу, такому как http: // localhost: 8080 / mvcdi / demo / person / list, мы получим сообщение об ошибке, поскольку listPeople.jsp не существует. Мы создадим следующие простые страницы JSP, чтобы мы могли что-то отобразить в браузере. Каждая страница состоит из стандартной структуры и некоторого текста, который указывает, на какой странице мы находимся.

<html>
    <head>
        <title>List People</title>
    </head>
    <body>
        List People (this text changes per page)
    </body>
</html>

Мы делаем это для следующих страниц в корне веб-каталога.

  • viewPerson.jsp
  • listPeople.jsp
  • updatedPerson.jsp
  • editPerson.jsp (дополнительные изменения см. ниже)

Одна другая страница — это страница editPerson.jsp, на которой мы хотим разместить форму, содержащую только кнопку, чтобы мы могли выполнить запрос POST, который сопоставляется с методом на контроллере, который обрабатывает запрос POST со страницы editPerson.jsp, и переходит к обновленная страницаPerson.jsp в ответ.

<html>
    <head>
        <title>Editing Person</title>
    </head>
    <body>
        Editing Person
        <form method="post">
            Some Form Here<br/>
            <input type="submit" value="Update" />
        </form>
    </body>
</html>

If we go to http://localhost:8080/mvcdi/demo/person/edit and click the submit button, we get sent to the page that tells us we just updated someone because that is the page reference returned from the controller method for the edit path with a POST request method.

This wraps up the second installment of this series on implementing Spring MVC in Java EE 6 and CDI and covers the bulk of the request mapping so we can direct our web requests to controller methods and ultimately to specific pages. Next time we’ll look at providing model data to the pages.
In this second article on implementing Spring MVC in Java EE 6 we’ll take the metadata we extracted in part one and use it to invoke request mapped controller methods in response to web requests and then direct the user to a web page based on the result of the method. [more]

We’ll be implementing the following pieces of code :

  • Write a servlet that will dispatch the requests to our MVC handler class.
  • In the MVC Handler, we’ll take a web request and invoke the appropriate controller method
  • Take the result from the request mapped method and resolve it to a view which the user is forwarded to

Our servlet code takes incoming requests and delegates them to the injected MvcHandler instance.

@WebServlet(urlPatterns = "/demo/*")
public class DelegatingServlet extends HttpServlet {

    @Inject
    private MvcHandler mvcHandler;

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doHandleRequest(req, resp, RequestMethod.GET);
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doHandleRequest(req, resp, RequestMethod.POST);
    }

    private void doHandleRequest(HttpServletRequest request, HttpServletResponse response,RequestMethod requestMethod) {
        mvcHandler.handleRequest(request.getPathInfo(),requestMethod,request,response,getServletContext());
    }
}

Our servlet uses the @WebServlet annotation to register the servlet for the /demo/* url path. It injects the instance of our MvcHandler class and uses it to handle the GET and POST requests. When we call the MVC handler, we have to pass in multiple objects that will be used by the handler. Looking ahead, we can see this is going to grow since we have controller methods, model values, outcomes and view names to pass around so we’ll create a new object called a RequestContext that will keep a hold of all these things and we can pass all those items around as a single object.

It makes our method calls look nicer with fewer parameters and we don’t have to keep adding parameters to methods as we realize that the method needs a new piece of information. Instead, we can add an attribute to the request context and make it available anywhere in the pipeline. Working with a single context means we can break the handler down to a specific set of steps with the product of each method (i.e. fetch model values) being held in the context and used in the next method. It also means we can convert it to an interface and/or abstract some of the information available to provide different implementations (i.e portal version). For now, we just need a basic version with a servlet context, request, response objects. We’ll also need to store the controller, the controller method and the outcome from that method.

public class RequestContext {

    private final ServletContext servletContext;
    private final HttpServletRequest request;
    private final HttpServletResponse response;
    private final RequestMethod requestMethod;
    private Object controller;
    private ControllerMethod method;
    private Object outcome;

    public RequestContext(ServletContext servletContext,
            HttpServletRequest request, HttpServletResponse response,
            RequestMethod requestMethod) {
        this.servletContext = servletContext;
        this.request = request;
        this.response = response;
        this.requestMethod = requestMethod;
    }

    //getters and setters omitted
}

Going back to our Mvc Handler, the code in this class pulls everything together and orchestrates things as it receives calls from the servlet.

public class MvcHandler {

    @Inject
    private ControllerInfo controllerInfo;

    @Inject
    private ControllerMethodMatcher matcher;

    @Inject
    private BeanManager beanManager;    

    public void handleRequest(RequestContext context) {
        //find the method matching this request
        context.setMethod(matcher.findMatching(controllerInfo, context));
        if (context.getMethod() != null) {
            context.setController(locateController(context.getMethod().getControllerClass()));
            Object outcome = execute(controller,context.getMethod().getMethod(),null);
            context.setOutcome(outcome);
            handleResponse(context);
        } else {
            throw new RuntimeException("Unable to find method for " + context.getRequestMethod() + " request with url " + context.getPath());
        }
    }

    private void handleResponse(RequestContext context) {
        if (context.getOutcome() instanceof String) {
            String outcome = (String) context.getOutcome();
            String view = "/"+outcome+".jsp";
            context.forwardTo(view);
        }
    }

    private Object locateController(Class<?> controllerClass) {
          //returns a bean of type controllerClass from CDI
    }

    private Object execute(Object instance,Method javaMethod, Object[] params) {
       //executes the java method on this instance with these params
    }
}

We inject our instance of ControllerInfo which holds all the controller methods and we have defined our handleRequest() method that receives a web request from the servlet and performs the following steps :

  • Find a matching controller method for this request.
  • Locate the controller
  • Execute the controller method on that controller instance saving the outcome returned from the method.
  • Handle the correct response back to the user.

For now, we are just assuming the controller method returns a string that indicates the view name which we forward to. I added a method to handle forwarding on the request context.

Matching Methods

The ControllerMethodMatcher is an interface that can be used to locate a controller method in the list of controller methods that matches the incoming request info held in the request context

public interface ControllerMethodMatcher {
    ControllerMethod findMatching(ControllerInfo info,RequestContext context);
}

Our default implementation of this is really simple for now. We iterate through our list of controller methods and check that the request type matches the request method of the incoming request. If it does, then as long as the url path starts with the controller level prefix and ends with the method level suffix, then it is a match. Because we already sorted the controller methods in order of the larger expressions first, we know that the first match we come across is the best for now.

public class DefaultControllerMatcher implements ControllerMethodMatcher {

    public ControllerMethod findMatching(ControllerInfo info,RequestContext context) {
        for (ControllerMethod method : info.getControllerMethods()) {
            if (matches(method, context)) {
                return method;
            }
        }
        return null;
    }

    protected boolean matches(ControllerMethod methodToTest, RequestContext context) {
        String path = context.getPath();
        boolean result = methodToTest.matchesRequestMethod(context.getRequestMethod())
            && (methodToTest.getPrefix() == null || path.startsWith(methodToTest.getPrefix()))
            && (methodToTest.getSuffix() == null || path.endsWith(methodToTest.getSuffix()));
        return result;
    }
}

We can override this class, and re-implement the matches method later on and since we pass in the RequestContext we will have all sorts of information available to us to determine the best match. One example of the use of the request context is when we pass parameters in the URL (i.e. people\{id}\), which will need to be accounted for in the match and also possibly extracted and saved in the request context.

Executing the Controller Method

For now, we can just use reflection to execute the controller method instance. The params value passed in is currently fixes as null.

private Object execute(Object instance,Method javaMethod, Object[] params) {
    return javaMethod.invoke(instance,params);
}

Looking ahead, we will need to implement parameter injection so we can inject models, http requests and other objects as parameters to our controller methods. We will also have other types of methods that we will need to call with parameters injection. For example, ModelAttribute annotated methods will be called to retrieve model values and may have values injected into it. As it is though, we are keeping it nice and simple.

Seeing it in action

Finally we can put it all together so we can see it in action in a web application. For now our MVC code is in our web project to make integrating it easier. We can later extract the MVC code to a stand-alone module to use as a library in other projects. We’ve already set up a controller and our servlet, now we just need to create a couple of pages based on the string returned from the mapped methods. If we run the application now and go to a url such as http://localhost:8080/mvcdi/demo/person/list we will get an error message because listPeople.jsp doesn’t exist. We’ll create the following simple jsp pages so we can display something in the browser. Each page just consists of the boilerplate structure and some text that indicates which page we are on.

<html>
    <head>
        <title>List People</title>
    </head>
    <body>
        List People (this text changes per page)
    </body>
</html>

We do this for the following pages in the root of the web directory.

  • viewPerson.jsp
  • listPeople.jsp
  • updatedPerson.jsp
  • editPerson.jsp (see below for additional changes)

The one different page is the editPerson.jsp page where we want to put a form containing only a button so we can issue a POST request which maps to the method on the controller that handles the POST request from the editPerson.jsp page and navigates to the updatedPerson.jsp page in response.

<html>
    <head>
        <title>Editing Person</title>
    </head>
    <body>
        Editing Person
        <form method="post">
            Some Form Here<br/>
            <input type="submit" value="Update" />
        </form>
    </body>
</html>

If we go to http://localhost:8080/mvcdi/demo/person/edit and click the submit button, we get sent to the page that tells us we just updated someone because that is the page reference returned from the controller method for the edit path with a POST request method.

This wraps up the second installment of this series on implementing Spring MVC in Java EE 6 and CDI and covers the bulk of the request mapping so we can direct our web requests to controller methods and ultimately to specific pages. Next time we’ll look at providing model data to the pages.

 

From http://www.andygibson.net/blog/article/implementing-spring-mvc-with-cdi-and-java-ee-6/