1. 概述

在这个教程中,我们将深入探讨前端控制器模式,这是企业模式中马丁·福勒(Martin Fowler)在《企业应用架构模式》一书中定义的一部分。

前端控制器被定义为“处理网站所有请求的控制器”。它位于Web应用程序的前端,将请求分发给后续资源,并提供诸如安全性、国际化以及向特定用户展示特定视图等通用行为的接口。

这使得应用程序能够在运行时改变其行为,并有助于阅读和维护代码,防止代码重复。

前端控制器通过单个处理器对象集中处理所有请求。

2. 它的工作原理是什么?

前端控制器模式主要分为两个部分:一个单一的调度控制器和一个命令层次结构。以下UML图展示了通用前端控制器实现的类关系:

这个单一的控制器将请求分发给命令,以触发与请求关联的行为。

为了演示其实现,我们将在这个教程中实现一个FrontControllerServlet控制器,并将命令作为继承自抽象FrontCommand类的类。

3. 设置

3.1. Maven依赖

首先,我们将在一个新的Maven WAR项目中设置依赖,包括javax.servlet-api

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.0-b01</version>
    <scope>provided</scope>
</dependency>

以及jetty-maven-plugin

<plugin>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>9.4.0.M1</version>
    <configuration>
        <webApp>
            <contextPath>/front-controller</contextPath>
        </webApp>
    </configuration>
</plugin>

3.2. 模型

接下来,我们将定义一个Model类和一个模型Repository。我们将使用以下Book类作为我们的模型:

public class Book {
    private String author;
    private String title;
    private Double price;

    // standard constructors, getters and setters
}

这是仓库,你可以查看源代码以获取具体实现,或者自己提供一个:

public interface Bookshelf {
    default void init() {
        add(new Book("Wilson, Robert Anton & Shea, Robert", 
          "Illuminati", 9.99));
        add(new Book("Fowler, Martin", 
          "Patterns of Enterprise Application Architecture", 27.88));
    }

    Bookshelf getInstance();

    <E extends Book> boolean add(E book);

    Book findByTitle(String title);
}

3.3. FrontControllerServlet

Servlet本身的实现相当简单。我们从请求中提取命令名称,动态创建一个命令类的新实例并执行它。

这允许我们在不更改前端控制器代码库的情况下添加新命令。

另一种选择是使用静态条件逻辑来实现Servlet。这具有编译时错误检查的优点:

public class FrontControllerServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, 
      HttpServletResponse response) {
        FrontCommand command = getCommand(request);
        command.init(getServletContext(), request, response);
        command.process();
    }

    private FrontCommand getCommand(HttpServletRequest request) {
        try {
            Class type = Class.forName(String.format(
              "com.baeldung.enterprise.patterns.front." 
              + "controller.commands.%sCommand",
              request.getParameter("command")));
            return (FrontCommand) type
              .asSubclass(FrontCommand.class)
              .newInstance();
        } catch (Exception e) {
            return new UnknownCommand();
        }
    }
}

3.4. FrontCommand

让我们实现一个名为FrontCommand的抽象类,其中包含所有命令共享的行为。

这个类可以访问ServletContext及其请求和响应对象。此外,它还将处理视图解析:

public abstract class FrontCommand {
    protected ServletContext context;
    protected HttpServletRequest request;
    protected HttpServletResponse response;

    public void init(
      ServletContext servletContext,
      HttpServletRequest servletRequest,
      HttpServletResponse servletResponse) {
        this.context = servletContext;
        this.request = servletRequest;
        this.response = servletResponse;
    }

    public abstract void process() throws ServletException, IOException;

    protected void forward(String target) throws ServletException, IOException {
        target = String.format("/WEB-INF/jsp/%s.jsp", target);
        RequestDispatcher dispatcher = context.getRequestDispatcher(target);
        dispatcher.forward(request, response);
    }
}

FrontCommand的具象实现,例如SearchCommand,将包含在找到书籍或找不到书籍时的条件逻辑:

public class SearchCommand extends FrontCommand {
    @Override
    public void process() throws ServletException, IOException {
        Book book = new BookshelfImpl().getInstance()
          .findByTitle(request.getParameter("title"));
        if (book != null) {
            request.setAttribute("book", book);
            forward("book-found");
        } else {
            forward("book-notfound");
        }
    }
}

如果应用程序正在运行,我们可以通过指向浏览器到http://localhost:8080/front-controller/?command=Search&title=patterns来达到这个命令。

SearchCommand将解析为两个视图,第二个视图可以通过以下请求进行测试:http://localhost:8080/front-controller/?command=Search&title=any-title

为了完成我们的场景,我们将实现另一个命令,当Servlet未知的命令请求时,它将作为默认处理。这个视图可以通过http://localhost:8080/front-controller/?command=Order&title=any-title访问,或者完全省略URL参数。

4. 部署

由于我们决定创建一个WAR文件项目,我们需要一个web部署描述符。通过这个web.xml,我们可以在任何Servlet容器中运行我们的Web应用程序:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
  version="3.1">
    <servlet>
        <servlet-name>front-controller</servlet-name>
        <servlet-class>
            com.baeldung.enterprise.patterns.front.controller.FrontControllerServlet
        </servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>front-controller</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

最后一步是运行'mvn install jetty:run'并在浏览器中检查我们的视图。

5. 总结

至此,我们已经熟悉了前端控制器模式及其作为Servlet和命令层次结构实现的基本概念。

如往常一样,你可以在GitHub上找到完整的代码。