JavaWeb入門_模仿天貓整站Tmall_SSM實踐項目


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
框架:SpringSpring MVCMybatisSSM整合
數據庫: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 主要提供如下功能

  1. 指定 spring 的配置文件為 classpath 下的 applicationContext.xml
  2. 設置中文過濾器
  3. 指定 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() 傳遞過來的集合。

listCategory.jsp 部分代碼

localhost/admin_category_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語句,而且其維護起來也比較麻煩。
所以我們做進一步的改進,主要是在分頁方式和逆向工程方面做了重構。

  1. 分頁方式
    目前的分頁方式是自己寫分頁對應的 limit SQL 語句,並且提供一個獲取總數的 count(*) SQL。 不僅如此, mapper, service, service.impl 里都要提供兩個方法:
    list(Page page);
    count();
    分類是這么做的,后續其他所有的實體類要做分頁管理的時候都要這么做,所以為了提高開發效率,把目前的分頁方式改為使用 pageHelper 分頁插件來實現。

  2. 逆向工程
    目前分類管理中 Mybatis 中相關類都是自己手動編寫的,包括:Category.java, CategoryMapper.java和CategoryMapper.xml。
    尤其是 CategoryMapper.xml 里面主要是SQL語句,可以預見在接下來的開發任務中,隨着業務邏輯的越來越復雜,SQL 語句也會越來越復雜,進而導致開發速度降低,出錯率增加,維護成本上升等問題。
    為了解決手動編寫 SQL 語句效率低這個問題,我們對 Mybatis 部分的代碼,使用逆向工程進行重構。
    所謂的逆向工程,就是在已經存在的數據庫表結構基礎上,通過工具,自動生成 Category.java, CategoryMapper.java 和 CategoryMapper.xml,想想就很美好是吧。


pageHelper 分頁

因為使用插件可以獲取總數信息和實現分頁查詢了,所以關於分頁操作的部分配置和代碼要做修改。

修改 CategoryMapper.xml

  1. 去掉 total SQL 語句
  2. 修改 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 類 進行如下操作:

  1. 去掉 total() 方法
  2. 去掉 list(Page page) 方法
  3. 新增 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 接口

image.png

與手動編寫的 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 及其他中間件等教程, 可以注冊一個賬戶,能保存學習記錄。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM