SpringData 基於SpringBoot快速入門

SpringData 基於SpringBoot快速入門

本章經過學習SpringData 和SpringBoot 相關知識將面向服務架構(SOA)的單點登陸系統(SSO)須要的代碼實現。這樣能夠從實戰中學習兩個框架的知識,又能夠爲單點登陸系統打下基礎。經過本章你將掌握 SpringBoot項目的搭建,Starter pom的使用,配置全局文件,核心註解SpringBootApplication 介紹以及單元測試 SpringBootTest註解的使用。SpringData 的入門使用,Repository接口的使用,查詢方法關鍵字的規則定義,@Query,@Modifying 註解的使用,最後是開發中的建議和遇到的問題。html

SpringBoot 知識

SpringBoot 是一個用於簡化Spring應用搭建開發的框架。開發過程當中,咱們常常經過配置xml文件來整合第三方技術。而這些重複整合的工做交給了SpringBoot完成。SpringBoot使用"習慣優於配置"的理念幫咱們快速搭建並運行項目。對主流的開發框架能無配置集成。筆者用的開發工具是sts(Spring Tool Suite),其操做和eclipse幾乎一致。若沒有這個工具,建立Maven項目是同樣的。文章底部提供源碼地址!前端

項目搭建

Starter pom

先看看Maven項目核心配置文件 pom.xmljava

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itdragon</groupId>
    <artifactId>springbootStudy</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>springbootStudy</name>
    <description>Demo project for Spring Boot</description>
    <!-- 
        添加 spring boot的父級依賴,它是SpringBoot項目的標誌
        spring-boot-starter-parent 它是一個特殊的starter,提供了不少相關的Maven依賴,不用再爲version而頭疼了,大大簡化了開發
    -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency><!-- 添加web依賴 ,包含spring和springmvc等-->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency><!-- 添加對jpa的支持,包含spring-data和Hibernate -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency><!-- mysql鏈接的jar包 -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 由於SpringBoot內嵌的tomcat不支持jsp頁面,同時SpringBoot也不推薦用jsp -->
           <groupId>org.apache.tomcat.embed</groupId>
           <artifactId>tomcat-embed-jasper</artifactId>
           <scope>provided</scope>
        </dependency>
        <dependency><!-- jsp標籤庫 -->
           <groupId>javax.servlet</groupId>
           <artifactId>jstl</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin><!-- SpringBoot 編譯插件 -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

細心的同窗會發現該文件中出現大量的 spring-boot-starter-* 的語句。SpringBoot之因此能簡化開發的祕密就在這裏------ Starter pom
spring-boot-starter-parent :父級依賴,SpringBoot項目的標誌。裏面封裝了不少jar的版本
spring-boot-starter-web :對web項目的支持,其中包含了SpringMVC和tomcat
spring-boot-starter-data-jpa :對JPA的支持,其中包含了經常使用的SpringData和Hibernate,沒有Mybatis哦
spring-boot-starter-tomcat :使用tomcat做爲Servlet容器
spring-boot-starter-test :對經常使用測試框架的支持,如JUnit
還有不少......mysql

配置全局文件

再看看SpringBoot項目全局配置文件 application.propertiesgit

# 配置tomcat端口號
server.port=8081

# 配置SpringMVC視圖解析器
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# 配置鏈接池,默認使用的是tomcat的鏈接池,但實際不多用tomcat的鏈接池
spring.datasource.url=jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=UTF8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置方言 不然提示:Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
# 自動更新數據庫表結構,也能夠是 validate | update | create | create-drop
spring.jpa.properties.hibernate.hbm2ddl.auto=update
# 顯示sql語句
spring.jpa.show-sql=true

全局配置文件能夠是application.properties 也能夠是 application.yml,建議放在resources目錄下。更多配置: https://github.com/ITDragonBl...github

核心註解

最後是SpringBoot HelloWorld項目的入口類,只須要下面一個java文件,執行main方法,便可實現頁面的跳轉和數據返回的功能。web

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@SpringBootApplication
public class SpringbootStudyApplication {
    
    @RequestMapping("/")
    public String index() {
        return "index";
    }
    
    @RequestMapping("hello")
    @ResponseBody
    public String helloWorld() {
        return "Hello SpringBoot !";
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringbootStudyApplication.class, args);
    }
}

@SpringBootApplication 是 SpringBoot 的核心註解,通常用在入口類上。它是一個組合註解,其中主要內容有一下三個
@SpringBootConfiguration:是一個類級註釋,指示對象是一個bean定義的源,能夠理解爲xml中的beans,通常和 @Bean 註解一塊兒使用。
@EnableAutoConfiguration:啓用 Spring 應用程序上下文的自動配置,試圖猜想和配置您可能須要的bean。自動配置類一般採用基於你的 classpath 和已經定義的 beans 對象進行應用。
@ComponentScan:該註解會自動掃描指定包下的所有標有 @Component、@Service、@Repository、@Controller註解 的類,並註冊成beanspring

SpringData入口類sql

package com.itdragon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StartApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(StartApplication.class, args);
    }

}

SpringDataJPA 知識

SpringData 是一個用於簡化數據庫訪問,並支持雲服務的開源框架。支持非關係型數據庫(NoSQL) 和 關係型數據庫。其主要目的是使數據庫的訪問變得方便快捷。
SpringData JPA 是由Spring提供的簡化JPA開發的框架,致力於減小數據訪問層的開發量。數據庫

POJO層

建立實體類User 表,對應數據庫表名是 itdragon_user,id做爲自增加的主鍵,plainPassword是不保存到數據庫的明文密碼。

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;

/**
 * 用戶實體類
 * @author itdragon
 *
 */
@Table(name="itdragon_user")
@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;                        // 自增加主鍵
    private String account;                    // 登陸的帳號
    private String userName;                // 註冊的暱稱
    @Transient
    private String plainPassword;             // 登陸時的密碼,不持久化到數據庫
    private String password;                // 加密後的密碼
    private String salt;                    // 用於加密的鹽
    private String iphone;                    // 手機號
    private String email;                    // 郵箱
    private String platform;                // 用戶來自的平臺
    private String createdDate;                // 用戶註冊時間
    private String updatedDate;                // 用戶最後一次登陸時間
    
    // 省略get/set/toString 方法
}

Repository接口層

建立UserRepository,這是SpringData 的核心知識點,咱們先看代碼

import java.util.List;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import com.itdragon.pojo.User;

/**
 * 核心知識:SpringData Repository 接口
 * 
 * CrudRepository 接口提供了最基本的對實體類的添刪改查操做 
 * - T save(T entity);                    //保存單個實體 
 * - T findOne(ID id);                    // 根據id查找實體         
 * - void delete(ID/T/Iterable);        // 根據Id刪除實體,刪除實體,批量刪除 
 * PagingAndSortingRepository 提供了分頁與排序功能
 * - <T, ID extends Serializable>             // 第一個參數傳實體類,第二個參數傳註解數據類型
 * - Iterable<T> findAll(Sort sort);         // 排序 
 * - Page<T> findAll(Pageable pageable);     // 分頁查詢(含排序功能)
 * JpaSpecificationExecutor 提供了Specification(封裝 JPA Criteria查詢條件)的查詢功能
 * - List<T> findAll(Specification<T> spec);
 * - Page<T> findAll(Specification<T> spec, Pageable pageable);
 * - List<T> findAll(Specification<T> spec, Sort sort);
 * 
 * 開發建議
 * 1. 這裏值列出的是經常使用方法
 * 2. CrudRepository 中的findAll() 方法要慎用。當數據庫中數據量大,多線程腳本調用findAll方法,系統可能會宕機。
 * 3. CrudRepository 中的deletAll()方法要慎用。這是物理刪除,如今企業通常採用邏輯刪除。
 * 4. PagingAndSortingRepository 和 JpaSpecificationExecutor 能知足大部分業務需求。
 */
public interface UserRepository extends PagingAndSortingRepository<User, Long>, 
    JpaSpecificationExecutor<User>{
    
    /**
     * 重點知識:SpringData 查詢方法定義規範
     * 
     * 1. 查詢方法名通常以 find | read | get 開頭,建議用find
     *     findByAccount : 經過account查詢User
     *     account是User的屬性,拼接時首字母需大寫
     * 2. 支持的關鍵詞有不少好比 Or,Between,isNull,Like,In等
     *     findByEmailEndingWithAndCreatedDateLessThan : 查詢在指定時間前註冊,並以xx郵箱結尾的用戶
     *     And : 而且
     *     EndingWith : 以某某結尾
     *     LessThan : 小於
     * 
     * 注意
     * 如有User(用戶表) Platform(用戶平臺表) 存在一對一的關係,且User表中有platformId字段
     * SpringData 爲了區分:
     * findByPlatFormId     表示經過platformId字段查詢
     * findByPlatForm_Id     表示經過platform實體類中id字段查詢
     * 
     * 開發建議
     * 表的設計,儘可能作單表查詢,以確保高併發場景減輕數據庫的壓力。
     */
    
    // 1 經過帳號查用戶信息
    User findByAccount(String account);
    // 2 獲取指定時間內以xx郵箱結尾的用戶信息
    List<User> findByEmailEndingWithAndCreatedDateLessThan(String email, String createdDate);

    /**
     * 重點知識:使用 @Query 註解
     * 
     * 上面的方法雖然簡單(不用寫sql語句),但它有最爲致命的問題-----不支持複雜查詢,其次是命名太長
     * 1. 使用@Query 註解實現複雜查詢,設置 nativeQuery=true 使查詢支持原生sql
     * 2. 配合@Modifying 註解實現建立,修改,刪除操做
     * 3. SpringData 默認查詢事件爲只讀事務,若要修改數據則需手動添加事務註解
     * 
     * 注意
     * 若@Query 中有多個參數,SpringData 提供兩種方法:
     * 第一種 ?1 ... ?2         要求參數順序一致
     * 第二種 :xxx ... :yyy     xxx 和 yyy 必須是實體類對應的屬性值,不要求參數順序但參數前要加上@Param("xxx")
     * 模糊查詢可以使用 %xxx%
     * 
     * 開發建議
     * 1. 參數填寫的順序要保持一致,不要給本身添加麻煩
     * 2. 建議使用@Query,可讀性較高
     */
    // 3 獲取某平臺活躍用戶數量
    @Query(value="SELECT count(u.id) FROM User u WHERE u.platform = :platform AND u.updatedDate <= :updatedDate")
    long getActiveUserCount(@Param("platform")String platform, @Param("updatedDate")String updatedDate);
    
    // 4 經過郵箱或者手機號模糊查詢用戶信息
    @Query(value="SELECT u FROM User u WHERE u.email LIKE %?1% OR u.iphone LIKE %?2%")
    List<User> findByEmailAndIhpneLike(String email, String iphone);
    
    // 5 修改用戶郵箱
    @Modifying
    @Query("UPDATE User u SET u.email = :email WHERE u.id = :id")
    void updateUserEmail(@Param("id") Long id, @Param("email") String email);
    
}

代碼中共有五個方法,每一個方法都包含了不少的知識點。
方法一和方法二主要介紹的是SpringData關鍵字的用法。
1 關鍵字的解析
這裏用findByPlatFormId() 方法來介紹SpringData 解析查詢方法的流程。
第一步:去除關鍵字findBy
第二步:剩下的PlatFormId 首字母小寫並在User對象中找是否有該屬性,如有則查詢並結束。若沒有則第三步
第三步:platFormId,從右到左截取到第一個大寫字母,再判斷剩下的platForm是否爲User對象,如此循環直到結束爲止。
2 級聯屬性區分
若查詢的屬性是實體類,爲了不誤會和衝突,用"_"表示屬性中的屬性
3 查詢分頁排序
若findByPlatFormId() 方法想要排序或者分頁,是能夠在後面加Pageable,Sort參數。
findByPlatFormId(String platFormId, Pageable pageable)
findByPlatFormId(String platFormId, Sort sort)
4 其餘的關鍵字
關鍵字

方法三到方法五主要介紹的是 @Query 註解的使用。
1 傳參方式
索引參數:?n ,n從1開始,表示第一個參數。方法傳入的參數的照順序和個數要和 n 保持一致。
命名參數::key ,傳參必須有 @Param("key") 註解修飾
2 原生的sql
@Query 註解支持本地查詢,即用原生的sql語句。如:@Query(value="xxxx", nativeQuery=true)
3 @Modifying
若直接執行修改操做,SpringDataJPA 會提示錯誤信息 Executing an update/delete query 。是由於Spring Data 默認全部的查詢均聲明爲只讀事務。
因此咱們要在Service層添加 @Transactional 註解。

SpringDataJPA 核心知識Repository接口
Repository: 空接口,標識做用,代表任何繼承它的均爲Repository接口類
CrudRepository: 繼承 Repository,實現了一組 CRUD 相關的方法 
PagingAndSortingRepository: 繼承 CrudRepository,實現了一組分頁排序相關的方法
JpaRepository: 繼承 PagingAndSortingRepository,實現一組 JPA 規範相關的方法
JpaSpecificationExecutor: 不屬於Repository體系,實現一組 JPA Criteria 查詢相關的方法 
PagingAndSortingRepository 和 JpaSpecificationExecutor 基本知足企業中大部分的需求。也能夠自定義Repository,只需繼承 JpaRepository 便可具有了通用的數據訪問控制層的能力。
進入各自接口類中,使用快捷鍵 Ctrl + o 便可查看當前類的全部方法,因此這裏就不貼出來了。

JpaSpecificationExecutor
PagingAndSortingRepository
CrudRepository

 

Service層

建立UserService 並加上註解 @Transactional

import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.itdragon.common.ItdragonResult;
import com.itdragon.pojo.User;
import com.itdragon.repository.UserRepository;

@Service
@Transactional
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public ItdragonResult registerUser(User user) {
        // 檢查用戶名是否註冊,通常在前端驗證的時候處理,由於註冊不存在高併發的狀況,這裏再加一層查詢是不影響性能的
        if (null != userRepository.findByAccount(user.getAccount())) {
            return ItdragonResult.build(400, "");
        }
        userRepository.save(user);
        // 註冊成功後選擇發送郵件激活。如今通常都是短信驗證碼
        return ItdragonResult.build(200, "");
    }
    
    public ItdragonResult editUserEmail(String email) {
        // 經過Session 獲取用戶信息, 這裏僞裝從Session中獲取了用戶的id,後面講解SOA面向服務架構中的單點登陸系統時,修改此處代碼 FIXME
        long id = 3L;
        // 添加一些驗證,好比短信驗證
        userRepository.updateUserEmail(id, email);
        return ItdragonResult.ok();
    }
    
}

單元測試

SpringBoot 的單元測試,須要用到 @RunWith 和 @SpringBootTest註解,代碼註釋中有詳細介紹。

import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.junit4.SpringRunner;
import com.itdragon.StartApplication;
import com.itdragon.common.ItdragonUtils;
import com.itdragon.pojo.User;
import com.itdragon.repository.UserRepository;
import com.itdragon.service.UserService;

/**
 * @RunWith    它是一個運行器
 * @RunWith(SpringRunner.class) 表示讓測試運行於Spring測試環境,不用啓動spring容器便可使用Spring環境
 * @SpringBootTest(classes=StartApplication.class)  表示將StartApplication.class歸入到測試環境中,若不加這個則提示bean找不到。
 * 
 * @author itdragon
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes=StartApplication.class)
public class SpringbootStudyApplicationTests {
    
    @Autowired
    private UserService userService;
    @Autowired
    private UserRepository userRepository;

    @Test
    public void contextLoads() {
    }
    
    @Test    // 測試註冊,新增數據
    public void registerUser() {
        User user = new User();
        user.setAccount("gitLiu");
        user.setUserName("ITDragonGit");
        user.setEmail("itdragon@git.com");
        user.setIphone("12349857999");
        user.setPlainPassword("adminroot");
        user.setPlatform("github");
        user.setCreatedDate(ItdragonUtils.getCurrentDateTime());
        user.setUpdatedDate(ItdragonUtils.getCurrentDateTime());
        ItdragonUtils.entryptPassword(user);
        userService.registerUser(user);
    }
    
    @Test    // 測試SpringData 關鍵字
    public void findByEmailEndingWithAndCreatedDateLessThan() {
        List<User> users = userRepository.findByEmailEndingWithAndCreatedDateLessThan("qq.com", ItdragonUtils.getCurrentDateTime());
        System.out.println(users.toString());
    }
    
    @Test    // 測試SpringData @Query 註解和傳多個參數
    public void getActiveUserCount() {
        long activeUserCount = userRepository.getActiveUserCount("weixin", ItdragonUtils.getCurrentDateTime());
        System.out.println(activeUserCount);
    }
    
    @Test    // 測試SpringData @Query 註解,傳多個參數 和 like 查詢
    public void findByEmailAndIhpneLike() {
        List<User> users = userRepository.findByEmailAndIhpneLike("163.com", "6666");
        System.out.println(users.toString());
    }
    
    @Test    // 測試SpringData @Query 註解 和 @Modifying 註解
    public void updateUserEmail() {
        /**
         * org.springframework.dao.InvalidDataAccessApiUsageException:Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query
         * userRepository.updateUserEmail(3L, "update@email.com");
         */
        userService.editUserEmail("update@email.com");
    }
    
    @Test    // 測試SpringData PagingAndSortingRepository 接口
    public void testPagingAndSortingRepository() {
        int page = 1;     // 從0開始,第二頁
        int size = 3;    // 每頁三天數據
        PageRequest pageable = new PageRequest(page, size, new Sort(new Order(Direction.ASC, "id")));
        Page<User> users = userRepository.findAll(pageable);
        System.out.println(users.getContent().toString()); // 當前數據庫中有5條數據,正常狀況能夠打印兩條數據,id分別爲4,5 (先排序,後分頁)
    }
    
    @Test    // 測試SpringData JpaSpecificationExecutor 接口
    public void testJpaSpecificationExecutor(){
        int pageNo = 1;
        int pageSize = 3;
        PageRequest pageable = new PageRequest(pageNo, pageSize);
        Specification<User> specification = new Specification<User>() {
            @Override
            public Predicate toPredicate(Root<User> root,
                    CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate predicate = cb.gt(root.get("id"), 1); // 查詢id 大於 1的數據
                return predicate;
            }
        };
        Page<User> users = userRepository.findAll(specification, pageable);
        System.out.println(users.getContent().toString());    // 當前數據庫中有5條數據,正常狀況能夠打印一條數據,id爲5
    }
    
}

可能存在的問題

項目啓動時提示 Unknown character set: 'utf8mb4'

致使的緣由多是mysql服務器版本安裝不正確,解決方法有兩種。
第一種:換mysql-connector-java jar包版本爲 5.1.6 (不推薦); 當前jar版本爲 5.1.44。
第二種:重裝mysql版本,當前最新版本是5.7。教程都準備好了。
https://www.cnblogs.com/sshou... (mysql安裝)
http://blog.csdn.net/y6947219... (mysql卸載)

SpringBoot 鏈接池配置疑惑

咱們只是在全局配置文件中設置了相關值,就完成了鏈接池的配置,想必你們都有所疑惑。其實當咱們在pom.xml文件中加入spring-boot-starter-data-jpa 依賴時,SpringBoot就會自動使用tomcat-jdbc鏈接池。
固然咱們也可使用其餘的鏈接池。
https://www.cnblogs.com/gslbl... (springBoot數據庫鏈接池經常使用配置)
https://www.cnblogs.com/xiaos... (SpringBoot使用c3p0)

STS工具 ctrl + shift + o 從新導包快捷鍵失效

解決方法:preference -> general -> keys ,找到 Organize Imports ,而後 在 When 裏面選擇 Editing Java Source

總結

1 SpringDataJPA 是簡化JPA開發的框架,SpringBoot是簡化項目開發的框架。
2 spring-boot-starter-parent 是SpringBoot項目的標誌
3 @SpringBootApplication 註解是SpringBoot項目的入口
4 SpringData 經過查詢關鍵字和 @Query註解實現對數據庫的訪問
5 SpringData 經過PagingAndSortingRepository 實現分頁,排序和經常使用的crud操做

源碼地址:https://github.com/ITDragonBl...

到這裏 SpringData 基於SpringBoot快速入門就結束了,若是有什麼問題請指教,若是以爲不錯能夠點一下推薦。

相關文章
相關標籤/搜索