Spring–添加SpringMVC –第1部分

欢迎来到本教程的第四部分。 在这一部分中,我们将使用Spring MVC编写控制器和视图,并考虑我们的REST模型。

我们必须做的第一件事,就是根据目前的情况制作一个Web应用程序。 我们将web / WEB-INF文件夹添加到我们的项目根目录。 在WEB-INF内创建jsp文件夹。 我们将把JSP放在那个地方。 在该文件夹内,我们将放置具有以下内容的部署描述符web.xml文件:

<?xml version='1.0' encoding='UTF-8'?>
<web-app xmlns='http://java.sun.com/xml/ns/javaee'
         xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
         xsi:schemaLocation='http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd'
         version='3.0'>

    <display-name>timesheet-app</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:persistence-beans.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>timesheet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>timesheet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

注意,我们正在使用称为时间表的servlet。 这是一个调度程序servlet。 下图说明了Spring的调度程序servlet的工作方式(在下面的图片中称为Front控制器):

  1. 请求由调度程序Servlet处理
  2. 分派器servlet决定将请求传递到哪个控制器(通过请求映射,我们将在后面看到),然后将请求委托
  3. 控制器创建模型并将其传递回调度程序Servlet
  4. 分派器Servlet解析视图的逻辑名称,在此绑定模型并呈现视图

最后一步很神秘。 调度程序Servlet如何解析视图的逻辑名称? 它使用称为ViewResolver的东西。 但是我们不会手工创建自己的,而是只创建另一个配置文件,使用ViewResolver定义一个bean,并由Spring注入它。 在WEB-INF中创建另一个Spring配置文件。 按照约定,它必须命名为timesheet-servlet.xml ,因为我们将DispatcherServlet命名为“ timesheet”,并且这是文件名,Spring将在其中默认情况下查找config。 还创建包org.timesheet.web 。 这是我们将放置控制器的地方(它们也只是带注释的POJO)。

这是时间表-servlet.xml

<?xml version='1.0' encoding='UTF-8'?>
<beans xmlns='http://www.springframework.org/schema/beans'
       xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
       xmlns:context='http://www.springframework.org/schema/context'
       xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd'>

    <context:component-scan base-package='org.timesheet.web' />

    <bean
        class='org.springframework.web.servlet.view.InternalResourceViewResolver'>
        <property name='prefix' value='/WEB-INF/jsp/' />
        <property name='suffix' value='.jsp' />
    </bean>
</beans>

我们定义了前缀后缀来解析逻辑名。 真的很简单。 我们这样计算名称:
全名=前缀+逻辑名+后缀
因此,使用我们的InternalResourceViewResolver,逻辑名称“ index”将解析为“ /WEB-INF/jsp/index.jsp”。

对于视图,我们将结合使用JSP技术和JSTL(标记库),因此我们需要向pom.xml文件中添加另一个依赖项:

<dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
        </dependency>

现在,我们想在/ timesheet-app / welcome上处理GET。 因此,我们需要编写视图和控制器(对于Model,我们将使用Spring工具中的一个)。 让我们从控制器开始:

package org.timesheet.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.Date;

@Controller
@RequestMapping('/welcome')
public class WelcomeController {

    @RequestMapping(method = RequestMethod.GET)
    public String showMenu(Model model) {
        model.addAttribute('today', new Date());
        return 'index';
    }

}

因此,当有人访问url欢迎(在我们的示例中为http:// localhost:8080 / timesheet-app / welcome )时,此控制器将处理请求。 我们还使用Model并在那里绑定名为“ today”的值。 这就是我们如何获得查看页面的价值。

请注意,我的应用程序的根目录是/ timesheet-app。 这称为应用程序上下文 。 您当然可以更改它,但是假设您是应用程序上下文,则所有其余代码都按这样设置。 如果要部署WAR,它将基于WAR的名称。

从showMenu方法中,我们返回“索引”,它将被解析为WEB-INF / jsp / index.jsp,因此让我们创建一个这样的页面并放置一些基本内容:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %>
<%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %>

<html>
<head>
    <title>Welcome to Timesheet app!</title>
</head>
<body>
    <h1>Welcome to the Timesheet App!</h1>

    <ul>
        <li><a href='managers'>List managers</a></li>
        <li><a href='employees'>List employees</a></li>
        <li><a href='tasks'>List tasks</a></li>
        <li><a href='timesheets'>List timesheets</a></li>
    </ul>

    <h2>Also check out <a href='timesheet-service'>extra services!</a></h2>
    Today is: <fmt:formatDate value='${today}' pattern='dd-MM-yyyy' />
</body>
</html>

回顾一下,我们添加了web.xml文件,timesheet-servlet.xml Spring配置文件,控制器类和jsp页面。 让我们尝试在某些Web容器上运行它。 我将使用Tomcat7,但是如果您对其他Web容器甚至应用程序服务器更满意,请随时进行切换。 现在,有很多方法可以运行Tomcat以及部署应用程序。 您可以:

无论选择哪种方式,请确保您可以访问上述URL,然后再继续。 坦白说,用Java进行部署对我来说并不是一件很有趣的事情,因此,如果您感到沮丧,请不要放弃,它可能不会第一次起作用。 但是使用上面的教程,您可能不会有任何问题。 另外,不要忘记设置正确的应用程序上下文。

在编写更多控制器之前,让我们准备一些数据。 当Spring创建welcomeController bean时,我们想要一些数据。 因此,现在,让我们编写虚拟生成器,它会创建一些实体。 在本教程的后面,我们将看到一些更实际的解决方案。

辅助程序包放在Web程序包下,然后将控制器放置在EntityGenerator类中:

package org.timesheet.web.helpers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.domain.Timesheet;
import org.timesheet.service.GenericDao;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.service.dao.ManagerDao;
import org.timesheet.service.dao.TaskDao;
import org.timesheet.service.dao.TimesheetDao;

import java.util.List;

/**
 * Small util helper for generating entities to simulate real system.
 */
@Service
public final class EntityGenerator {

    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private ManagerDao managerDao;

    @Autowired
    private TaskDao taskDao;

    @Autowired
    private TimesheetDao timesheetDao;

    public void generateDomain() {
        Employee steve = new Employee('Steve', 'Design');
        Employee bill = new Employee('Bill', 'Marketing');
        Employee linus = new Employee('Linus', 'Programming');
        
        // free employees (no tasks/timesheets)
        Employee john = new Employee('John', 'Beatles');
        Employee george = new Employee('George', 'Beatles');
        Employee ringo = new Employee('Ringo', 'Beatles');
        Employee paul = new Employee('Paul', 'Beatles');

        Manager eric = new Manager('Eric');
        Manager larry = new Manager('Larry');
        
        // free managers
        Manager simon = new Manager('Simon');
        Manager garfunkel = new Manager('Garfunkel');

        addAll(employeeDao, steve, bill, linus, john, george, ringo, paul);
        addAll(managerDao, eric, larry, simon, garfunkel);

        Task springTask = new Task('Migration to Spring 3.1', eric, steve, linus);
        Task tomcatTask = new Task('Optimizing Tomcat', eric, bill);
        Task centosTask = new Task('Deploying to CentOS', larry, linus);

        addAll(taskDao, springTask, tomcatTask, centosTask);

        Timesheet linusOnSpring = new Timesheet(linus, springTask, 42);
        Timesheet billOnTomcat = new Timesheet(bill, tomcatTask, 30);

        addAll(timesheetDao, linusOnSpring, billOnTomcat);
    }
    
    public void deleteDomain() {
        List<Timesheet> timesheets = timesheetDao.list();
        for (Timesheet timesheet : timesheets) {
            timesheetDao.remove(timesheet);
        }

        List<Task> tasks = taskDao.list();
        for (Task task : tasks) {
            taskDao.remove(task);
        }

        List<Manager> managers = managerDao.list();
        for (Manager manager : managers) {
            managerDao.remove(manager);
        }

        List<Employee> employees = employeeDao.list();
        for (Employee employee : employees) {
            employeeDao.remove(employee);
        }
    }
    
    private <T> void addAll(GenericDao<T, Long> dao, T... entites) {
        for (T o : entites) {
            dao.add(o);
        }
    }
}

现在,让我们使用WelcomeController的代码。 我们将在此处注入生成器,并放置使用@PostConstruct注释进行注释的特殊方法。 这是用于bean生命周期的JSR-250注释,Spring对此进行了支持。 这意味着,在Spring IoC容器实例化welcomeController bean之后,将立即调用此方法。

package org.timesheet.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.timesheet.web.helpers.EntityGenerator;

import javax.annotation.PostConstruct;
import java.util.Date;

@Controller
@RequestMapping('/welcome')
public class WelcomeController {

    @Autowired
    private EntityGenerator entityGenerator;

    @RequestMapping(method = RequestMethod.GET)
    public String showMenu(Model model) {
        model.addAttribute('today', new Date());
        return 'index';
    }

    @PostConstruct
    public void prepareFakeDomain() {
        entityGenerator.deleteDomain();
        entityGenerator.generateDomain();
    }
    
}

好吧,现在就为域逻辑编写一些控制器!

我们将从编写Employee的控制器开始。 首先,在org.timesheet.web包下创建EmployeeController类。 将class标记为Web控制器并处理“ /员工”请求:

@Controller
@RequestMapping('/employees')
public class EmployeeController { ...

为了处理持久性数据(在这种情况下为Employees),我们需要DAO并将其通过Spring的IoC容器自动连接,所以我们就可以这样做:

private EmployeeDao employeeDao;

    @Autowired
    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }

现在我们要处理HTTP GET方法。 当用户使用Web浏览器访问http:// localhost:8080 / timesheet-app / employees时,控制器必须处理GET请求。 只是联系DAO并收集所有员工并将他们纳入模型。

@RequestMapping(method = RequestMethod.GET)
    public String showEmployees(Model model) {
        List<Employee> employees = employeeDao.list();
        model.addAttribute('employees', employees);

        return 'employees/list';
    }

在jsp文件夹下,创建employees文件夹,我们将在其中放置所有相应的雇员JSP。 可能您已经注意到,包含员工列表的页面将解析为/WEB-INF/jsp/employees/list.jsp。 因此,创建这样的页面。 稍后,我们将查看内容,如果您愿意,可以暂时在其中放置随机文本以查看其是否有效。

在JSP页面中,我们将在员工个人页面旁边显示一个链接,该链接看起来像http:// localhost:8080 / timesheet-app / employees / {id} ,其中ID是员工的ID。 这是RESTful URL,因为它是面向资源的,我们正在直接标识资源。 RESTless URL类似于http:// localhost:8080 / timesheet-app / employees.html?id = 123。 那是面向行动的,不能识别资源。

让我们向控制器添加另一个方法来处理此URL:

@RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getEmployee(@PathVariable('id') long id, Model model) {
        Employee employee = employeeDao.find(id);
        model.addAttribute('employee', employee);

        return 'employees/view';
    }

同样,在/ jsp / employees文件夹下创建view.jsp页面。 在此页面上,我们还想更改员工。 我们只是访问相同的URL,但使用不同的Web方法-POST。 这意味着,我们正在从有限模型中提供数据以进行更新。

此方法处理员工更新:

@RequestMapping(value = '/{id}', method = RequestMethod.POST)
    public String updateEmployee(@PathVariable('id') long id, Employee employee) {
        employee.setId(id);
        employeeDao.update(employee);

        return 'redirect:/employees';
    }

在这种情况下,我们使用GET或POST方法访问employee / {id}。 但是,如果我们要删除员工怎么办? 我们将访问相同的URL,但使用不同的方法-DELETE 。 我们将在EmployeeDao中使用其他业务逻辑。 如果出现任何问题,我们将引发包含无法删除的员工的异常。 因此,在这种情况下,请添加控制器方法:

/**
     * Deletes employee with specified ID
     * @param id Employee's ID
     * @return redirects to employees if everything was ok
     * @throws EmployeeDeleteException When employee cannot be deleted
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteEmployee(@PathVariable('id') long id)
            throws EmployeeDeleteException {

        Employee toDelete = employeeDao.find(id);
        boolean wasDeleted = employeeDao.removeEmployee(toDelete);

        if (!wasDeleted) {
            throw new EmployeeDeleteException(toDelete);
        }

        // everything OK, see remaining employees
        return 'redirect:/employees';
    }

注意,我们正在从该方法返回重定向 。 redirect:前缀表示应将请求重定向到它之前的路径。

创建包org.timesheet.web.exceptions并将EmployeeDeleteException放在下面:

package org.timesheet.web.exceptions;

import org.timesheet.domain.Employee;

/**
 * When employee cannot be deleted.
 */
public class EmployeeDeleteException extends Exception {

    private Employee employee;

    public EmployeeDeleteException(Employee employee) {
        this.employee = employee;
    }

    public Employee getEmployee() {
        return employee;
    }
}

可以说,可以直接从DAO抛出此异常。 现在我们该如何处理? Spring有一个特殊的注释,称为@ExceptionHandler 。 我们将其放置在控制器中,并在抛出指定异常时,使用ExceptionHandler注释的方法将对其进行处理并解析正确的视图:

/**
     * Handles EmployeeDeleteException
     * @param e Thrown exception with employee that couldn't be deleted
     * @return binds employee to model and returns employees/delete-error
     */
    @ExceptionHandler(EmployeeDeleteException.class)
    public ModelAndView handleDeleteException(EmployeeDeleteException e) {
        ModelMap model = new ModelMap();
        model.put('employee', e.getEmployee());
        return new ModelAndView('employees/delete-error', model);
    }

好的,时间到了JSP。 我们将使用一些资源(例如* .css或* .js),以便在您的Web应用程序根目录中创建resources文件夹。 WEB-INF不是root,它是上面的文件夹。 因此,资源和WEB-INF现在应该在目录树中处于同一级别。 我们已经将调度程序servlet配置为处理每个请求(使用/ url模式),但是我们不想让它的atm处理静态资源。 我们将通过简单地将默认servlet的映射放入我们的web.xml文件中来解决该问题:

<servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/resources/*</url-pattern>
    </servlet-mapping>

在那些资源下创建styles.css文件。 即使我们稍后将使用lota之类的东西,我们现在也将CSS放在整个应用程序中。

table, th {
    margin: 10px;
    padding: 5px;
    width: 300px;
}

.main-table {
    border: 2px solid green;
    border-collapse: collapse;
}

.wide {
    width: 600px;
}

.main-table th {
    background-color: green;
    color: white;
}

.main-table td {
    border: 1px solid green;
}

th {
    text-align: left;
}

h1 {
    margin: 10px;
}

a {
    margin: 10px;
}

label {
    display: block;
    text-align: left;
}

#list {
    padding-left: 10px;
    position: relative;
}

#list ul {
    padding: 0;
}

#list li {
    list-style: none;
    margin-bottom: 1em;
}

.hidden {
    display: none;
}

.delete {
    margin: 0;
    text-align: center;
}

.delete-button {
    border: none;
    background: url('/timesheet-app/resources/delete.png') no-repeat top left;
    color: transparent;
    cursor: pointer;
    padding: 2px 8px;
}

.task-table {
    width: 150px;
    border: 1px solid #dcdcdc;
}

.errors {
    color: #000;
    background-color: #ffEEEE;
    border: 3px solid #ff0000;
    padding: 8px;
    margin: 16px;
}

现在,让我们创建employeeess / list.jsp页面:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %>
<%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Employees</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h1>List of employees</h1>
    <a href='employees?new'>Add new employee</a>
    <table cellspacing='5' class='main-table'>
        <tr>
            <th>Name</th>
            <th>Department</th>
            <th>Details</th>
            <th>Delete</th>
        </tr>
        <c:forEach items='#{employees}' var='emp'>
            <tr>
                <td>${emp.name}</td>
                <td>${emp.department}</td>
                <td>
                    <a href='employees/${emp.id}'>Go to page</a>
                </td>
                <td>
                    <sf:form action='employees/${emp.id}' method='delete' cssClass='delete'>
                        <input type='submit' class='delete-button' value='' />
                    </sf:form>
                </td>
            </tr>
        </c:forEach>
    </table>

    <br />
    <a href='welcome'>Go back</a>
</body>
</html>

在该页面上,我们正在资源下链接css(具有包括应用程序上下文的全名)。 还有链接到员工详细信息页面(view.jsp)的链接,该页面由员工的ID解析。
最有趣的部分是SF taglib的用法。 为了保持对Web 1.0的友好,我们很遗憾不能直接使用DELETE。 直到HTML4和XHTML1,HTML表单只能使用GET和POST。 解决方法是,如果实际上应将POST用作DELETE,则使用标记的隐藏字段。 这正是Spring免费为我们服务的-仅使用sf:form前缀。 因此,我们正在通过HTTP POST隧道传送DELETE,但它将被正确调度。 为此,我们必须为此在web.xml中添加特殊的Spring过滤器:

<filter>
        <filter-name>httpMethodFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.HiddenHttpMethodFilter
        </filter-class>
    </filter>
    
    <filter-mapping>
        <filter-name>httpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

即使JSP是实际上已编译为servlet的Java特定技术,我们也可以像使用任何HTML页面一样使用它。 我们添加了一些CSS,现在我们添加了最受欢迎的javascript库– jQuery。 转到https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js并下载jquery.js文件,并将其拖放到资源文件夹中。 我们将允许用户使用POST更新资源,因此我们将使用jQuery进行某些DOM操作-只是出于幻想。 您可以在普通HTML页面中使用几乎所有内容。

现在让我们创建/employees/view.jsp -这是员工的详细页面。

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Employee page</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Employee info</h2>
    <div id='list'>
        <sf:form method='post'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${employee.name}' disabled='true'/>
                </li>
                <li>
                    <label for='department'>Department:</label>
                    <input name='department' id='department' value='${employee.department}' disabled='true' />
                </li>
                <li>
                    <input type='button' value='Unlock' id='unlock' />
                    <input type='submit' value='Save' id='save' class='hidden' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='../employees'>Go Back</a>

    <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
    <script>
        (function() {
            $('#unlock').on('click', function() {
                $('#unlock').addClass('hidden');

                // enable stuff
                $('#name').removeAttr('disabled');
                $('#department').removeAttr('disabled');
                $('#save').removeClass('hidden');
            });
        })();
    </script>
</body>
</html>

在页面内部,我们引用jQuery文件,并具有自动调用的匿名功能-单击具有ID“解锁”的按钮后,我们将其隐藏,显示提交按钮并解锁字段,以便可以更新员工。 按下“保存”按钮后,我们将被重定向回员工列表,并且此列表已更新。

我们将在Employee上完成CRUD的最后一项功能是添加。 我们将通过使用GET和我们称之为new的额外参数来访问员工来解决这一问题。 因此,用于添加员工的URL将是: http:// localhost:8080 / timesheet-app / employees?new
让我们为此修改控制器:

@RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createEmployeeForm(Model model) {
        model.addAttribute('employee', new Employee());
        return 'employees/new';
    }

这将为新的JSP页面提供服务-/ WEB-INF / jsp / employees / new.jsp

<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<html>
<head>
    <title>Add new employee</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Add new Employee</h2>
    <div id='list'>
        <sf:form method='post' action='employees'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${employee.name}'/>
                </li>
                <li>
                    <label for='department'>Department:</label>
                    <input name='department' id='department'
                           value='${employee.department}' />
                </li>
                <li>
                    <input type='submit' value='Save' id='save' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='employees'>Go Back</a>
</body>
</html>

该页面与view.jsp非常相似。 在现实世界的应用程序中,我们将使用Apache Tiles之类的方法来减少冗余代码,但是现在让我们不必担心。

请注意,我们提交的表格带有“员工”操作。 回到我们的控制器,让我们使用POST http方法处理员工:

@RequestMapping(method = RequestMethod.POST)
    public String addEmployee(Employee employee) {
        employeeDao.add(employee);

        return 'redirect:/employees';
    }

而且,当我们无法删除雇员jsp / employees / delete-error.jsp时,请不要忘记错误的JSP页面:

<html>
<head>
    <title>Cannot delete employee</title>
</head>
<body>
    Oops! Resource <a href='${employee.id}'>${employee.name}</a> can not be deleted.

    <p>
        Make sure employee doesn't have assigned any task or active timesheet.
    </p>

    <br /><br /><br />
    <a href='../welcome'>Back to main page.</a>
</body>
</html>

就是这样,我们为员工提供了完整的CRUD功能。 让我们回顾一下我们刚刚做的基本步骤:

  • 添加了EmployeeController类
  • 在Web根目录中为静态内容创建资源文件夹
  • 在web.xml中为默认servlet添加了映射
  • 在资源文件夹中添加了styles.css
  • 在web.xml中使用过滤器配置了POST-DELETE隧道
  • 下载jQuery.js并添加到我们的资源文件夹中
  • 添加了employeeess / list.jsp页面
  • 添加了employeeess / view.jsp页面
  • 添加了employeeess / new.jsp页面
  • 添加了employees / delete-error.jsp页面

现在,这是控制器的完整代码:

package org.timesheet.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.timesheet.domain.Employee;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.web.exceptions.EmployeeDeleteException;

import java.util.List;

/**
 * Controller for handling Employees.
 */
@Controller
@RequestMapping('/employees')
public class EmployeeController {

    private EmployeeDao employeeDao;

    @Autowired
    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }

    public EmployeeDao getEmployeeDao() {
        return employeeDao;
    }

    /**
     * Retrieves employees, puts them in the model and returns corresponding view
     * @param model Model to put employees to
     * @return employees/list
     */
    @RequestMapping(method = RequestMethod.GET)
    public String showEmployees(Model model) {
        List<Employee> employees = employeeDao.list();
        model.addAttribute('employees', employees);

        return 'employees/list';
    }

    /**
     * Deletes employee with specified ID
     * @param id Employee's ID
     * @return redirects to employees if everything was ok
     * @throws EmployeeDeleteException When employee cannot be deleted
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteEmployee(@PathVariable('id') long id)
            throws EmployeeDeleteException {

        Employee toDelete = employeeDao.find(id);
        boolean wasDeleted = employeeDao.removeEmployee(toDelete);

        if (!wasDeleted) {
            throw new EmployeeDeleteException(toDelete);
        }

        // everything OK, see remaining employees
        return 'redirect:/employees';
    }

    /**
     * Handles EmployeeDeleteException
     * @param e Thrown exception with employee that couldn't be deleted
     * @return binds employee to model and returns employees/delete-error
     */
    @ExceptionHandler(EmployeeDeleteException.class)
    public ModelAndView handleDeleteException(EmployeeDeleteException e) {
        ModelMap model = new ModelMap();
        model.put('employee', e.getEmployee());
        return new ModelAndView('employees/delete-error', model);
    }

    /**
     * Returns employee with specified ID
     * @param id Employee's ID
     * @param model Model to put employee to
     * @return employees/view
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getEmployee(@PathVariable('id') long id, Model model) {
        Employee employee = employeeDao.find(id);
        model.addAttribute('employee', employee);

        return 'employees/view';
    }

    /**
     * Updates employee with specified ID
     * @param id Employee's ID
     * @param employee Employee to update (bounded from HTML form)
     * @return redirects to employees
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.POST)
    public String updateEmployee(@PathVariable('id') long id, Employee employee) {
        employee.setId(id);
        employeeDao.update(employee);

        return 'redirect:/employees';
    }

    /**
     * Creates form for new employee
     * @param model Model to bind to HTML form
     * @return employees/new
     */
    @RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createEmployeeForm(Model model) {
        model.addAttribute('employee', new Employee());
        return 'employees/new';
    }

    /**
     * Saves new employee to the database
     * @param employee Employee to save
     * @return redirects to employees
     */
    @RequestMapping(method = RequestMethod.POST)
    public String addEmployee(Employee employee) {
        employeeDao.add(employee);

        return 'redirect:/employees';
    }
    
}

如果您使用的是SpringSource Tool Suite,则可以直接在IDE中检查映射。 将“ Spring项目性质”添加到您的项目中,在Properties-> Spring-> Bean Support中配置Spring的配置文件。 然后右键单击项目,然后按Spring Tools-> Show Request Mappings,您应该看到类似以下内容:

关于员工的最后一件事是编写JUnit测试。 由于我们的WEB-INF中有timesheet-servlet.xml,因此无法在JUnit测试中访问其bean。 我们要做的是从timesheet-servlet.xml中 删除以下行:

<context:component-scan base-package='org.timesheet.web' />

现在,我们在src / main / resources中创建新的Spring Bean配置,并将其称为controllers.xml 。 我们唯一关心的是将自动扫描控制器放在此处,因此内容非常简单:

<?xml version='1.0' encoding='UTF-8'?>
<beans xmlns='http://www.springframework.org/schema/beans'
       xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
       xmlns:context='http://www.springframework.org/schema/context'
       xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd'>

    <context:component-scan base-package='org.timesheet.web' />

</beans>

为了使上下文知道那些spring bean,请像下面这样更改web.xml中的context-param:

<context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:persistence-beans.xml
            classpath:controllers.xml
        </param-value>
    </context-param>

另外,我们现在必须将bean从controllers.xml导入到timesheet-servlet.xml中,因此,我们添加了以下内容,而不是从timesheet-servlet.xml中删除<context:component-scan…行:

<import resource='classpath:controllers.xml' />

这将使我们能够将控制器自动连接到测试。 好的,因此在测试源文件夹中,创建包org.timesheet.web,然后将EmployeeControllerTest放在那里。 这非常简单,我们仅将控制器测试为POJO,以及它如何影响持久层(通过DAO验证)。 但是,我们做了一个例外。 在方法testDeleteEmployeeThrowsException中 ,我们将明确告诉DAO在尝试删除雇员时返回false。 这将为我们节省复杂的对象创建和附加DAO的注入。 我们将为此使用流行的模拟框架Mockito

向您的pom.xml添加依赖项:

<dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>1.9.0</version>
        </dependency>

测试EmployeeController:

package org.timesheet.web;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.web.exceptions.EmployeeDeleteException;

import java.util.Collection;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ContextConfiguration(locations = {'/persistence-beans.xml', '/controllers.xml'})
public class EmployeeControllerTest extends DomainAwareBase {
    
    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private EmployeeController controller;
    
    private Model model; // used for controller
    
    @Before
    public void setUp() {
        model = new ExtendedModelMap();
    }

    @After
    public void cleanUp() {
        List<Employee> employees = employeeDao.list();
        for (Employee employee : employees) {
            employeeDao.remove(employee);
        }
    }

    @Test
    public void testShowEmployees() {
        // prepare some data
        Employee employee = new Employee('Lucky', 'Strike');
        employeeDao.add(employee);

        // use controller
        String view = controller.showEmployees(model);
        assertEquals('employees/list', view);

        List<Employee> listFromDao = employeeDao.list();
        Collection<?> listFromModel = (Collection<?>) model.asMap().get('employees');

        assertTrue(listFromModel.contains(employee));
        assertTrue(listFromDao.containsAll(listFromModel));
    }
    
    @Test
    public void testDeleteEmployeeOk() throws EmployeeDeleteException {
        // prepare ID to delete
        Employee john = new Employee('John Lennon', 'Singing');
        employeeDao.add(john);
        long id = john.getId();

        // delete & assert
        String view = controller.deleteEmployee(id);
        assertEquals('redirect:/employees', view);
        assertNull(employeeDao.find(id));
    }

    @Test(expected = EmployeeDeleteException.class)
    public void testDeleteEmployeeThrowsException() throws EmployeeDeleteException {
        // prepare ID to delete
        Employee john = new Employee('John Lennon', 'Singing');
        employeeDao.add(john);
        long id = john.getId();

        // mock DAO for this call
        EmployeeDao mockedDao = mock(EmployeeDao.class);
        when(mockedDao.removeEmployee(john)).thenReturn(false);

        EmployeeDao originalDao = controller.getEmployeeDao();
        try {
            // delete & expect exception
            controller.setEmployeeDao(mockedDao);
            controller.deleteEmployee(id);
        } finally {
            controller.setEmployeeDao(originalDao);
        }
    }

    @Test
    public void testHandleDeleteException() {
        Employee john = new Employee('John Lennon', 'Singing');
        EmployeeDeleteException e = new EmployeeDeleteException(john);
        ModelAndView modelAndView = controller.handleDeleteException(e);

        assertEquals('employees/delete-error', modelAndView.getViewName());
        assertTrue(modelAndView.getModelMap().containsValue(john));
    }
    
    @Test
    public void testGetEmployee() {
        // prepare employee
        Employee george = new Employee('George Harrison', 'Singing');
        employeeDao.add(george);
        long id = george.getId();
        
        // get & assert
        String view = controller.getEmployee(id, model);
        assertEquals('employees/view', view);
        assertEquals(george, model.asMap().get('employee'));
    }

    @Test
    public void testUpdateEmployee() {
        // prepare employee
        Employee ringo = new Employee('Ringo Starr', 'Singing');
        employeeDao.add(ringo);
        long id = ringo.getId();

        // user alters Employee in HTML form
        ringo.setDepartment('Drums');

        // update & assert
        String view = controller.updateEmployee(id, ringo);
        assertEquals('redirect:/employees', view);
        assertEquals('Drums', employeeDao.find(id).getDepartment());
    }

    @Test
    public void testAddEmployee() {
        // prepare employee
        Employee paul = new Employee('Paul McCartney', 'Singing');
        
        // save but via controller
        String view = controller.addEmployee(paul);
        assertEquals('redirect:/employees', view);

        // employee is stored in DB
        assertEquals(paul, employeeDao.find(paul.getId()));
    }
}

注意,我们如何在try / finally块中使用模拟的dao进行设置。 仅用于那一次调用以确保引发正确的异常。 如果您从未见过嘲笑,我绝对建议您了解有关此技术的更多信息。 有很多模拟框架。 我们选择的一种-Mockito-带有非常简洁的语法,该语法大量使用Java静态导入。

现在,经理与员工非常相似,因此没有任何大问题,让我们为经理添加非常相似的内容:

首先,在WEB-INF / jsp中创建管理器文件夹。

现在让我们编写控制器并注入相应的DAO:

@Controller
@RequestMapping('/managers')
public class ManagerController {

    private ManagerDao managerDao;

    @Autowired
    public void setManagerDao(ManagerDao managerDao) {
        this.managerDao = managerDao;
    }

    public ManagerDao getManagerDao() {
        return managerDao;
    }
}

列表管理员的添加方法:

/**
     * Retrieves managers, puts them in the model and returns corresponding view
     * @param model Model to put employees to
     * @return managers/list
     */
    @RequestMapping(method = RequestMethod.GET)
    public String showManagers(Model model) {
        List<Manager> employees = managerDao.list();
        model.addAttribute('managers', employees);

        return 'managers/list';
    }

list.jsp添加到jsp / managers:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %>
<%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Managers</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h1>List of managers</h1>
    <a href='managers?new'>Add new manager</a>
    <table cellspacing='5' class='main-table'>
        <tr>
            <th>Name</th>
            <th>Details</th>
            <th>Delete</th>
        </tr>
        <c:forEach items='#{managers}' var='man'>
            <tr>
                <td>${man.name}</td>
                <td>
                    <a href='managers/${man.id}'>Go to page</a>
                </td>
                <td>
                    <sf:form action='managers/${man.id}' method='delete' cssClass='delete'>
                        <input type='submit' value='' class='delete-button' />
                    </sf:form>
                </td>
            </tr>
        </c:forEach>
    </table>

    <br />
    <a href='welcome'>Go back</a>
</body>
</html>

添加删除管理员的方法:

/**
     * Deletes manager with specified ID
     * @param id Manager's ID
     * @return redirects to managers if everything was OK
     * @throws ManagerDeleteException When manager cannot be deleted
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteManager(@PathVariable('id') long id)
            throws ManagerDeleteException {

        Manager toDelete = managerDao.find(id);
        boolean wasDeleted = managerDao.removeManager(toDelete);

        if (!wasDeleted) {
            throw new ManagerDeleteException(toDelete);
        }

        // everything OK, see remaining managers
        return 'redirect:/managers';
    }

删除失败时的异常:

package org.timesheet.web.exceptions;

import org.timesheet.domain.Manager;

/**
 * When manager cannot be deleted
 */
public class ManagerDeleteException extends Exception {

    private Manager manager;

    public ManagerDeleteException(Manager manager) {
        this.manager = manager;
    }

    public Manager getManager() {
        return manager;
    }
}

处理此异常的方法:

/**
     * Handles ManagerDeleteException
     * @param e Thrown exception with manager that couldn't be deleted
     * @return binds manager to model and returns managers/delete-error
     */
    @ExceptionHandler(ManagerDeleteException.class)
    public ModelAndView handleDeleteException(ManagerDeleteException e) {
        ModelMap model = new ModelMap();
        model.put('manager', e.getManager());
        return new ModelAndView('managers/delete-error', model);
    }

添加获取经理页面的方法:

/**
     * Returns manager with specified ID
     * @param id Managers's ID
     * @param model Model to put manager to
     * @return managers/view
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getManager(@PathVariable('id') long id, Model model) {
        Manager manager = managerDao.find(id);
        model.addAttribute('manager', manager);

        return 'managers/view';
    }

在jsp / managers下添加经理页面view.jsp

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Manager page</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Manager info</h2>
    <div id='list'>
        <sf:form method='post'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${manager.name}' disabled='true'/>
                </li>
                <li>
                    <input type='button' value='Unlock' id='unlock' />
                    <input type='submit' value='Save' id='save' class='hidden' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='../managers'>Go Back</a>

    <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
    <script>
        (function() {
            $('#unlock').on('click', function() {
                $('#unlock').addClass('hidden');

                // enable stuff
                $('#name').removeAttr('disabled');
                $('#save').removeClass('hidden');
            });
        })();
    </script>
</body>
</html>

JSP页面,用于处理删除时的错误:

<html>
<head>
    <title>Cannot delete manager</title>
</head>
<body>
    Oops! Resource <a href='${manager.id}'>${manager.name}</a> can not be deleted.

    <p>
        Make sure manager doesn't have assigned any task or active timesheet.
    </p>

    <br /><br /><br />
    <a href='../welcome'>Back to main page.</a>
</body>
</html>

添加更新管理器的方法:

/**
     * Updates manager with specified ID
     * @param id Manager's ID
     * @param manager Manager to update (bounded from HTML form)
     * @return redirects to managers
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.POST)
    public String updateManager(@PathVariable('id') long id, Manager manager) {
        manager.setId(id);
        managerDao.update(manager);

        return 'redirect:/managers';
    }

添加用于返回新经理表格的方法:

/**
     * Creates form for new manager
     * @param model Model to bind to HTML form
     * @return manager/new
     */
    @RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createManagerForm(Model model) {
        model.addAttribute('manager', new Manager());
        return 'managers/new';
    }

在jsp / managers下为新经理new.jsp添加页面:

<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<html>
<head>
    <title>Add new manager</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Add new Manager</h2>
    <div id='list'>
        <sf:form method='post' action='managers'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${manager.name}'/>
                </li>
                <li>
                    <input type='submit' value='Save' id='save' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='managers'>Go Back</a>
</body>
</html>

最后,添加用于添加管理器的方法:

/**
     * Saves new manager to the database
     * @param manager Manager to save
     * @return redirects to managers
     */
    @RequestMapping(method = RequestMethod.POST)
    public String addManager(Manager manager) {
        managerDao.add(manager);

        return 'redirect:/managers';
    }

好了,这部分的最后一段代码是ManagerController的测试用例:

package org.timesheet.web;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Manager;
import org.timesheet.service.dao.ManagerDao;
import org.timesheet.web.exceptions.ManagerDeleteException;

import java.util.Collection;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ContextConfiguration(locations = {'/persistence-beans.xml', '/controllers.xml'})
public class ManagerControllerTest extends DomainAwareBase {
    
    @Autowired
    private ManagerDao managerDao;

    @Autowired
    private ManagerController controller;
    
    private Model model; // used for controller
    
    @Before
    public void setUp() {
        model = new ExtendedModelMap();
    }

    @After
    public void cleanUp() {
        List<Manager> managers = managerDao.list();
        for (Manager manager : managers) {
            managerDao.remove(manager);
        }
    }

    @Test
    public void testShowManagers() {
        // prepare some data
        Manager manager = new Manager('Bob Dylan');
        managerDao.add(manager);

        // use controller
        String view = controller.showManagers(model);
        assertEquals('managers/list', view);

        List<Manager> listFromDao = managerDao.list();
        Collection<?> listFromModel = (Collection<?>) model.asMap().get('managers');

        assertTrue(listFromModel.contains(manager));
        assertTrue(listFromDao.containsAll(listFromModel));
    }
    
    @Test
    public void testDeleteManagerOk() throws ManagerDeleteException {
        // prepare ID to delete
        Manager john = new Manager('John Lennon');
        managerDao.add(john);
        long id = john.getId();

        // delete & assert
        String view = controller.deleteManager(id);
        assertEquals('redirect:/managers', view);
        assertNull(managerDao.find(id));
    }

    @Test(expected = ManagerDeleteException.class)
    public void testDeleteManagerThrowsException() throws ManagerDeleteException {
        // prepare ID to delete
        Manager john = new Manager('John Lennon');
        managerDao.add(john);
        long id = john.getId();

        // mock DAO for this call
        ManagerDao mockedDao = mock(ManagerDao.class);
        when(mockedDao.removeManager(john)).thenReturn(false);

        ManagerDao originalDao = controller.getManagerDao();
        try {
            // delete & expect exception
            controller.setManagerDao(mockedDao);
            controller.deleteManager(id);
        } finally {
            controller.setManagerDao(originalDao);
        }
    }

    @Test
    public void testHandleDeleteException() {
        Manager john = new Manager('John Lennon');
        ManagerDeleteException e = new ManagerDeleteException(john);
        ModelAndView modelAndView = controller.handleDeleteException(e);

        assertEquals('managers/delete-error', modelAndView.getViewName());
        assertTrue(modelAndView.getModelMap().containsValue(john));
    }

    @Test
    public void testGetManager() {
        // prepare manager
        Manager george = new Manager('George Harrison');
        managerDao.add(george);
        long id = george.getId();
        
        // get & assert
        String view = controller.getManager(id, model);
        assertEquals('managers/view', view);
        assertEquals(george, model.asMap().get('manager'));
    }

    @Test
    public void testUpdateManager() {
        // prepare manager
        Manager ringo = new Manager('Ringo Starr');
        managerDao.add(ringo);
        long id = ringo.getId();

        // user alters manager in HTML form
        ringo.setName('Rango Starr');

        // update & assert
        String view = controller.updateManager(id, ringo);
        assertEquals('redirect:/managers', view);
        assertEquals('Rango Starr', managerDao.find(id).getName());
    }

    @Test
    public void testAddManager() {
        // prepare manager
        Manager paul = new Manager('Paul McCartney');
        
        // save but via controller
        String view = controller.addManager(paul);
        assertEquals('redirect:/managers', view);

        // manager is stored in DB
        assertEquals(paul, managerDao.find(paul.getId()));
    }
}

请求映射现在看起来像这样:

因此,在这一部分中,我们学习了什么是Spring MVC,如何将实体用作模型,如何以POJO风格编写控制器,RESTful设计的外观,如何使用JSP创建视图以及如何使用CSS和JavaScript设置应用程序。

我们为员工和经理编写了控制器。 在下一部分中,我们将继续为“任务和时间表”编写控制器。 在进行下一部分之前,请确保到目前为止一切正常。

这是src文件夹(仅扩展了新内容。不必担心.iml文件,它们用于IntelliJ):

这是网络文件夹:

参考: 第4部分–添加Spring MVC –第1部分来自vrtoonjava博客上的JCG合作伙伴 Michal Vrtiak。


翻译自: https://www.javacodegeeks.com/2012/09/spring-adding-spring-mvc-part-1.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值