我們看招聘信息的時候,經常會看到這一點,需要具備SSH框架的技能;而且在大部分教學課堂中,也會把SSH作爲最核心的教學內容。
但是,我們在實際應用中發現,SpringMVC可以完全替代Struts,配合註解的方式,編程非常快捷,而且通過restful風格定義url,讓地址看起來非常優雅。
另外,MyBatis也可以替換Hibernate,正因爲MyBatis的半自動特點,我們程序猿可以完全掌控SQL,這會讓有數據庫經驗的程序猿能開發出高效率的SQL語句,而且XML配置管理起來也非常方便。
好了,如果你也認同我的看法,那麼下面我們一起來做整合吧!
在寫代碼之前我們先了解一下這三個框架分別是幹什麼的?
相信大以前也看過不少這些概念,我這就用大白話來講,如果之前有了解過可以跳過這一大段,直接看代碼!
-
SpringMVC:它用於web層,相當於controller(等價於傳統的servlet和struts的action),用來處理用戶請求。舉個例子,用戶在地址欄輸入http://網站域名/login,那麼springmvc就會攔截到這個請求,並且調用controller層中相應的方法,(中間可能包含驗證用戶名和密碼的業務邏輯,以及查詢數據庫操作,但這些都不是springmvc的職責),最終把結果返回給用戶,並且返回相應的頁面(當然也可以只返回json/xml等格式數據)。springmvc就是做前面和後面過程的活,與用戶打交道!!
-
Spring:太強大了,以至於我無法用一個詞或一句話來概括它。但與我們平時開發接觸最多的估計就是IOC容器,它可以裝載bean(也就是我們java中的類,當然也包括service dao裏面的),有了這個機制,我們就不用在每次使用這個類的時候爲它初始化,很少看到關鍵字new。另外spring的aop,事務管理等等都是我們經常用到的。
-
MyBatis:如果你問我它跟鼎鼎大名的Hibernate有什麼區別?我只想說,他更符合我的需求。第一,它能自由控制sql,這會讓有數據庫經驗的人(當然不是說我啦~捂臉~)編寫的代碼能搞提升數據庫訪問的效率。第二,它可以使用xml的方式來組織管理我們的sql,因爲一般程序出錯很多情況下是sql出錯,別人接手代碼後能快速找到出錯地方,甚至可以優化原來寫的sql。
SSM框架整合配置
好了,前面bb那麼多,下面我們真正開始敲代碼了~
首先我們打開IED,我這裏用的是eclipse(你們應該也是用的這個,對嗎?),創建一個動態web項目,建立好相應的目錄結構(重點!)
(打了馬賽克是因爲這裏還用不到,你們不要那麼污好不好?)
我說一下每個目錄都有什麼用吧(第一次畫表格,我發現markdown的表格語法很不友好呀~)
這個目錄結構同時也遵循maven的目錄規範~
文件名 |
作用 |
src |
根目錄,沒什麼好說的,下面有main和test。 |
|
主要目錄,可以放java代碼和一些資源文件。 |
|
存放我們的java代碼,這個文件夾要使用Build Path -> Use as Source Folder,這樣看包結構會方便很多,新建的包就相當於在這裏新建文件夾咯。 |
|
存放資源文件,譬如各種的spring,mybatis,log配置文件。 |
|
存放dao中每個方法對應的sql,在這裏配置,無需寫daoImpl。 |
|
這裏當然是存放spring相關的配置文件,有dao service web三層。 |
|
其實這個可以沒有,但是爲了項目完整性還是加上吧。 |
|
這個貌似是最熟悉的目錄了,用來存放我們前端的靜態資源,如jsp js css。 |
|
這裏的資源是指項目的靜態資源,如js css images等。 |
|
很重要的一個目錄,外部瀏覽器無法訪問,只有羨慕內部才能訪問,可以把jsp放在這裏,另外就是web.xml了。你可能有疑問了,爲什麼上面java中的resources裏面的配置文件不妨在這裏,那麼是不是會被外部竊取到?你想太多了,部署時候基本上只有webapp裏的會直接輸出到根目錄,其他都會放入WEB-INF裏面,項目內部依然可以使用classpath:XXX來訪問,好像IDE裏可以設置部署輸出目錄,這裏扯遠了~ |
|
這裏是測試分支。 |
|
測試java代碼,應遵循包名相同的原則,這個文件夾同樣要使用Build Path -> Use as Source Folder,這樣看包結構會方便很多。 |
|
沒什麼好說的,好像也很少用到,但這個是maven的規範。 |
我先新建好幾個必要的包,併爲大家講解一下每個包的作用,順便理清一下後臺的思路~
包名 |
名稱 |
作用 |
dao |
數據訪問層(接口) |
與數據打交道,可以是數據庫操作,也可以是文件讀寫操作,甚至是redis緩存操作,總之與數據操作有關的都放在這裏,也有人叫做dal或者數據持久層都差不多意思。爲什麼沒有daoImpl,因爲我們用的是mybatis,所以可以直接在配置文件中實現接口的每個方法。 |
entity |
實體類 |
一般與數據庫的表相對應,封裝dao層取出來的數據爲一個對象,也就是我們常說的pojo,一般只在dao層與service層之間傳輸。 |
dto |
數據傳輸層 |
剛學框架的人可能不明白這個有什麼用,其實就是用於service層與web層之間傳輸,爲什麼不直接用entity(pojo)?其實在實際開發中發現,很多時間一個entity並不能滿足我們的業務需求,可能呈現給用戶的信息十分之多,這時候就有了dto,也相當於vo,記住一定不要把這個混雜在entity裏面,答應我好嗎? |
service |
業務邏輯(接口) |
寫我們的業務邏輯,也有人叫bll,在設計業務接口時候應該站在“使用者”的角度。額,不要問我爲什麼這裏沒顯示!IDE調皮我也拿它沒辦法~ |
serviceImpl |
業務邏輯(實現) |
實現我們業務接口,一般事務控制是寫在這裏,沒什麼好說的。 |
web |
控制器 |
springmvc就是在這裏發揮作用的,一般人叫做controller控制器,相當於struts中的action。 |
還有最後一步基礎工作,導入我們相應的jar包,我使用的是maven來管理我們的jar,所以只需要在pom.xml
中加入相應的依賴就好了,如果不使用maven的可以自己去官網下載相應的jar,放到項目WEB-INF/lib目錄下。關於maven的學習大家可以看慕課網的視頻教程,這裏就不展開了。我把項目用到的jar都寫在下面,版本都不是最新的,大家有經驗的話可以自己調整版本號。另外,所有jar都會與項目一起打包放到我的github上,喜歡的給個star吧~
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.soecode.ssm</groupId>
<artifactId>ssm</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>ssm Maven Webapp</name>
<url>http://github.com/liyifeng1994/ssm</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2</version>
</dependency>
</dependencies>
<build>
<finalName>ssm</finalName>
</build>
</project>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
下面真的要開始進行編碼工作了,堅持到這裏辛苦大家了~
第一步:我們先在spring
文件夾裏新建spring-dao.xml
文件,因爲spring的配置太多,我們這裏分三層,分別是dao
service web。
- 讀入數據庫連接相關參數(可選)
- 配置數據連接池
- 配置連接屬性,可以不讀配置項文件直接在這裏寫死
- 配置c3p0,只配了幾個常用的
- 配置SqlSessionFactory對象(mybatis)
- 掃描dao層接口,動態實現dao接口,也就是說不需要daoImpl,sql和參數都寫在xml文件上
spring-dao.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.xsd">
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxPoolSize" value="30" />
<property name="minPoolSize" value="10" />
<property name="autoCommitOnClose" value="false" />
<property name="checkoutTimeout" value="10000" />
<property name="acquireRetryAttempts" value="2" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
<property name="typeAliasesPackage" value="com.soecode.lyf.entity" />
<property name="mapperLocations" value="classpath:mapper/*.xml" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="basePackage" value="com.soecode.lyf.dao" />
</bean>
</beans>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
因爲數據庫配置相關參數是讀取配置文件,所以在resources
文件夾裏新建一個jdbc.properties
文件,存放我們4個最常見的數據庫連接屬性,這是我本地的,大家記得修改呀~還有喜歡傳到github上“大頭蝦們”記得刪掉密碼,不然別人就很容易得到你服務器的數據庫配置信息,然後幹一些羞羞的事情,你懂的!!
jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3307/ssm?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=
友情提示:配置文件中的jdbc.username,如果寫成username,可能會與系統環境中的username變量衝突,所以到時候真正連接數據庫的時候,用戶名就被替換成系統中的用戶名(有得可能是administrator),那肯定是連接不成功的,這裏有個小坑,我被坑了一晚上!!
因爲這裏用到了mybatis,所以需要配置mybatis核心文件,在recources
文件夾裏新建mybatis-config.xml
文件。
- 使用自增主鍵
- 使用列別名
- 開啓駝峯命名轉換 create_time -> createTime
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="useGeneratedKeys" value="true" />
<setting name="useColumnLabel" value="true" />
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
</configuration>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
第二步:剛弄好dao層,接下來到service層了。在spring
文件夾裏新建spring-service.xml
文件。
- 掃描service包所有註解 @Service
- 配置事務管理器,把事務管理交由spring來完成
- 配置基於註解的聲明式事務,可以直接在方法上@Transaction
spring-service.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"
xmlns:tx="http://www.springframework.org/schema/tx"
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.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.soecode.lyf.service" />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
第三步:配置web層,在spring
文件夾裏新建spring-web.xml
文件。
- 開啓SpringMVC註解模式,可以使用@RequestMapping,@PathVariable,@ResponseBody等
- 對靜態資源處理,如js,css,jpg等
- 配置jsp 顯示ViewResolver,例如在controller中某個方法返回一個string類型的”login”,實際上會返回”/WEB-INF/login.jsp”
- 掃描web層 @Controller
spring-web.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"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<mvc:annotation-driven />
<mvc:default-servlet-handler/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<context:component-scan base-package="com.soecode.lyf.web" />
</beans>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
第四步:最後就是修改web.xml
文件了,它在webapp
的WEB-INF
下。
web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1" metadata-complete="true">
<servlet>
<servlet-name>seckill-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>seckill-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
我們在項目中經常會使用到日誌,所以這裏還有配置日誌xml,在resources
文件夾裏新建logback.xml
文件,所給出的日誌輸出格式也是最基本的控制檯s呼出,大家有興趣查看logback官方文檔。
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
到目前爲止,我們一共寫了7個配置文件,我們一起來看下最終的配置文件結構圖。
SSM框架應用實例(圖書管理系統)
一開始想就這樣結束教程,但是發現其實很多人都還不會把這個SSM框架用起來,特別是mybatis部分。那我現在就以最常見的“圖書管理系統”中【查詢圖書】和【預約圖書】業務來做一個demo吧!
首先新建數據庫名爲ssm
,再創建兩張表:圖書表book
和預約圖書表appointment
,並且爲book
表初始化一些數據,sql如下。
schema.sql
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=1000 DEFAULT CHARSET=utf8 COMMENT='圖書表'
-- 初始化圖書數據
INSERT INTO `book` (`book_id`, `name`, `number`)
VALUES
(1000, 'Java程序設計', 10),
(1001, '數據結構', 10),
(1002, '設計模式', 10),
(1003, '編譯原理', 10)
-- 創建預約圖書表
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`),
INDEX `idx_appoint_time` (`appoint_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='預約圖書表'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
在entity
包中添加兩個對應的實體,圖書實體Book.java
和預約圖書實體Appointment.java
。
Book.java
package com.soecode.lyf.entity;
public class Book {
private long bookId;
private String name;
private int number;
}
Appointment.java
package com.soecode.lyf.entity;
import java.util.Date;
/**
* 預約圖書實體
*/
public class Appointment {
private long bookId;
private long studentId;
private Date appointTime;
private Book book;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
在dao
包新建接口BookDao.java
和Appointment.java
BookDao.java
package com.soecode.lyf.dao;
import java.util.List;
import com.soecode.lyf.entity.Book;
public interface BookDao {
/**
* 通過ID查詢單本圖書
*
* @param id
* @return
*/
Book queryById(long id);
/**
* 查詢所有圖書
*
* @param offset 查詢起始位置
* @param limit 查詢條數
* @return
*/
List<Book> queryAll(@Param("offset") int offset, @Param("limit") int limit);
/**
* 減少館藏數量
*
* @param bookId
* @return 如果影響行數等於>1,表示更新的記錄行數
*/
int reduceNumber(long bookId);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
AppointmentDao.java
package com.soecode.lyf.dao;
import org.apache.ibatis.annotations.Param;
import com.soecode.lyf.entity.Appointment;
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
提示:這裏爲什麼要給方法的參數添加@Param
註解呢?是因爲該方法有兩個或以上的參數,一定要加,不然mybatis識別不了。上面的BookDao
接口的queryById
方法和reduceNumber
方法只有一個參數book_id
,所以可以不用加 @Param
註解,當然加了也無所謂~
注意,這裏不需要實現dao接口不用編寫daoImpl, mybatis會給我們動態實現,但是我們需要編寫相應的mapper。
在mapper
目錄裏新建兩個文件BookDao.xml
和AppointmentDao.xml
,分別對應上面兩個dao接口,代碼如下。
BookDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.soecode.lyf.dao.BookDao">
<select id="queryById" resultType="Book" parameterType="long">
SELECT
book_id,
name,
number
FROM
book
WHERE
book_id = #{bookId}
</select>
<select id="queryAll" resultType="Book">
SELECT
book_id,
name,
number
FROM
book
ORDER BY
book_id
LIMIT #{offset}, #{limit}
</select>
<update id="reduceNumber">
UPDATE book
SET number = number - 1
WHERE
book_id = #{bookId}
AND number > 0
</update>
</mapper>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
AppointmentDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.soecode.lyf.dao.AppointmentDao">
<insert id="insertAppointment">
INSERT ignore INTO appointment (book_id, student_id)
VALUES (#{bookId}, #{studentId})
</insert>
<select id="queryByKeyWithBook" resultType="Appointment">
SELECT
a.book_id,
a.student_id,
a.appoint_time,
b.book_id "book.book_id",
b.`name` "book.name",
b.number "book.number"
FROM
appointment a
INNER JOIN book b ON a.book_id = b.book_id
WHERE
a.book_id = #{bookId}
AND a.student_id = #{studentId}
</select>
</mapper>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
mapper總結:namespace
是該xml對應的接口全名,select
和update
中的id
對應方法名,resultType
是返回值類型,parameterType
是參數類型(這個其實可選),最後#{...}
中填寫的是方法的參數,看懂了是不是很簡單!!我也這麼覺得~
還有一個小技巧要交給大家,就是在返回Appointment
對象包含了一個屬性名爲book
的Book對象,那麼可以使用"book.屬性名"
的方式來取值,看上面queryByKeyWithBook
方法的sql。
dao
層寫完了,接下來test
對應的package
寫我們測試方法吧。
因爲我們之後會寫很多測試方法,在測試前需要讓程序讀入spring-dao和mybatis等配置文件,所以我這裏就抽離出來一個BaseTest
類,只要是測試方法就繼承它,這樣那些繁瑣的重複的代碼就不用寫那麼多了~
BaseTest.java
package com.soecode.lyf;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* 配置spring和junit整合,junit啓動時加載springIOC容器 spring-test,junit
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml" })
public class BaseTest {
}
因爲spring-service
在service
層的測試中會時候到,這裏也一起引入算了!
新建BookDaoTest.java
和AppointmentDaoTest.java
兩個dao測試文件。
BookDaoTest.java
package com.soecode.lyf.dao;
import java.util.List;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.soecode.lyf.BaseTest;
import com.soecode.lyf.entity.Book;
public 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 {
List<Book> books = 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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
BookDaoTest測試結果
testQueryById
testQueryAll
testReduceNumber
AppointmentDaoTest.java
package com.soecode.lyf.dao;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.soecode.lyf.BaseTest;
import com.soecode.lyf.entity.Appointment;
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());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
AppointmentDaoTest測試結果
testInsertAppointment
testQueryByKeyWithBook
嗯,到這裏一切到很順利~那麼我們繼續service層的編碼吧~可能下面開始信息裏比較大,大家要做好心理準備~
首先,在寫我們的控制器之前,我們先定義幾個預約圖書操作返回碼的數據字典,也就是我們要返回給客戶端的信息。我們這類使用枚舉類,沒聽過的小夥伴要好好惡補一下了(我也是最近才學到的= =)
預約業務操作返回碼說明
返回碼 |
說明 |
1 |
預約成功 |
0 |
庫存不足 |
-1 |
重複預約 |
-2 |
系統異常 |
新建一個包叫enums
,在裏面新建一個枚舉類AppointStateEnum.java
,用來定義預約業務的數據字典,沒聽懂沒關係,我們直接看代碼吧~是不是感覺有模有樣了!
AppointStateEnum.java
package com.soecode.lyf.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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
接下來,在dto
包下新建AppointExecution.java
用來存儲我們執行預約操作的返回結果。
AppointExecution.java
package com.soecode.lyf.dto;
import com.soecode.lyf.entity.Appointment;
import com.soecode.lyf.enums.AppointStateEnum;
/**
* 封裝預約執行後結果
*/
public class AppointExecution {
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
接着,在exception
包下新建三個文件
NoNumberException.java
RepeatAppointException.java
AppointException.java
預約業務異常類(都需要繼承RuntimeException),分別是無庫存異常、重複預約異常、預約未知錯誤異常,用於業務層非成功情況下的返回(即成功返回結果,失敗拋出異常)。
NoNumberException.java
package com.soecode.lyf.exception;
/**
* 庫存不足異常
*/
public class NoNumberException extends RuntimeException {
public NoNumberException(String message) {
super(message);
}
public NoNumberException(String message, Throwable cause) {
super(message, cause);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
RepeatAppointException.java
package com.soecode.lyf.exception;
/**
* 重複預約異常
*/
public class RepeatAppointException extends RuntimeException {
public RepeatAppointException(String message) {
super(message);
}
public RepeatAppointException(String message, Throwable cause) {
super(message, cause);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
AppointException.java
package com.soecode.lyf.exception;
/**
* 預約業務異常
*/
public class AppointException extends RuntimeException {
public AppointException(String message) {
super(message);
}
public AppointException(String message, Throwable cause) {
super(message, cause);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
咱們終於可以編寫業務代碼了,在service
包下新建BookService.java
圖書業務接口。
BookService.java
package com.soecode.lyf.service;
import java.util.List;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.entity.Book;
/**
* 業務接口:站在"使用者"角度設計接口 三個方面:方法定義粒度,參數,返回類型(return 類型/異常)
*/
public interface BookService {
/**
* 查詢一本圖書
*
* @param bookId
* @return
*/
Book getById(long bookId);
/**
* 查詢所有圖書
*
* @return
*/
List<Book> getList();
/**
* 預約圖書
*
* @param bookId
* @param studentId
* @return
*/
AppointExecution appoint(long bookId, long studentId);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
在service.impl
包下新建BookServiceImpl.java
使用BookService
接口,並實現裏面的方法。
BookServiceImpl
package com.soecode.lyf.service.impl;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.soecode.lyf.dao.AppointmentDao;
import com.soecode.lyf.dao.BookDao;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.entity.Appointment;
import com.soecode.lyf.entity.Book;
import com.soecode.lyf.enums.AppointStateEnum;
import com.soecode.lyf.exception.AppointException;
import com.soecode.lyf.exception.NoNumberException;
import com.soecode.lyf.exception.RepeatAppointException;
import com.soecode.lyf.service.BookService;
@Service
public class BookServiceImpl implements BookService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private BookDao bookDao;
@Autowired
private AppointmentDao appointmentDao;
@Override
public Book getById(long bookId) {
return bookDao.queryById(bookId);
}
@Override
public List<Book> getList() {
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) {
throw new NoNumberException("no number");
} else {
int insert = appointmentDao.insertAppointment(bookId, studentId);
if (insert <= 0) {
throw new RepeatAppointException("repeat appoint");
} else {
Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId);
return new AppointExecution(bookId, AppointStateEnum.SUCCESS, appointment);
}
}
} catch (NoNumberException e1) {
throw e1;
} catch (RepeatAppointException e2) {
throw e2;
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new AppointException("appoint inner error:" + e.getMessage());
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
下面我們來測試一下我們的業務代碼吧~因爲查詢圖書的業務不復雜,所以這裏只演示我們最重要的預約圖書業務!!
BookServiceImplTest.java
package com.soecode.lyf.service.impl;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.soecode.lyf.BaseTest;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.service.BookService;
public 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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
BookServiceImplTest測試結果
testAppoint
首次執行是“預約成功”,如果再次執行的話,應該會出現“重複預約”,哈哈,我們所有的後臺代碼都通過單元測試啦~~是不是很開心~
咱們還需要在dto
包裏新建一個封裝json返回結果的類Result.java
,設計成泛型。
Result.java
package com.soecode.lyf.dto;
/**
* 封裝json對象,所有返回結果都使用它
*/
public class Result<T> {
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
最後,我們寫web層,也就是controller,我們在web
包下新建BookController.java
文件。
BookController.java
package com.soecode.lyf.web;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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.bind.annotation.ResponseBody;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.dto.Result;
import com.soecode.lyf.entity.Book;
import com.soecode.lyf.enums.AppointStateEnum;
import com.soecode.lyf.exception.NoNumberException;
import com.soecode.lyf.exception.RepeatAppointException;
import com.soecode.lyf.service.BookService;
@Controller
@RequestMapping("/book")
public class BookController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private BookService bookService;
@RequestMapping(value = "/list", method = RequestMethod.GET)
private String list(Model model) {
List<Book> list = bookService.getList();
model.addAttribute("list", list);
return "list";
}
@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";
}
@RequestMapping(value = "/{bookId}/appoint", method = RequestMethod.POST, produces = {
"application/json; charset=utf-8" })
@ResponseBody
private Result<AppointExecution> appoint(@PathVariable("bookId") Long bookId, @RequestParam("studentId") Long studentId) {
if (studentId == null || studentId.equals("")) {
return new Result<>(false, "學號不能爲空");
}
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<AppointExecution>(true, execution);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
因爲我比較懶,所以我們就不測試controller了,好討厭寫前端,嗚嗚嗚~
到此,我們的SSM框架整合配置,與應用實例部分已經結束了,我把所有源碼和jar包一起打包放在了我的GitHub上,需要的可以去下載,喜歡就給個star吧,這篇東西寫了兩個晚上也不容易啊。
完整代碼下載地址:https://github.com/liyifeng1994/ssm
2017-02-28更新(感謝網友EchoXml發現):
修改預約業務代碼,失敗時拋異常,成功時才返回結果,控制層根據捕獲的異常返回相應信息給客戶端,而不是業務層直接返回錯誤結果。上面的代碼已經作了修改,而且錯誤示範也註釋保留着,之前誤人子弟了,還好有位網友前幾天提出質疑,我也及時做了修改。
2017-03-30更新(感謝網友ergeerge1建議):
修改BookController幾處錯誤
1.detail方法不是返回json的,故不用加@ResponseBody註解
2.appoint方法應該加上@ResponseBody註解
3.另外studentId參數註解應該是@RequestParam
4.至於controller測試,測試appoint方法可不必寫jsp,用curl就行,比如
curl -H “Accept: application/json; charset=utf-8” -d “studentId=1234567890” localhost:8080/book/1003/appoint
文章轉載:http://blog.csdn.net/qq598535550/article/details/51703190