Tmall_SSM
技術棧 Spring MVC+ Mybatis + Spring + Jsp + Tomcat , 是 Java Web 入門非常好的練手項目
效果展示:
項目簡介
關聯項目
github - 天貓 JavaEE 項目
github - 天貓 SSH 項目
github - 天貓 SSM 項目
之前使用 JavaEE 整套技術和 SSH 框架來作為解決方案,實現模仿天貓網站的各種業務場景,現在開始使用 SSM 框架技術。
項目用到的技術如下:
Java:Java SE基礎
前端:HTML
,CSS
,JavaScript
,JQuery
,AJAX
,Bootstrap
J2EE:Tomcat
,Servlet
,JSP
,Filter
框架:Spring
,Spring MVC
,Mybatis
,SSM整合
數據庫:MySQL
開發工具:IDEA
,Maven
表結構
建表sql 已經放在 Github 項目的 /sql 文件夾下
表名 | 中文含義 | 介紹 |
---|---|---|
Category | 分類表 | 存放分類信息,如女裝,平板電視,沙發等 |
Property | 屬性表 | 存放屬性信息,如顏色,重量,品牌,廠商,型號等 |
Product | 產品表 | 存放產品信息,如LED40EC平板電視機,海爾EC6005熱水器 |
PropertyValue | 屬性值表 | 存放屬性值信息,如重量是900g,顏色是粉紅色 |
ProductImage | 產品圖片表 | 存放產品圖片信息,如產品頁顯示的5個圖片 |
Review | 評論表 | 存放評論信息,如買回來的蠟燭很好用,么么噠 |
User | 用戶表 | 存放用戶信息,如斬手狗,千手小粉紅 |
Order | 訂單表 | 存放訂單信息,包括郵寄地址,電話號碼等信息 |
OrderItem | 訂單項表 | 存放訂單項信息,包括購買產品種類,數量等 |
一 | 多 |
---|---|
Category-分類 | Product-產品 |
Category-分類 | Property-屬性 |
Property-屬性 | PropertyValue-屬性值 |
Product-產品 | PropertyValue-屬性值 |
Product-產品 | ProductImage-產品圖片 |
Product-產品 | Review-評價 |
User-用戶 | Order-訂單 |
Product-產品 | OrderItem-訂單項 |
User-用戶 | OrderItem-訂單項 |
Order-訂單 | OrderItem-訂單項 |
User-用戶 | User-評價 |
以上直接看可能暫時無法完全理解,結合后面具體到項目的業務流程就明白了。
開發流程
首先使用經典的 SSM 模式進行由淺入深地開發出第一個分類管理模塊 ,
然后分析這種方式的弊端,再對其進行項目重構,使得框架更加緊湊,后續開發更加便利和高效率。
分類管理模塊
Category 實體類
准備 Category 實體類,定義對應的字段即可。
舉個例子,對於 分類 / category
的 實體類 和 表結構 設計如下:
Mapper 接口
public interface CategoryMapper {
List<Category> list();
}
CategoryMapper.xml 指定映射的 sql 和結果集
com.caozhihu.tmall.mapper.CategoryMapper 對應上面的 Mapper 接口。mybatis 的 sql 是手打的,還好有逆向工程,后面重構會講。
<mapper namespace="com.caozhihu.tmall.mapper.CategoryMapper">
<select id="list" resultType="Category">
select * from category order by id desc
</select>
</mapper>
CategoryService 接口
public interface CategoryService{
List<Category> list();
}
CategoryServiceImpl 實現類
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
CategoryMapper categoryMapper;
public List<Category> list(){
return categoryMapper.list();
};
}
在 list() 方法中,通過其自動裝配的一個 CategoryMapper 對象的 list() 方法來獲取所有的分類對象。
CategoryController 控制類
@Controller //聲明當前類是一個控制器
@RequestMapping("") //訪問的時候無需額外的地址
public class CategoryController {
@Autowired //自動裝配進 categoryService 接口
CategoryService categoryService;
@RequestMapping("admin_category_list")
public String list(Model model){
List<Category> cs= categoryService.list();
model.addAttribute("cs", cs);
return "admin/listCategory";
}
}
在list方法中,通過 categoryService.list() 獲取所有的 Category 對象,然后放在 "cs" 中,並服務端跳轉到“admin/listCategory” 視圖。
jdbc.properties 數據庫配置文件
#數據庫配置文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/tmall_ssm?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=admin
applicationContext.xml
這里配置使用了阿里巴巴的 druid 數據庫連接池,這些配置基本都是固定寫法,PSCache 就是 PreparedStatement 緩存,據說可以大幅提升性能。
<beans>
<!-- 啟動對注解的識別 -->
<context:annotation-config/>
<context:component-scan base-package="com.caozhihu.tmall.service"/>
<!-- 導入數據庫配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置數據庫連接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 基本屬性 url、user、password -->
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="1"/>
<property name="minIdle" value="1"/>
<property name="maxActive" value="20"/>
<!-- 配置獲取連接等待超時的時間 -->
<property name="maxWait" value="60000"/>
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 1"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize"
value="20"/>
</bean>
<!--Mybatis的SessionFactory配置-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="typeAliasesPackage" value="com.caozhihu.tmall.pojo"/>
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations">
<array>
<value>classpath:com/caozhihu/tmall/mapper/*.xml</value>
<value>classpath:mapper/*.xml</value>
</array>
</property>
<!--分頁插件-->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
</value>
</property>
</bean>
</array>
</property>
</bean>
<!--Mybatis的Mapper文件識別,mybatis-spring提供了MapperScannerConfigurer這個類,
它將會查找類路徑下的映射器並自動將它們創建成MapperFactoryBean-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.caozhihu.tmall.mapper"/>
</bean>
</beans>
這里只放了核心配置部分,頭部命名空間已省略
springMVC.xml
<beans>
<!--啟動注解識別-->
<context:annotation-config/>
<context:component-scan base-package="com.caozhihu.tmall.controller">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<mvc:annotation-driven/>
<!--開通靜態資源的訪問-->
<mvc:default-servlet-handler/>
<!-- 視圖定位 例如 admin/listCategory 會被定位成 /WEB-INF/jsp/admin/listCategory.jsp-->
<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>
<!-- 對上傳文件的解析-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
</beans>
web.xml
web.xml 主要提供如下功能
- 指定 spring 的配置文件為 classpath 下的 applicationContext.xml
- 設置中文過濾器
- 指定 spring mvc 配置文件為 classpath 下的 springMVC.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<!-- spring的配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--中文過濾器-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- spring mvc核心:分發servlet -->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- spring mvc的配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springMVC.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
訪問 jsp 顯示數據
Controller 中的 Model 攜帶數據跳轉到 jsp ,作為視圖,擔當的角色是顯示數據,借助 JSTL 的 c:forEach 標簽遍歷從 CategoryController.list() 傳遞過來的集合。
管理分類剩下部分就不展開了
完整的CategoryMapper.xml
代碼如下
<mapper namespace="com.how2java.tmall.mapper.CategoryMapper">
<select id="list" resultType="Category">
select * from category order by id desc
<if test="start!=null and count!=null">
limit #{start},#{count}
</if>
</select>
<select id="total" resultType="int">
select count(*) from category
</select>
<insert id="add" keyProperty="id" useGeneratedKeys="true" parameterType="Category" >
insert into category ( name ) values (#{name})
</insert>
<delete id="delete">
delete from category where id= #{id}
</delete>
<select id="get" resultType="Category">
select * from category where id= #{id}
</select>
<update id="update" parameterType="Category" >
update category set name=#{name} where id=#{id}
</update>
</mapper>
完整的CategoryMapper
接口代碼如下
public interface CategoryMapper {
List<Category> list(Page page);
int total();
void add(Category category);
void delete(int id);
Category get(int id);
void update(Category category);
}
完整的CategoryService
接口代碼如下
public interface CategoryService{
int total();
List<Category> list(Page page);
void add(Category category);
void delete(int id);
Category get(int id);
void update(Category category);
}
完整的CategoryServiceImpl
實現類代碼就不放着了,只是實現了每個方法,並在其中調用對應的 CategoryMapper 方法而已,如下:
public List<Category> list(Page page) { return categoryMapper.list(page); }
思路流程圖
項目重構
分類管理中的 CategoryMapper.xml 使用很直接的 SQL 語句開發出來,這樣的好處是簡單易懂,便於理解。可是,隨着本項目功能的展開和復雜度的提升,使用這種直接的SQL語句方式的開發效率較低,需要自己手動寫每一個SQL語句,而且其維護起來也比較麻煩。
所以我們做進一步的改進,主要是在分頁方式和逆向工程方面做了重構。
-
分頁方式
目前的分頁方式是自己寫分頁對應的 limit SQL 語句,並且提供一個獲取總數的 count(*) SQL。 不僅如此, mapper, service, service.impl 里都要提供兩個方法:
list(Page page);
count();
分類是這么做的,后續其他所有的實體類要做分頁管理的時候都要這么做,所以為了提高開發效率,把目前的分頁方式改為使用 pageHelper 分頁插件來實現。 -
逆向工程
目前分類管理中 Mybatis 中相關類都是自己手動編寫的,包括:Category.java, CategoryMapper.java和CategoryMapper.xml。
尤其是 CategoryMapper.xml 里面主要是SQL語句,可以預見在接下來的開發任務中,隨着業務邏輯的越來越復雜,SQL 語句也會越來越復雜,進而導致開發速度降低,出錯率增加,維護成本上升等問題。
為了解決手動編寫 SQL 語句效率低這個問題,我們對 Mybatis 部分的代碼,使用逆向工程進行重構。
所謂的逆向工程,就是在已經存在的數據庫表結構基礎上,通過工具,自動生成 Category.java, CategoryMapper.java 和 CategoryMapper.xml,想想就很美好是吧。
pageHelper 分頁
因為使用插件可以獲取總數信息和實現分頁查詢了,所以關於分頁操作的部分配置和代碼要做修改。
修改 CategoryMapper.xml
- 去掉 total SQL 語句
- 修改 list SQL 語句,去掉其中的 limit
<mapper namespace="com.how2java.tmall.mapper.CategoryMapper">
<select id="list" resultType="Category">
select * from category order by id desc
</select>
<insert id="add" keyProperty="id" useGeneratedKeys="true" parameterType="Category" >
insert into category ( name ) values (#{name})
</insert>
<delete id="delete">
delete from category where id= #{id}
</delete>
<select id="get" resultType="Category">
select * from category where id= #{id}
</select>
<update id="update" parameterType="Category" >
update category set name=#{name} where id=#{id}
</update>
</mapper>
使用 PageHelper 提供的方法進行分頁查詢
對 CategoryMapper 接口
/CategoryService 接口
/ CategoryServiceImpl 類
進行如下操作:
- 去掉 total() 方法
- 去掉 list(Page page) 方法
- 新增 list() 方法
使用分頁插件后的 CategoryController.list()方法
@Controller
@RequestMapping("")
public class CategoryController {
@Autowired
CategoryService categoryService;
@RequestMapping("admin_category_list")
public String list(Model model,Page page){
PageHelper.offsetPage(page.getStart(),page.getCount());
List<Category> cs= categoryService.list();
int total = (int) new PageInfo<>(cs).getTotal();
page.setTotal(total);
model.addAttribute("cs", cs);
model.addAttribute("page", page);
return "admin/listCategory";
}
}
在 applicationContext.xml 配置 pagehelper 插件
其實在上面顯示的已經是配置過插件了,這里再提一下,就是在 SqlSessionFactoryBean 命名空間內設置一個 plugins 的屬性。
<!--Mybatis的SessionFactory配置-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
</value>
</property>
</bean>
</array>
</property>
Mybatis 逆向工程
MybatisGenerator 插件是 Mybatis 官方提供的,這個插件存在一個問題 ,即當第一次生成了CategoryMapper.xml 之后,再次運行會導致 CategoryMapper.xml 生成重復內容,而影響正常的運行。
為了解決這個問題,需要自己寫一個小插件類 OverIsMergeablePlugin 。
OverIsMergeablePlugin
這是復制別人的,具體原理還沒研究。
public class OverIsMergeablePlugin extends PluginAdapter {
@Override
public boolean validate(List<String> warnings) {
return true;
}
@Override
public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) {
try {
Field field = sqlMap.getClass().getDeclaredField("isMergeable");
field.setAccessible(true);
field.setBoolean(sqlMap, false);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
generatorConfig.xml 指定生成策略
這里提供一部分代碼,具體的在 github
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!--避免生成重復代碼的插件-->
<plugin type="com.caozhihu.tmall.util.OverIsMergeablePlugin"/>
<!--是否在代碼中顯示注釋-->
<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!--數據庫鏈接地址賬號密碼-->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost/tmall_ssm"
userId="root" password="admin">
</jdbcConnection>
<!--該屬性可以控制是否強制DECIMAL和NUMERIC類型的字段轉換為Java類型的java.math.BigDecimal,默認值為false,一般不需要配置。-->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!--生成pojo類存放位置-->
<javaModelGenerator targetPackage="com.caozhihu.tmall.pojo" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!--生成xml映射文件存放位置-->
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!--生成mapper類存放位置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.caozhihu.tmall.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!--生成對應表及類名-->
<table tableName="category" domainObjectName="Category" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
selectByExampleQueryId="false">
<property name="my.isgen.usekeys" value="true"/>
<property name="useActualColumnNames" value="true"/>
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
MybatisGenerator 生成執行類
運行即生成 mapper,pojo,xml 文件,核心代碼如下
List<String> warnnings = new ArrayList<>();
boolean overwrite = true;
InputStream is = MybatisGenerator.class.getClassLoader().getResource("generatorConfig.xml").openStream();//獲取配置文件對應路徑的輸入流
ConfigurationParser configurationParser = new ConfigurationParser(warnnings);
Configuration configuration = configurationParser.parseConfiguration(is);
is.close();
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(configuration, callback, warnnings);
myBatisGenerator.generate(null);
自動生成的 CategoryMapper.xml
這是插件自動生成的 xml,與我們自己手動寫的也差不了多少,主要區別在於提供了一個 id="Example_Where_Clause" 的 SQL,借助這個可以進行多條件查詢。
自動生成的 pojo 類
MybatisGenerator 會生成一個類叫做 XXXXExample 的。 它的作用是進行排序,條件查詢的時候使用。
在分類管理里用到了排序,但是沒有使用到其條件查詢,在后續的屬性管理里就會看到其條件查詢的用法了。
自動生成的 mapper 接口
與手動編寫的 CategoryMapper 對比,CategoryMapper 也是提供 CURD 一套,不過方法名發生了變化,比如:
delete() 叫做 deleteByPrimaryKey(),
update 叫做 updateByPrimaryKey(),
除此之外,還提供了一個 updateByPrimaryKeySelective()
方法,其作用是只更新,即只修改新插入的不為 null 的字段。(比如當前數據是 {name,age} ,插入新數據是 {newName,null},如果使用此方法,則插入之后數據變為 {newName,age} 而不是 {newName,null})
還有個改動是 list() 方法 ,變成了selectByExample(CategoryExample example);
修改 CategoryServiceImpl 實現類
因為 CategoryMapper 的方法名發生了變化,所以 CategoryServiceImpl 要做相應的調整。
值得一提的是list方法:
public List<Category> list() {
CategoryExample example =new CategoryExample();
example.setOrderByClause("id desc");
return categoryMapper.selectByExample(example);
}
按照這種寫法,傳遞一個 example 對象,這個對象指定按照 id 倒排序來查詢
我查看了 xml 里的映射, 在對應的查詢語句 selectByExample 里面,
會判斷 orderByClause 是否為空,如果不為空就追加 order by ${orderByClause}
這樣如果設置了 orderByClause 的值為“id desc” ,執行的 sql 則會是 order by id desc
然后,我們再根據數據庫字段,一次性生成所有的 實體類,example 類,mapper 和 xml,如果需要定制,直接在生成的東西上修改就行了,真是舒服啊。
后台還有其他管理頁面的,比如屬性管理、產品管理等,由於篇幅原因,具體的請移步github-Tmall_SSM項目。
前台頁面展示
此處是 SSH 跑起來截的圖,SSM 版本目前只做了后台,前台未做,敬請期待...
本文所講不足整個項目的 1/10 ,有興趣的朋友請移步 github 項目的地址 。
參考
天貓SSM整站學習教程 里面除了本項目,還有 Java 基礎,前端,Tomcat 及其他中間件等教程, 可以注冊一個賬戶,能保存學習記錄。