环境
- Windows10
- JDK1.8
- MySql5.5
工具
- Maven3.5
- IDEA2017
- Navicat(数据库连接工具)
准备工作
步骤
1、新建Maven webapp项目
建好基本目录,按住 Ctrl+Alt+Shift+s配置目录,如下图
新建几个包,如下图:
包名 | 名称 | 作用 |
---|---|---|
dao | 数据访问层(接口 | 与数据操作有关的都放在这里 |
entity | 实体类 | 一般与数据库的表相对应,封装dao层取出来的数据为一个对象,也就是我们常说的pojo,一般只在dao层与service层之间传输 |
dto | 数据传输层 | 用于service层与web层之间传输,相当于vo |
service | 业务逻辑(接口) | 写我们的业务逻辑,在设计业务接口时候应该站在“使用者”的角度。 |
serviceImpl | 业务逻辑(实现) | 实现我们业务接口,一般事务控制是写在这里,没什么好说的。 |
controller | 控制器 | springmvc就是在这里发挥作用的 |
2、配置文件
配置完成的目录如下:
-
pom.xml
4.0.0 com.messchx ssmdemo war 1.0-SNAPSHOT ssmdemo Maven Webapp http://maven.apache.org ssmdemo org.mortbay.jetty maven-jetty-plugin 6.1.7 8888 30000 ${project.build.directory}/${pom.artifactId}-${pom.version} / org.mybatis.generator mybatis-generator-maven-plugin 1.3.2 org.apache.maven.plugins maven-compiler-plugin src/main/java **/*.xml **/*.properties src/main/resources **/*.xml **/*.properties UTF-8 UTF-8 4.3.5.RELEASE 3.4.1 javax javaee-api 7.0 junit junit 4.12 ch.qos.logback logback-classic 1.2.2 com.fasterxml.jackson.core jackson-databind 2.8.7 mysql mysql-connector-java 5.1.41 runtime com.mchange c3p0 0.9.5.2 org.mybatis mybatis ${mybatis.version} org.mybatis mybatis-spring 1.3.1 org.springframework spring-core ${spring.version} org.springframework spring-beans ${spring.version} org.springframework spring-context ${spring.version} org.springframework spring-jdbc ${spring.version} org.springframework spring-tx ${spring.version} org.springframework spring-web ${spring.version} org.springframework spring-webmvc ${spring.version} org.springframework spring-test ${spring.version} -
spring配置文件,主要是dao、service、controller
-
spring-dao.xml
jdbc.properties
jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf8jdbc.username=rootjdbc.password=root
提示:配置文件中的jdbc.username,如果写成username,可能会与系统环境中的username变量冲突,所以到时候真正连接数据库的时候,用户名就被替换成系统中的用户名(有得可能是administrator),那肯定是连接不成功的
mybatis-config.xml
-
spring-service.xml
-
spring-controller.xml
-
-
web.xml
dispatcherServlet org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring/spring-*.xml 1 dispatcherServlet / -
logback.xml(日志)
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n -
generatorConfig.xml,mybatis逆向工程,生成dao(即mapper)和model(即po),这里先不用
到这里基本的配置都完了,剩下的就是写代码。
3、代码,应用实例(图书管理系统)
mysql数据文件:
SET FOREIGN_KEY_CHECKS=0;-- ------------------------------ Table structure for appointment-- ----------------------------DROP TABLE IF EXISTS `appointment`;CREATE TABLE `appointment` ( `book_id` bigint(20) NOT NULL COMMENT '图书ID', `student_id` bigint(20) NOT NULL COMMENT '学号', `appoint_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '预约时间', PRIMARY KEY (`book_id`,`student_id`), KEY `idx_appoint_time` (`appoint_time`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='预约图书表';-- ------------------------------ Records of appointment-- ----------------------------INSERT INTO `appointment` VALUES ('1000', '12345678910', '2018-01-16 15:44:36');INSERT INTO `appointment` VALUES ('1001', '12345678910', '2018-01-16 15:58:02');-- ------------------------------ Table structure for book-- ----------------------------DROP TABLE IF EXISTS `book`;CREATE TABLE `book` ( `book_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '图书ID', `name` varchar(100) NOT NULL COMMENT '图书名称', `number` int(11) NOT NULL COMMENT '馆藏数量', PRIMARY KEY (`book_id`)) ENGINE=InnoDB AUTO_INCREMENT=1004 DEFAULT CHARSET=utf8 COMMENT='图书表';-- ------------------------------ Records of book-- ----------------------------INSERT INTO `book` VALUES ('1000', 'Java程序设计', '9');INSERT INTO `book` VALUES ('1001', '数据结构', '9');INSERT INTO `book` VALUES ('1002', '设计模式', '10');INSERT INTO `book` VALUES ('1003', '编译原理', '10');
3.1 dao层
在entity包中添加图书实体Book.java和预约图书实体Appointment.java
Book.javapublic class Book { private long bookId;// 图书ID private String name;// 图书名称 private int number;// 馆藏数量 // 省略构造方法,getter和setter方法,toString方法}
Appointment.java
/**预约图书实体*/public class Appointment { private long bookId;// 图书ID private long studentId;// 学号 private Date appointTime;// 预约时间 // 多对一的复合属性 private Book book;// 图书实体 // 省略构造方法,getter和setter方法,toString方法}
在dao包新建接口BookDao.java和Appointment.java
BookDao.javapublic interface BookDao { /** * 通过ID查询单本图书 * * @param id * @return */ Book queryById(long id); /** * 查询所有图书 * * @param offset 查询起始位置 * @param limit 查询条数 * @return */ ListqueryAll(@Param("offset") int offset, @Param("limit") int limit); /** * 减少馆藏数量 * * @param bookId * @return 如果影响行数等于>1,表示更新的记录行数 */ int reduceNumber(long bookId);}
AppointmentDao.java
public interface AppointmentDao { /** * 插入预约图书记录 * * @param bookId * @param studentId * @return 插入的行数 */ int insertAppointment(@Param("bookId") long bookId, @Param("studentId") long studentId); /** * 通过主键查询预约图书记录,并且携带图书实体 * * @param bookId * @param studentId * @return */ Appointment queryByKeyWithBook(@Param("bookId") long bookId, @Param("studentId") long studentId);}
提示:这里为什么要给方法的参数添加@Param注解呢?是因为该方法有两个或以上的参数,一定要加,不然mybatis识别不了。上面的BookDao接口的queryById方法和reduceNumber方法只有一个参数book_id,所以可以不用加 @Param注解,当然加了也无所谓~
我们不需要实现dao接口, mybatis会动态实现,但是我们需要编写相应的mapper。在mapper目录里新建两个文件BookDao.xml和ppointmentDao.xml,分别对应上面两个dao接口。BookDao.xml
UPDATE book SET number = number - 1 WHERE book_id = #{bookId} AND number > 0
AppointmentDao.xml
INSERT ignore INTO appointment (book_id, student_id) VALUES (#{bookId}, #{studentId})
mapper总结:namespace是该xml对应的接口全名,select和update中的id对应方法名,resultType是返回值类型,parameterType是参数类型(这个其实可选),最后#{...}中填写的是方法的参数,还有一个小技巧要交给大家,就是在返回Appointment对象包含了一个属性名为book的Book对象,那么可以使用"book.属性名"的方式来取值,看上面queryByKeyWithBook方法的sql。
测试:在测试分支建立相应的包,测试前需要让程序读入spring-dao和mybatis等配置文件,所以我这里就抽离出来一个BaseTest类,只要是测试方法就继承它即可。
BaseTest.java/*** 配置spring和junit整合,junit启动时加载springIOC容器 spring-test,junit*/@RunWith(SpringJUnit4ClassRunner.class)// 告诉junit spring配置文件@ContextConfiguration({ "classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml" })public class BaseTest {}
新建BookDaoTest.java和AppointmentDaoTest.java两个dao测试文件
BookDaoTest.javapublic class BookDaoTest extends BaseTest { @Autowired private BookDao bookDao; @Test public void testQueryById() throws Exception { long bookId = 1000; Book book = bookDao.queryById(bookId); System.out.println(book); } @Test public void testQueryAll() throws Exception { Listbooks = bookDao.queryAll(0, 4); for (Book book : books) { System.out.println(book); } } @Test public void testReduceNumber() throws Exception { long bookId = 1000; int update = bookDao.reduceNumber(bookId); System.out.println("update=" + update); }}
BookDaoTest测试结果,testQueryById
testQueryAll
testReduceNumber
AppointmentDaoTest.java
public class AppointmentDaoTest extends BaseTest{ @Autowired private AppointmentDao appointmentDao; @Test public void testInsertAppointment() throws Exception { long bookId = 1000; long studentId = 12345678910L; int insert = appointmentDao.insertAppointment(bookId, studentId); System.out.println("insert=" + insert); } @Test public void testQueryByKeyWithBook() throws Exception { long bookId = 1000; long studentId = 12345678910L; Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId); System.out.println(appointment); System.out.println(appointment.getBook()); }}
AppointmentDaoTest测试结果,testInsertAppointment
testQueryByKeyWithBook
3.2 servic层
我们先定义几个预约图书操作返回码的数据字典,也就是我们要返回给客户端的信息。我们这类使用枚举类
返回码 | 说明 |
---|---|
1 | 预约成功 |
0 | 库存不足 |
-1 | 重复预约 |
-2 | 系统异常 |
AppointStateEnum.java
package com.messchx.ssm.enums;/** * 使用枚举表述常量数据字典 */public enum AppointStateEnum { SUCCESS(1, "预约成功"), NO_NUMBER(0, "库存不足"), REPEAT_APPOINT(-1, "重复预约"), INNER_ERROR(-2, "系统异常"); private int state; private String stateInfo; private AppointStateEnum(int state, String stateInfo) { this.state = state; this.stateInfo = stateInfo; } public int getState() { return state; } public String getStateInfo() { return stateInfo; } public static AppointStateEnum stateOf(int index) { for (AppointStateEnum state : values()) { if (state.getState() == index) { return state; } } return null; }}
在dto包下新建AppointExecution.java用来存储我们执行预约操作的返回结果。
AppointExecution.java/*** 封装预约执行后结果*/public class AppointExecution { // 图书ID private long bookId; // 秒杀预约结果状态 private int state; // 状态标识 private String stateInfo; // 预约成功对象 private Appointment appointment; public AppointExecution() { } // 预约失败的构造器 public AppointExecution(long bookId, AppointStateEnum stateEnum) { this.bookId = bookId; this.state = stateEnum.getState(); this.stateInfo = stateEnum.getStateInfo(); } // 预约成功的构造器 public AppointExecution(long bookId, AppointStateEnum stateEnum, Appointment appointment) { this.bookId = bookId; this.state = stateEnum.getState(); this.stateInfo = stateEnum.getStateInfo(); this.appointment = appointment; } // 省略getter和setter方法,toString方法}
在exception包下新建三个文件NoNumberException.java RepeatAppointException.java AppointException.java预约业务异常类(都需要继承RuntimeException),分别是无库存异常、重复预约异常、预约未知错误异常,用于业务层非成功情况下的返回(即成功返回结果,失败抛出异常)。
NoNumberException.java/*** 库存不足异常*/public class NoNumberException extends RuntimeException { public NoNumberException(String message) { super(message); } public NoNumberException(String message, Throwable cause) { super(message, cause); }}
RepeatAppointException.java
/*** 重复预约异常*/public class RepeatAppointException extends RuntimeException { public RepeatAppointException(String message) { super(message); } public RepeatAppointException(String message, Throwable cause) { super(message, cause); }}
AppointException.java
/*** 预约业务异常*/public class AppointException extends RuntimeException { public AppointException(String message) { super(message); } public AppointException(String message, Throwable cause) { super(message, cause); }}
在service包下新建BookService.java图书业务接口
BookService.java/*** 业务接口:站在"使用者"角度设计接口 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)*/public interface BookService { /** * 查询一本图书 * * @param bookId * @return */ Book getById(long bookId); /** * 查询所有图书 * * @return */ ListgetList(); /** * 预约图书 * * @param bookId * @param studentId * @return */ AppointExecution appoint(long bookId, long studentId);}
在service.impl包下新建BookServiceImpl.java使用BookService接口,并实现里面的方法。
BookServiceImpl@Servicepublic class BookServiceImpl implements BookService { private Logger logger = LoggerFactory.getLogger(this.getClass()); // 注入Service依赖 @Autowired private BookDao bookDao; @Autowired private AppointmentDao appointmentDao; @Override public Book getById(long bookId) { return bookDao.queryById(bookId); } @Override public ListgetList() { return bookDao.queryAll(0, 1000); } @Override @Transactional /** * 使用注解控制事务方法的优点: 1.开发团队达成一致约定,明确标注事务方法的编程风格 * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作,RPC/HTTP请求或者剥离到事务方法外部 * 3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制 */ public AppointExecution appoint(long bookId, long studentId) { try { // 减库存 int update = bookDao.reduceNumber(bookId); if (update <= 0) {// 库存不足 //return new AppointExecution(bookId, AppointStateEnum.NO_NUMBER);//错误写法 throw new NoNumberException("no number"); } else { // 执行预约操作 int insert = appointmentDao.insertAppointment(bookId, studentId); if (insert <= 0) {// 重复预约 //return new AppointExecution(bookId, AppointStateEnum.REPEAT_APPOINT);//错误写法 throw new RepeatAppointException("repeat appoint"); } else {// 预约成功 Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId); return new AppointExecution(bookId, AppointStateEnum.SUCCESS, appointment); } } // 要先于catch Exception异常前先catch住再抛出,不然自定义的异常也会被转换为AppointException,导致控制层无法具体识别是哪个异常 } catch (NoNumberException e1) { throw e1; } catch (RepeatAppointException e2) { throw e2; } catch (Exception e) { logger.error(e.getMessage(), e); // 所有编译期异常转换为运行期异常 //return new AppointExecution(bookId, AppointStateEnum.INNER_ERROR);//错误写法 throw new AppointException("appoint inner error:" + e.getMessage()); } }}
测试一下业务代码
BookServiceImplTest.javapublic class BookServiceImplTest extends BaseTest { @Autowired private BookService bookService; @Test public void testAppoint() throws Exception { long bookId = 1001; long studentId = 12345678910L; AppointExecution execution = bookService.appoint(bookId, studentId); System.out.println(execution); }}
BookServiceImplTest测试结果
首次执行是“预约成功”,如果再次执行的话,应该会出现“重复预约”在dto包里新建一个封装json返回结果的类Result.java,设计成泛型
Result.java/*** 封装json对象,所有返回结果都使用它*/public class Result{ private boolean success;// 是否成功标志 private T data;// 成功时返回的数据 private String error;// 错误信息 public Result() { } // 成功时的构造器 public Result(boolean success, T data) { this.success = success; this.data = data; } // 错误时的构造器 public Result(boolean success, String error) { this.success = success; this.error = error; } // 省略getter和setter方法}
3.3 controller层
BookController.java
@Controller@RequestMapping("/book") // url:/模块/资源/{id}/细分 /seckill/listpublic class BookController { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private BookService bookService; @RequestMapping(value = "/list", method = RequestMethod.GET) private String list(Model model) { Listlist = bookService.getList(); model.addAttribute("list", list); // list.jsp + model = ModelAndView return "list";// WEB-INF/jsp/"list".jsp } @RequestMapping(value = "/{bookId}/detail", method = RequestMethod.GET) private String detail(@PathVariable("bookId") Long bookId, Model model) { if (bookId == null) { return "redirect:/book/list"; } Book book = bookService.getById(bookId); if (book == null) { return "forward:/book/list"; } model.addAttribute("book", book); return "detail"; } //ajax json @RequestMapping(value = "/{bookId}/appoint", method = RequestMethod.POST, produces = { "application/json; charset=utf-8" }) @ResponseBody private Result appoint(@PathVariable("bookId") Long bookId, @RequestParam("studentId") Long studentId) { if (studentId == null || studentId.equals("")) { return new Result<>(false, "学号不能为空"); } //AppointExecution execution = bookService.appoint(bookId, studentId);//错误写法,不能统一返回,要处理异常(失败)情况 AppointExecution execution = null; try { execution = bookService.appoint(bookId, studentId); } catch (NoNumberException e1) { execution = new AppointExecution(bookId, AppointStateEnum.NO_NUMBER); } catch (RepeatAppointException e2) { execution = new AppointExecution(bookId, AppointStateEnum.REPEAT_APPOINT); } catch (Exception e) { execution = new AppointExecution(bookId, AppointStateEnum.INNER_ERROR); } return new Result (true, execution); }}