我们必须做的第一件事,就是根据目前的情况制作一个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控制器):
- 请求由调度程序Servlet处理
- 分派器servlet决定将请求传递到哪个控制器(通过请求映射,我们将在后面看到),然后将请求委托
- 控制器创建模型并将其传递回调度程序Servlet
- 分派器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