在上一部分中,我们为经理和员工实现了控制器。 既然我们知道了自己的出路,我们将做很少(但仅做很少)更复杂的事情-任务和时间表的控制器。

因此,让我们从org.timesheet.web开始。 TaskController 。 首先创建一个类,这次我们将访问更丰富的域,因此我们需要为任务,员工和经理自动连接三个DAOS。

@Controller
@RequestMapping('/tasks')
public class TaskController {private TaskDao taskDao;private EmployeeDao employeeDao;private ManagerDao managerDao;@Autowiredpublic void setTaskDao(TaskDao taskDao) {this.taskDao = taskDao;}@Autowiredpublic void setEmployeeDao(EmployeeDao employeeDao) {this.employeeDao = employeeDao;}@Autowiredpublic void setManagerDao(ManagerDao managerDao) {this.managerDao = managerDao;}public EmployeeDao getEmployeeDao() {return employeeDao;}public TaskDao getTaskDao() {return taskDao;}public ManagerDao getManagerDao() {return managerDao;}
}

让我们处理/ tasks上的GET请求:

/*** Retrieves tasks, puts them in the model and returns corresponding view* @param model Model to put tasks to* @return tasks/list*/@RequestMapping(method = RequestMethod.GET)public String showTasks(Model model) {model.addAttribute('tasks', taskDao.list());return 'tasks/list';}

我们将把JSP放在任务子文件夹中。 首先是用于显示所有任务的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'%><!-- resolve variables -->
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task>'--%><html>
<head><title>Tasks</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h1>List of tasks</h1><a href='tasks?new'>Add new task</a><table cellspacing='5' class='main-table wide'><tr><th style='width: 35%;'>Description</th><th>Manager</th><th>Employees</th><th>Completed</th><th style='width: 20%;'>Details</th><th>Delete</th></tr><c:forEach items='${tasks}' var='task'><tr><td>${task.description}</td><td><a href='managers/${task.manager.id}'>${task.manager.name}</a></td><td><c:forEach items='${task.assignedEmployees}' var='emp'><a href='employees/${emp.id}'>${emp.name}</a></c:forEach></td><td><div class='delete'><c:choose><c:when test='${task.completed}'>Done</c:when><c:when test='${!task.completed}'>In progress</c:when></c:choose></div></td><td><a href='tasks/${task.id}'>Go to page</a></td><td><sf:form action='tasks/${task.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 task with specified ID* @param id Task's ID* @return redirects to tasks if everything was ok* @throws TaskDeleteException When task cannot be deleted*/@RequestMapping(value = '/{id}', method = RequestMethod.DELETE)public String deleteTask(@PathVariable('id') long id) throws TaskDeleteException {Task toDelete = taskDao.find(id);boolean wasDeleted = taskDao.removeTask(toDelete);if (!wasDeleted) {throw new TaskDeleteException(toDelete);}// everything OK, see remaining tasksreturn 'redirect:/tasks';}

TaskDeleteException:

package org.timesheet.web.exceptions;import org.timesheet.domain.Task;/*** When task cannot be deleted.*/
public class TaskDeleteException extends Exception {private Task task;public TaskDeleteException(Task task) {this.task = task;}public Task getTask() {return task;}
}

处理此异常的方法:

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

JSP页面jsp / tasks / delete-error.jsp用于显示删除错误:

<%--@elvariable id='task' type='org.timesheet.domain.Task'--%><html>
<head><title>Cannot delete task</title>
</head>
<body>Oops! Resource <a href='${task.id}'>${task.description}</a> can not be deleted.<p>Make sure there are no timesheets assigned on task.</p><br /><br /><br /><a href='../welcome'>Back to main page.</a>
</body>
</html>

显示任务的详细信息将通过URI / tasks / {id}访问。 我们将同时在模型中添加任务和可以添加到任务中的未分配员工。 它将像这样处理:

/*** Returns task with specified ID* @param id Tasks's ID* @param model Model to put task to* @return tasks/view*/@RequestMapping(value = '/{id}', method = RequestMethod.GET)public String getTask(@PathVariable('id') long id, Model model) {Task task = taskDao.find(id);model.addAttribute('task', task);// add all remaining employeesList<Employee> employees = employeeDao.list();Set<Employee> unassignedEmployees = new HashSet<Employee>();for (Employee employee : employees) {if (!task.getAssignedEmployees().contains(employee)) {unassignedEmployees.add(employee);}}model.addAttribute('unassigned', unassignedEmployees);return 'tasks/view';}

现在,事情有些复杂了。 我们想显示任务的用户详细信息页面。 在此任务上,我们要添加/删除分配给它的员工。
首先,让我们考虑一下URL。 任务已分配了员工,因此用于访问任务中员工的URL将如下所示:
/ tasks / {id} / employees / {employeeId} 要删除员工,我们只需使用DELETE方法访问此资源,因此让我们向控制器添加方法:

/*** Removes assigned employee from task* @param taskId Task's ID* @param employeeId Assigned employee's ID*/@RequestMapping(value = '/{id}/employees/{employeeId}', method = RequestMethod.DELETE)@ResponseStatus(HttpStatus.NO_CONTENT)public void removeEmployee(@PathVariable('id') long taskId,@PathVariable('employeeId') long employeeId) {Employee employee = employeeDao.find(employeeId);Task task = taskDao.find(taskId);task.removeEmployee(employee);taskDao.update(task);}

在视图页面上(我们稍后会看到),我们将简单地使用jQuery更改DOM模型,并从列表中删除分配的员工。
让我们假装什么都不会出错(我们有NO_CONTENT响应),因此员工将总是成功地从数据库中删除。 因此,我们可以简单地更改DOM模型。

对于添加员工,我们将有未分配员工的选择列表(或组合框)。 删除员工后,我们会将其添加到可用员工的选择中(他再次可用)。 添加员工后,我们将使用DAO更改Task并将其重定向回同一任务(所有内容都会更新)。 这是将员工分配给任务的代码:

/*** Assigns employee to tak* @param taskId Task's ID* @param employeeId Employee's ID (to assign)* @return redirects back to altered task: tasks/taskId*/@RequestMapping(value = '/{id}/employees/{employeeId}', method = RequestMethod.PUT)public String addEmployee(@PathVariable('id') long taskId,@PathVariable('employeeId') long employeeId) {Employee employee = employeeDao.find(employeeId);Task task = taskDao.find(taskId);task.addEmployee(employee);taskDao.update(task);return 'redirect:/tasks/' + taskId;}

最后,使用task / view.jsp了解Task的详细信息。 正如我提到的那样,有很多DOM更改,因此此代码似乎比平时更难。

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%><%--@elvariable id='task' type='org.timesheet.domain.Task'--%>
<%--@elvariable id='unassigned' type='java.util.List<org.timesheet.domain.Employee>'--%><html>
<head><title>Task page</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h2>Task info</h2><div id='list'><ul><li><label for='description'>Description:</label><input name='description' id='description' value='${task.description}'disabled='${task.completed ? 'disabled' : ''}' /></li><li><label for='manager'>Manager:</label><input name='manager' id='manager' value='${task.manager.name}'disabled='true' /></li><li><label for='employees'>Employees:</label><table id='employees' class='task-table'><c:forEach items='${task.assignedEmployees}' var='emp'><tr><sf:form action='${task.id}/employees/${emp.id}' method='delete'><td><a href='../employees/${emp.id}' id='href-${emp.id}'>${emp.name}</a></td><td><input type='submit' value='Remove' id='remove-${emp.id}' /><script src='/timesheet-app/resources/jquery-1.7.1.js'></script><script type='text/javascript'>$('#remove-${emp.id}').on('click', function() {$('#remove-${emp.id}').addClass('hidden');$('#href-${emp.id}').remove();// add to list of unassignedvar opt = document.createElement('option');opt.setAttribute('value', '${emp.id}');opt.textContent = '${emp.name}';$('#selected-emp').append(opt);});</script></td></sf:form></tr></c:forEach></table></li><li><label for='unassigned'>Unassgined:</label><table id='unassigned' class='task-table'><tr><sf:form method='put' id='add-form'><td><select id='selected-emp'><c:forEach items='${unassigned}' var='uemp'><option value='${uemp.id}'>${uemp.name}</option></c:forEach></select></td><td><input type='submit' value='Add' id='add-employee' /><script src='/timesheet-app/resources/jquery-1.7.1.js'></script><script type='text/javascript'>$('#add-employee').on('click', function() {$('#selected-emp').selected().remove();});</script></td></sf:form></tr></table></li></ul></div><br /><br /><a href='../tasks'>Go Back</a><script src='/timesheet-app/resources/jquery-1.7.1.js'></script><script type='text/javascript'>(function() {// prepare default form actionsetAddAction();// handler for changing action$('#selected-emp').on('change', function() {setAddAction();});function setAddAction() {var id = $('#selected-emp').val();$('#add-form').attr('action', '${task.id}/employees/' + id);}})();</script>
</body>
</html>

从代码中可以看到,我们再次仅使用HTML + JavaScript。 唯一特定于JSP的是将数据从模型带到页面。

OK,现在我们必须能够创建新的Task。 让我们准备用于提供表单的控制器,以添加将从/ tasks?new访问的任务:

/*** Creates form for new task.* @param model Model to bind to HTML form* @return tasks/new*/@RequestMapping(params = 'new', method = RequestMethod.GET)public String createTaskForm(Model model) {model.addAttribute('task', new Task());// list of managers to choose fromList<Manager> managers = managerDao.list();model.addAttribute('managers', managers);return 'tasks/new';}

任务包括名称,经理和分配的员工。 在本教程的范围内,我决定不实施最后一个。 我们只会产生一些员工。 如果您希望能够从某种选择列表中挑选员工并将他们分配给任务,那么请注意,这应该异步进行。 为此,您可以将特殊方法映射到控制器,并使用$ .post与jQuery进行AJAX发布。 我认为对于本教程来说,这太少了,但是如果您对如何在Spring中使用AJAX感兴趣,请查看有关Spring 3中的ajax简化的博客文章 。
在创建员工和经理时,我们仅将原始类型用于属性。 现在,我们想为任务分配实际的Manager实例。 因此,我们必须告诉Spring如何将选择列表(经理的ID)中的值转换为实际实例。 为此,我们将使用自定义的PropertyEditorSupport工具。 添加新的org.timesheet.web.editors包,创建新类ManagerEditor与下面的代码:

public class ManagerEditor extends PropertyEditorSupport {private ManagerDao managerDao;public ManagerEditor(ManagerDao managerDao) {this.managerDao = managerDao;}@Overridepublic void setAsText(String text) throws IllegalArgumentException {long id = Long.parseLong(text);Manager manager = managerDao.find(id);setValue(manager);}
}

ManagerEditor将在其构造函数中传递DAO。 它将通过ID查找实际的管理员,并调用父级的setValue。
Spring现在应该知道有这样一个编辑器,因此我们必须在控制器中注册它。 我们只需要将WebDataBinder作为参数的方法,并需要使用@InitBinder注释对其进行注释,如下所示:

@InitBinderprotected void initBinder(WebDataBinder binder) {binder.registerCustomEditor(Manager.class, new ManagerEditor(managerDao));}

就是这样,Spring现在知道如何直接从表单将经理分配给我们的任务。

最后保存任务的代码。 如前所述,在保存之前,我们将招募一些员工来执行任务:

/*** Saves new task to the database* @param task Task to save* @return redirects to tasks*/@RequestMapping(method = RequestMethod.POST)public String addTask(Task task) {// generate employeesList<Employee> employees = reduce(employeeDao.list());task.setAssignedEmployees(employees);taskDao.add(task);return 'redirect:/tasks';}

reduce方法,这是减少内存中员工的简单辅助方法。 这并不是十分有效,我们可以使用更复杂的查询来做到这一点,但现在就可以了。 如果需要,也可以随意滚动自己的归约逻辑:

/*** Reduces list of employees to some smaller amount.* Simulates user interaction.* @param employees Employees to reduced* @return New list of some employees from original employees list*/private List<Employee> reduce(List<Employee> employees) {List<Employee> reduced = new ArrayList<Employee>();Random random = new Random();int amount = random.nextInt(employees.size()) + 1;// max. five employeesamount = amount > 5 ? 5 : amount;for (int i = 0; i < amount; i++) {int randomIdx = random.nextInt(employees.size());Employee employee = employees.get(randomIdx);reduced.add(employee);employees.remove(employee);}return reduced;}

现在让我们看一下task / new.jsp页面:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%><%--@elvariable id='task' type='org.timesheet.domain.Task'--%>
<%--@elvariable id='managers' type='java.util.List<org.timesheet.domain.Manager'--%><html>
<head><title>Add new task</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h2>Add new Task</h2><div id='list'><sf:form method='post' action='tasks' commandName='task'><ul><li><label for='description'>Description:</label><input name='description' id='description' value='${task.description}' /></li><li><label for='manager-select'>Manager:</label><sf:select path='manager' id='manager-select'><sf:options items='${managers}' itemLabel='name' itemValue='id' /></sf:select></li><li>Employees will be generated ...</li><li><input type='submit' value='Save'></li></ul></sf:form></div><br /><br /><a href='tasks'>Go Back</a></body>
</html>

当然要测试控制器:

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.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.service.dao.ManagerDao;
import org.timesheet.service.dao.TaskDao;
import org.timesheet.web.exceptions.TaskDeleteException;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 TaskControllerTest extends DomainAwareBase {private Model model; // used for controller@Autowiredprivate TaskDao taskDao;@Autowiredprivate ManagerDao managerDao;@Autowiredprivate EmployeeDao employeeDao;@Autowiredprivate TaskController controller;@Beforepublic void setUp() {model = new ExtendedModelMap();    }@Afterpublic void cleanUp() {List<Task> tasks = taskDao.list();for (Task task : tasks) {taskDao.remove(task);}}@Testpublic void testShowTasks() {// prepare some dataTask task = sampleTask();// use controllerString view = controller.showTasks(model);assertEquals('tasks/list', view);List<Task> listFromDao = taskDao.list();Collection<?> listFromModel = (Collection<?>) model.asMap ().get('tasks');assertTrue(listFromModel.contains(task));assertTrue(listFromDao.containsAll(listFromModel));}@Testpublic void testDeleteTaskOk() throws TaskDeleteException {Task task = sampleTask();long id = task.getId();// delete & assertString view = controller.deleteTask(id);assertEquals('redirect:/tasks', view);assertNull(taskDao.find(id));}@Test(expected = TaskDeleteException.class)public void testDeleteTaskThrowsException() throws TaskDeleteException {Task task = sampleTask();long id = task.getId();// mock DAO for this callTaskDao mockedDao = mock(TaskDao.class);when(mockedDao.removeTask(task)).thenReturn(false);TaskDao originalDao = controller.getTaskDao();try {// delete & expect exceptioncontroller.setTaskDao(mockedDao);controller.deleteTask(id);} finally {controller.setTaskDao(originalDao);}}@Testpublic void testHandleDeleteException() {Task task = sampleTask();TaskDeleteException e = new TaskDeleteException(task);ModelAndView modelAndView = controller.handleDeleteException(e);assertEquals('tasks/delete-error', modelAndView.getViewName());assertTrue(modelAndView.getModelMap().containsValue(task));}@Testpublic void testGetTask() {Task task = sampleTask();long id = task.getId();// get & assertString view = controller.getTask(id, model);assertEquals('tasks/view', view);assertEquals(task, model.asMap().get('task'));}@Testpublic void testRemoveEmployee() {Task task = sampleTask();long id = task.getAssignedEmployees().get(0).getId();controller.removeEmployee(task.getId(), id);// task was updated inside controller in other transaction -> refreshtask = taskDao.find(task.getId());// get employee & assertEmployee employee = employeeDao.find(id);assertFalse(task.getAssignedEmployees().contains(employee));}@Testpublic void testAddEmployee() {Task task = sampleTask();Employee cassidy = new Employee('Butch Cassidy', 'Cowboys');employeeDao.add(cassidy);controller.addEmployee(task.getId(), cassidy.getId());// task was updated inside controller in other transaction -> refreshtask = taskDao.find(task.getId());// get employee & assertEmployee employee = employeeDao.find(cassidy.getId());assertTrue(task.getAssignedEmployees().contains(employee));}@Testpublic void testAddTask() {Task task = sampleTask();// save via controllerString view = controller.addTask(task);assertEquals('redirect:/tasks', view);// task is in DBassertEquals(task, taskDao.find(task.getId()));}private Task sampleTask() {Manager manager = new Manager('Jesse James');managerDao.add(manager);Employee terrence = new Employee('Terrence', 'Cowboys');Employee kid = new Employee('Sundance Kid', 'Cowboys');employeeDao.add(terrence);employeeDao.add(kid);Task task = new Task('Wild West', manager, terrence, kid);taskDao.add(task);return task;}
}

任务就是这样。 现在让我们为时间表创建控制器。 为我们需要的控制器和自动接线的DAO添加基本样板:

@Controller
@RequestMapping('/timesheets')
public class TimesheetController {private TimesheetDao timesheetDao;private TaskDao taskDao;private EmployeeDao employeeDao;@Autowiredpublic void setTimesheetDao(TimesheetDao timesheetDao) {this.timesheetDao = timesheetDao;}@Autowiredpublic void setTaskDao(TaskDao taskDao) {this.taskDao = taskDao;}@Autowiredpublic void setEmployeeDao(EmployeeDao employeeDao) {this.employeeDao = employeeDao;}public TimesheetDao getTimesheetDao() {return timesheetDao;}public TaskDao getTaskDao() {return taskDao;}public EmployeeDao getEmployeeDao() {return employeeDao;}
}

在时间表上处理GET请求的方法:

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

JSP将放置在时间表子文件夹中。 添加list.jsp页面,该页面基本上将遍历Timesheet的属性并滚动删除表单:

<%@ 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'%><!-- resolve variables -->
<%--@elvariable id='timesheets' type='java.util.List<org.timesheet.domain.Timesheet>'--%><html>
<head><title>Timesheets</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h1>List of timesheets</h1><a href='timesheets?new'>Add new timesheet</a><table cellspacing='5' class='main-table wide'><tr><th style='width: 30%'>Employee</th><th style='width: 50%'>Task</th><th>Hours</th><th>Details</th><th>Delete</th></tr><c:forEach items='${timesheets}' var='ts'><tr><td><a href='employees/${ts.who.id}'>${ts.who.name}</a></td><td><a href='tasks/${ts.task.id}'>${ts.task.description}</a></td><td>${ts.hours}</td><td><a href='timesheets/${ts.id}'>Go to page</a></td><td><sf:form action='timesheets/${ts.id}' method='delete' cssClass='delete'><input type='submit' class='delete-button'></sf:form></td></tr></c:forEach></table><br /><a href='welcome'>Go back</a>
</body>
</html>

删除时间表比删除任务更容易,因为我们不会破坏数据库中的任何约束,因此我们可以在DAO上使用默认的remove方法:

/*** Deletes timeshet with specified ID* @param id Timesheet's ID* @return redirects to timesheets*/@RequestMapping(value = '/{id}', method = RequestMethod.DELETE)public String deleteTimesheet(@PathVariable('id') long id) {Timesheet toDelete = timesheetDao.find(id);timesheetDao.remove(toDelete);return 'redirect:/timesheets';}

我们将照常通过将其ID添加到URI来访问单个时间表资源,因此我们将处理/ timesheets / {id}。 但是有分配给时间表的对象-任务实例和员工实例。 我们不希望表单将其无效。 因此,我们将为表单引入轻量级的命令支持对象。 我们将只更新小时,然后在实际的时间表实例中设置这些新小时:

/*** Returns timesheet with specified ID* @param id Timesheet's ID* @param model Model to put timesheet to* @return timesheets/view*/@RequestMapping(value = '/{id}', method = RequestMethod.GET)public String getTimesheet(@PathVariable('id') long id, Model model) {Timesheet timesheet = timesheetDao.find(id);TimesheetCommand tsCommand = new TimesheetCommand(timesheet);model.addAttribute('tsCommand', tsCommand);return 'timesheets/view';}

这是TimesheetCommand的代码,现在位于新包org.timesheet.web下。 命令

package org.timesheet.web.commands;import org.hibernate.validator.constraints.Range;
import org.timesheet.domain.Timesheet;import javax.validation.constraints.NotNull;public class TimesheetCommand {@NotNull@Range(min = 1, message = 'Hours must be 1 or greater')private Integer hours;private Timesheet timesheet;// default c-tor for bean instantiationpublic TimesheetCommand() {}public TimesheetCommand(Timesheet timesheet) {hours = timesheet.getHours();this.timesheet = timesheet;}public Integer getHours() {return hours;}public void setHours(Integer hours) {this.hours = hours;}public Timesheet getTimesheet() {return timesheet;}public void setTimesheet(Timesheet timesheet) {this.timesheet = timesheet;}@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}TimesheetCommand that = (TimesheetCommand) o;if (hours != null ? !hours.equals(that.hours) : that.hours != null) {return false;}if (timesheet != null ? !timesheet.equals(that.timesheet) : that.timesheet != null) {return false;}return true;}@Overridepublic int hashCode() {int result = hours != null ? hours.hashCode() : 0;result = 31 * result + (timesheet != null ? timesheet.hashCode() : 0);return result;}
}

很简单,但是@NotNull@Range注释是什么? 好吧,我们绝对不希望用户输入小时数为负数或零,因此我们将使用此简洁的JSR 303 Bean验证API。 要使其工作,只需将对Hibernate验证器的依赖项添加到pom.xml中

<dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>4.2.0.Final</version></dependency>

将Hibernate Validator放入我们的类路径后,将自动选择默认的验证器。 为了使其工作,我们必须启用注释驱动的MVC,因此将以下行添加到timesheet-servlet.xml bean配置文件中:

<mvc:annotation-driven />

我们将在几行后看到有效模型的用法。

现在,在时间表文件夹下,我们将创建view.jsp页面,其中包含有关单个时间表的信息:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%><%--@elvariable id='tsCommand' type='org.timesheet.web.commands.TimesheetCommand'--%><html>
<head><title>Timesheet page</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h2>Timesheet info</h2><div id='list'><sf:form method='post' modelAttribute='tsCommand'><sf:errors path='*' cssClass='errors' element='div' /><ul><li><label for='employeeName'>Assigned employee:</label><a id='employee' href='../employees/${tsCommand.timesheet.who.id}'>${tsCommand.timesheet.who.name}</a></li><li><label for='task'>Task:</label><a id='task' href='../tasks/${tsCommand.timesheet.task.id}'>${tsCommand.timesheet.task.description}</a></li><li><label for='hours'>Hours:</label><input name='hours' id='hours' value='${tsCommand.hours}' /></li><li><input type='submit' value='Save' /></li></ul></sf:form></div><br /><br /><a href='../timesheets'>Go Back</a>
</body>
</html>

在此视图页面中,我们具有提交按钮,该按钮将触发/ timesheets / {id}上的POST请求并传递更新的模型(该模型中的TimesheetCommand实例)。 因此,让我们处理一下。 我们将使用@Valid批注,它是JSR 303 Bean验证API的一部分,用于标记要验证的对象。 另请注意,必须使用@ModelAttribute批注对TimesheetCommand进行批注,因为此命令已绑定到Web视图。 验证错误存储在BindingResult对象中:

/*** Updates timesheet with given ID* @param id ID of timesheet to lookup from DB* @param tsCommand Lightweight command object with changed hours* @return redirects to timesheets*/@RequestMapping(value = '/{id}', method = RequestMethod.POST)public String updateTimesheet(@PathVariable('id') long id,@Valid @ModelAttribute('tsCommand') TimesheetCommand tsCommand,BindingResult result) {Timesheet timesheet = timesheetDao.find(id);if (result.hasErrors()) {tsCommand.setTimesheet(timesheet);return 'timesheets/view';}// no errors, update timesheettimesheet.setHours(tsCommand.getHours());timesheetDao.update(timesheet);return 'redirect:/timesheets';}

要进行添加,我们必须从现有任务和员工的选择菜单中进行选择,因此在提供新表单时,我们将传递这些列表:

/*** Creates form for new timesheet* @param model Model to bind to HTML form* @return timesheets/new*/@RequestMapping(params = 'new', method = RequestMethod.GET)public String createTimesheetForm(Model model) {model.addAttribute('timesheet', new Timesheet());model.addAttribute('tasks', taskDao.list());model.addAttribute('employees', employeeDao.list());return 'timesheets/new';}

为了显示员工和任务的选择列表,我们再次需要为其创建编辑器。 我们之前已经看到了这种方法,因此像以前一样,我们向我们的项目中添加2个使用相应DAO的新编辑器:

package org.timesheet.web.editors;import org.timesheet.domain.Employee;
import org.timesheet.service.dao.EmployeeDao;import java.beans.PropertyEditorSupport;/*** Will convert ID from combobox to employee's instance.*/
public class EmployeeEditor extends PropertyEditorSupport {private EmployeeDao employeeDao;public EmployeeEditor(EmployeeDao employeeDao) {this.employeeDao = employeeDao;}@Overridepublic void setAsText(String text) throws IllegalArgumentException {long id = Long.parseLong(text);Employee employee = employeeDao.find(id);setValue(employee);}
}
package org.timesheet.web.editors;import org.timesheet.domain.Task;
import org.timesheet.service.dao.TaskDao;import java.beans.PropertyEditorSupport;public class TaskEditor extends PropertyEditorSupport {private TaskDao taskDao;public TaskEditor(TaskDao taskDao) {this.taskDao = taskDao;}@Overridepublic void setAsText(String text) throws IllegalArgumentException {long id = Long.parseLong(text);Task task = taskDao.find(id);setValue(task);}
}

我们将在TimesheetController initBinder方法中注册这些编辑器:

@InitBinderprotected void initBinder(WebDataBinder binder) {binder.registerCustomEditor(Employee.class, new EmployeeEditor(employeeDao));binder.registerCustomEditor(Task.class, new TaskEditor(taskDao));}

现在,我们可以安全地在timesheets文件夹下添加new.jsp ,因为选择列表将正确地填充有模型中传递的数据:

<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %><%--@elvariable id='employees' type='java.util.List<org.timesheet.domain.Employee'--%>
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task'--%><html>
<head><title>Add new timesheet</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h2>Add new Timesheet</h2><div id='list'><sf:form method='post' action='timesheets' commandName='timesheet'><ul><li><label for='employees'>Pick employee:</label><sf:select path='who' id='employees'><sf:options items='${employees}' itemLabel='name' itemValue='id' /></sf:select></li><li><label for='tasks'>Pick task:</label><sf:select path='task' id='tasks'><sf:options items='${tasks}' itemLabel='description' itemValue='id' /></sf:select></li><li><label for='hours'>Hours:</label><sf:input path='hours' /></li><li><input type='submit' value='Save' /></li></ul></sf:form></div><br /><br /><a href='timesheets'>Go Back</a>
</body>
</html>

Submit按钮在/ timesheets路径上提交POST请求,因此我们将使用非常简单的控制器方法来处理此请求:

/*** Saves new Timesheet to the database* @param timesheet Timesheet to save* @return redirects to timesheets*/@RequestMapping(method = RequestMethod.POST)public String addTimesheet(Timesheet timesheet) {timesheetDao.add(timesheet);return 'redirect:/timesheets';}

因此,所有时间表功能现在都应该可以正常工作,只需确保使用应用程序一段时间即可。 当然,我们现在还将为TimesheetController编写单元测试。 在测试方法testUpdateTimesheetValid和testUpdateTimesheetInValid中,我们不是手动验证对象,而是模拟验证器:

package org.timesheet.web;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.validation.BindingResult;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.domain.Timesheet;
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 org.timesheet.web.commands.TimesheetCommand;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 TimesheetControllerTest extends DomainAwareBase {@Autowiredprivate TimesheetDao timesheetDao;@Autowiredprivate EmployeeDao employeeDao;@Autowiredprivate ManagerDao managerDao;@Autowiredprivate TaskDao taskDao;@Autowiredprivate TimesheetController controller;private Model model; // used for controller@Beforepublic void setUp() {model = new ExtendedModelMap();}@Testpublic void testShowTimesheets() {// prepare some dataTimesheet timesheet = sampleTimesheet();// use controllerString view = controller.showTimesheets(model);assertEquals('timesheets/list', view);List<Timesheet> listFromDao = timesheetDao.list();Collection<?> listFromModel = (Collection<?>) model.asMap().get('timesheets');assertTrue(listFromModel.contains(timesheet));assertTrue(listFromDao.containsAll(listFromModel));}@Testpublic void testDeleteTimesheet() {// prepare ID to deleteTimesheet timesheet = sampleTimesheet();timesheetDao.add(timesheet);long id = timesheet.getId();// delete & assertString view = controller.deleteTimesheet(id);assertEquals('redirect:/timesheets', view);assertNull(timesheetDao.find(id));}@Testpublic void testGetTimesheet() {// prepare timesheetTimesheet timesheet = sampleTimesheet();timesheetDao.add(timesheet);long id = timesheet.getId();TimesheetCommand tsCommand = new TimesheetCommand(timesheet);// get & assertString view = controller.getTimesheet(id, model);assertEquals('timesheets/view', view);assertEquals(tsCommand, model.asMap().get('tsCommand'));}@Testpublic void testUpdateTimesheetValid() {// prepare ID to deleteTimesheet timesheet = sampleTimesheet();timesheetDao.add(timesheet);long id = timesheet.getId();TimesheetCommand tsCommand = new TimesheetCommand(timesheet);// user alters Timesheet hours in HTML form with valid valuetsCommand.setHours(1337);BindingResult result = mock(BindingResult.class);when(result.hasErrors()).thenReturn(false);// update & assertString view = controller.updateTimesheet(id, tsCommand, result);assertEquals('redirect:/timesheets', view);assertTrue(1337 == timesheetDao.find(id).getHours());}@Testpublic void testUpdateTimesheetInValid() {// prepare ID to deleteTimesheet timesheet = sampleTimesheet();timesheetDao.add(timesheet);long id = timesheet.getId();TimesheetCommand tsCommand = new TimesheetCommand(timesheet);Integer originalHours = tsCommand.getHours();// user alters Timesheet hours in HTML form with valid valuetsCommand.setHours(-1);BindingResult result = mock(BindingResult.class);when(result.hasErrors()).thenReturn(true);// update & assertString view = controller.updateTimesheet(id, tsCommand, result);assertEquals('timesheets/view', view);assertEquals(originalHours, timesheetDao.find(id).getHours());}@Testpublic void testAddTimesheet() {// prepare timesheetTimesheet timesheet = sampleTimesheet();// save but via controllerString view = controller.addTimesheet(timesheet);assertEquals('redirect:/timesheets', view);// timesheet is stored in DBassertEquals(timesheet, timesheetDao.find(timesheet.getId()));}private Timesheet sampleTimesheet() {Employee marty = new Employee('Martin Brodeur', 'NHL');employeeDao.add(marty);Manager jeremy = new Manager('Jeremy');managerDao.add(jeremy);Task winStanleyCup = new Task('NHL finals', jeremy, marty);taskDao.add(winStanleyCup);Timesheet stanelyCupSheet = new Timesheet(marty, winStanleyCup, 100);timesheetDao.add(stanelyCupSheet);return stanelyCupSheet;}
}

我们要做的最后一个控制器是针对我们的特殊业务服务– TimesheetService。 我们已经实现并测试了它的逻辑。 Controller会将这些功能简单地合并到一个菜单页面,我们将使用Controller处理每个功能。 因此,让我们首先添加一些样板控制器定义和DAO布线:

@Controller
@RequestMapping('/timesheet-service')
public class TimesheetServiceController {private TimesheetService service;private EmployeeDao employeeDao;private ManagerDao managerDao;@Autowiredpublic void setService(TimesheetService service) {this.service = service;}@Autowiredpublic void setEmployeeDao(EmployeeDao employeeDao) {this.employeeDao = employeeDao;}@Autowiredpublic void setManagerDao(ManagerDao managerDao) {this.managerDao = managerDao;}}

当用户使用GET请求进入/ timesheet-service时,我们将使用填充的数据为他提供菜单服务:

/*** Shows menu of timesheet service: * that contains busiest task and employees and managers to* look for their assigned tasks.* @param model Model to put data to* @return timesheet-service/list*/@RequestMapping(method = RequestMethod.GET)public String showMenu(Model model) {model.addAttribute('busiestTask', service.busiestTask());model.addAttribute('employees', employeeDao.list());model.addAttribute('managers', managerDao.list());return 'timesheet-service/menu';}

再次,为了使内容在选择列表中起作用,我们将注册编辑器(我们将仅重用最近创建的编辑器):

@InitBinderprotected void initBinder(WebDataBinder binder) {binder.registerCustomEditor(Employee.class, new EmployeeEditor(employeeDao));binder.registerCustomEditor(Manager.class, new ManagerEditor(managerDao));}

现在我们将提供服务。 我们将再次拥有RESTful URL,但是实际资源不会像以前那样直接映射到域模型,而是一些内部服务的结果。 因此,获取ID为123的经理的任务将导致GET请求时间表/ manager-tasks / 123。 员工任务相同。 我们将使用选择列表的侦听器与jQuery形成实际的URL。 添加时间表服务文件夹,并在其中添加menu.jsp页面,其内容如下:

<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %><%--@elvariable id='busiestTask' type='org.timesheet.domain.Task'--%>
<%--@elvariable id='managers' type='java.util.List<org.timesheet.domain.Manager>'--%>
<%--@elvariable id='employees' type='java.util.List<org.timesheet.domain.Employee>'--%><html>
<head><title>Timesheet Service</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h1>Timesheet services</h1><div id='list'><h3>Busiest task</h3><ul><li><a href='/timesheet-app/tasks/${busiestTask.id}'id='busiest-task'>${busiestTask.description}</a></li></ul><h3>Tasks for manager</h3><sf:form method='get' id='manager-form'><ul><li><select id='select-managers'><c:forEach items='${managers}' var='man'><option value='${man.id}'>${man.name}</option></c:forEach></select></li><li><input type='submit' value='Search' /></li></ul></sf:form><h3>Tasks for employee</h3><sf:form method='get' id='employee-form'><ul><li><select id='select-employees'><c:forEach items='${employees}' var='emp'><option value='${emp.id}'>${emp.name}</option></c:forEach></select></li><li><input type='submit' value='Search'></li></ul></sf:form></div><br /><br /><a href='/timesheet-app/welcome'>Go Back</a><script src='/timesheet-app/resources/jquery-1.7.1.js'></script><script type='text/javascript'>(function() {// set default actionssetAddAction('#select-managers', '#manager-form', 'manager-tasks');setAddAction('#select-employees', '#employee-form', 'employee-tasks');// handler for chaning action$('#select-managers').on('change', function() {setAddAction('#select-managers', '#manager-form', 'manager-tasks');});$('#select-employees').on('change', function() {setAddAction('#select-employees', '#employee-form', 'employee-tasks');});function setAddAction(selectName, formName, action) {var id = $(selectName).val();$(formName).attr('action','/timesheet-app/timesheet-service/' + action + '/' + id);}})();</script>
</body>
</html>

获取给定经理的任务:

/*** Returns tasks for given manager* @param id ID of manager* @param model Model to put tasks and manager* @return timesheet-service/manager-tasks*/@RequestMapping(value = '/manager-tasks/{id}', method = RequestMethod.GET)public String showManagerTasks(@PathVariable('id') long id, Model model) {Manager manager = managerDao.find(id);List<Task> tasks = service.tasksForManager(manager);model.addAttribute('manager', manager);model.addAttribute('tasks', tasks);return 'timesheet-service/manager-tasks';}

结果页面timesheet -service / manager-tasks.jsp将呈现:

<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %><%--@elvariable id='manager' type='org.timesheet.domain.Manager'--%>
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task>'--%><html>
<head><title>Tasks for manager</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h3>Current manager: <a href='/timesheet-app/managers/${manager.id}'>${manager.name}</a></h3><div id='list'><c:forEach items='${tasks}' var='task'><li><a href='/timesheet-app/tasks/${task.id}'>${task.description}</a></li></c:forEach></div><br /><br /><a href='../'>Go Back</a>
</body>
</html>

我们将为员工做几乎相同的事情:

/*** Returns tasks for given employee* @param id ID of employee* @param model Model to put tasks and employee* @return timesheet-service/employee-tasks*/@RequestMapping(value = '/employee-tasks/{id}', method = RequestMethod.GET)public String showEmployeeTasks(@PathVariable('id') long id, Model model) {Employee employee = employeeDao.find(id);List<Task> tasks = service.tasksForEmployee(employee);model.addAttribute('employee', employee);model.addAttribute('tasks', tasks);return 'timesheet-service/employee-tasks';}

然后用jsp查看employee-tasks.jsp:

<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %><%--@elvariable id='employee' type='org.timesheet.domain.Employee'--%>
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task>'--%><html>
<head><title>Tasks for employee</title><link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body><h3>Current employee: <a href='/timesheet-app/employees/${employee.id}'>${employee.name}</a></h3><div id='list'><c:forEach items='${tasks}' var='task'><li><a href='/timesheet-app/tasks/${task.id}'>${task.description}</a></li></c:forEach></div><br /><br /><a href='../'>Go Back</a>
</body>
</html>

因此,请确保一切都集成良好,并为此新contoller添加单元测试:

package org.timesheet.web;import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.jdbc.SimpleJdbcTestUtils;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.service.TimesheetService;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.service.dao.ManagerDao;import static org.junit.Assert.assertEquals;/*** This test relies on fact that DAOs and Services are tested individually.* Only compares, if controller returns the same as individual services.*/
@ContextConfiguration(locations = {'/persistence-beans.xml', '/controllers.xml'})
public class TimesheetServiceControllerTest extends DomainAwareBase {@Autowiredprivate TimesheetServiceController controller;@Autowiredprivate TimesheetService timesheetService;@Autowiredprivate EmployeeDao employeeDao;@Autowiredprivate ManagerDao managerDao;@Autowiredprivate SimpleJdbcTemplate jdbcTemplate;private Model model;private final String createScript = 'src/main/resources/sql/create-data.sql';@Beforepublic void setUp() {model = new ExtendedModelMap();SimpleJdbcTestUtils.executeSqlScript(jdbcTemplate,new FileSystemResource(createScript), false);}@Testpublic void testShowMenu() {String view = controller.showMenu(model);assertEquals('timesheet-service/menu', view);assertEquals(timesheetService.busiestTask(),model.asMap().get('busiestTask'));// this should be done only on small data sample// might cause serious performance cost for completeassertEquals(employeeDao.list(), model.asMap().get('employees'));assertEquals(managerDao.list(), model.asMap().get('managers'));}@Testpublic void testShowManagerTasks() {// prepare some IDManager manager = managerDao.list().get(0);long id = manager.getId();String view = controller.showManagerTasks(id, model);assertEquals('timesheet-service/manager-tasks', view);assertEquals(manager, model.asMap().get('manager'));assertEquals(timesheetService.tasksForManager(manager),model.asMap().get('tasks'));}@Testpublic void testShowEmployeeTasks() {// prepare some IDEmployee employee = employeeDao.list().get(0);long id = employee.getId();String view = controller.showEmployeeTasks(id, model);assertEquals('timesheet-service/employee-tasks', view);assertEquals(employee, model.asMap().get('employee'));assertEquals(timesheetService.tasksForEmployee(employee),model.asMap().get('tasks'));}
}

这部分之后的项目结构(所有新内容都可见):

最终请求映射:

参考: 第5部分–在vrtoonjava博客上从我们的JCG合作伙伴 Michal Vrtiak 添加Spring MVC第2部分 。

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

Spring –添加Spring MVC –第2部分相关推荐

  1. Spring Boot Spring MVC 异常处理的N种方法

    默认行为 根据Spring Boot官方文档的说法: For machine clients it will produce a JSON response with details of the e ...

  2. Spring、Spring MVC、Spring Boot三者的关系还傻傻分不清楚?

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 在本文中,你将获取到有关于Spring,Spring MVC和Spring Boot的概述, ...

  3. 如何在Spring和Spring MVC项目中进行测试

    Spring框架概述 Spring大约包含了20个模块,这些模块组成了核心容器(Core Container).数据访问/集成(Data Access/Integration).Web.AOP(面向切 ...

  4. Spring Boot Spring MVC 异常处理的N种方法 1

    github:https://github.com/chanjarste... 参考文档: Spring Boot 1.5.4.RELEASE Documentation Spring framewo ...

  5. spring(7)spring mvc 的高级技术

    [0]README 1)本文部分文字描述转自:"Spring In Action(中/英文版)",旨在review  "spring(7)spring mvc 的高级技术 ...

  6. Spring–添加SpringMVC –第1部分

    欢迎来到本教程的第四部分. 在这一部分中,我们将使用Spring MVC编写控制器和视图,并考虑我们的REST模型. 我们必须做的第一件事,就是根据目前的情况制作一个Web应用程序. 我们将web / ...

  7. Spring–添加SpringMVC –第2部分

    在上一部分中,我们为经理和员工实现了控制器. 既然我们知道了解决方法,我们将做很少(但仅做很少)更复杂的事情–任务和时间表的控制器. 因此,让我们从org.timesheet.web开始. TaskC ...

  8. Spring 4 Security MVC登录注销示例

    Today we will learn about Spring Security Login Example. Before reading this post, please go through ...

  9. SSM框架实现用户查询、注册、登录——IDEA整合Spring、Spring MVC、Mybatis 框架

    目录 零.前言 一.说明 1.整合说明 2.最终目标 3.数据库准备 二.搭建整合环境 1.创建 maven 工程 2.导入依赖坐标 3.创建java和resources文件夹 4.创建类和接口文件 ...

最新文章

  1. 初识Redis,看这一篇就够了
  2. vue如何sleep_Java常见面试题 非常实用【个人经验】
  3. 加强数据中心安全的六条措施
  4. initramfs两种方法恢复_苹果手机备忘录删除了怎么恢复?两种方法,解决90%恢复问题...
  5. 《关键对话》学习总结
  6. python爬虫豆瓣250_python爬虫二 爬取豆瓣Top250上
  7. Monitor 监测CPU与内存
  8. 【Elasticsearch】Named Entity Annotations in Elasticsearch es 实体 注解
  9. centos7安装tomcat9过程
  10. html选择地区代码,37款城市地区选择器html代码_简约干净
  11. 说明波特率和比特率的关系---再谈一下编码的关系
  12. 计算机基础知识之信息技术初步
  13. 科研ABC - 文献检索
  14. 堆糖:爱豆图片分享社区
  15. grldr文件引导windows xp、ubuntu系统
  16. c语言编写生日祝福语大全,最经典的生日祝福语(精选50句)
  17. 字节编程题 雀魂启动
  18. 不知道有哪些可以语音转文字的软件?推荐你用这个
  19. java毕业设计商品供应管理系统mybatis+源码+调试部署+系统+数据库+lw
  20. jsp实现简单的购物车系统

热门文章

  1. MySQL substring()函数
  2. Java GC系列(2):Java垃圾回收是如何工作的
  3. 我猜,每个程序员对着电梯都想过调度算法吧
  4. 【填坑】博客搬家造成的博客重复问题
  5. java中判断 101-200 之间有多少个素数,并输出所有的素数
  6. hibernate+oracle+servlet实现插入数据的时候,不立马显示!!
  7. mybatis-plus 错误java.lang.NoClassDefFoundError: org/apache/velocity/context/Context
  8. java数据库编程——执行SQL 语句
  9. 对于高并发的理解及实践方案
  10. 如何导入数据模板到MVC