SpringBoot 2.3 整合最新版 ShardingJdbc + Druid + MyBatis 實現分庫分表

  今天項目不忙,想搞一下shardingJDBC分庫分表看看,主要想實現以下幾點:

  1. 捨棄xml配置,使用.yml或者.properties文件+java的方式配置spring。
  2. 使用 Druid 作為數據庫連接池,同時開啟監控界面,並支持監控多數據源。
  3. 不依賴 com.dangdangsharding-jdbc-core 包。此包過於古老,最後一次更新在2016年。目測只是封裝了一層,意義不大。感覺如果不是dangdang公司內部開發,沒必要用這個包。(且本人實測不能和最新的Druid包一起用,insert語句報錯)

  折騰了半天,網上找的例子大部分跑不通。直接自己從零開搞,全部組件直接上當前最新版本。

  SpringBoot: 2.3.0

  mybatis: 2.1.3

  druid: 1.1.22

  sharding-jdbc: 4.1.1

  注意:這裏因為是自己邊看源碼邊配置,(sharding官網的例子可能是版本問題基本沒法用,GitHub 我這裏網絡基本打不開),所以數據源和sharding大部分用java代碼配置。了解配置原理后,也可以簡化到 .yml / .properties 文件中。

Sharding-JDBC簡介

  Apache ShardingSphere 是一套開源的分佈式數據庫中間件解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規劃中)這 3 款相互獨立,卻又能夠混合部署配合使用的產品組成。

  Sharding-JDBC定位為輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅動,完全兼容 JDBC 和各種 ORM 框架。

  • 適用於任何基於 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
  • 支持任何第三方的數據庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
  • 支持任意實現JDBC規範的數據庫。目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 標準的數據庫。

Sharding配置示意圖

  簡單的理解如下圖,對sharding-jdbc進行配置,其實就是對所有需要進行分片的表進行配置。對錶的配置,則主要是對分庫的配置和分表的配置。這裏可以只分庫不分表,或者只分表不分庫,或者同時包含分庫和分表邏輯。

 

  先看一下我的項目目錄結構整體如下:

  

一、POM依賴配置

  完整的pom表如下,其中主要是對 mysql-connector-java、mybatis-spring-boot-starter、druid-spring-boot-starter、sharding-jdbc-core 的依賴。

  注意:sharding-jdbc-core 我用的4.0+的版本,因為已經晉陞為 apache 基金會的頂級項目,其 groupId 變為了 org.apache.shardingsphere,之前是io.shardingsphere。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>shardingjdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shardingjdbc</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <!--<sharding.jdbc.version>3.0.0</sharding.jdbc.version>-->
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-core</artifactId>
            <version>4.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.5</version>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

pom.xml

二、application.properties

  這裏配置了兩個數據源,為避免和自動裝配產生衝突,屬性前綴要和自動裝配掃描的前綴區分開,這裏我用 datasource0datasource1

  下面 spring.datasource.druid 開頭的配置,會被 druid 的代碼自動掃描裝配。

#################################### common config : ####################################
spring.application.name=shardingjdbc
# 應用服務web訪問端口
server.port=8080

# mybatis配置
mybatis.mapper-locations=classpath:com/example/shardingjdbc/mapper/*.xml
mybatis.type-aliases-package=com.example.shardingjdbc.**.entity

datasource0.url=jdbc:mysql://localhost:3306/test0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
datasource0.driver-class-name=com.mysql.cj.jdbc.Driver
datasource0.type=com.alibaba.druid.pool.DruidDataSource
datasource0.username=root
datasource0.password=852278

datasource1.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
datasource1.driver-class-name=com.mysql.cj.jdbc.Driver
datasource1.type=com.alibaba.druid.pool.DruidDataSource
datasource1.username=root
datasource1.password=852278

#
##### 連接池配置 #######
# 過濾器設置(第一個stat很重要,沒有的話會監控不到SQL)
spring.datasource.druid.filters=stat,wall,log4j2

##### WebStatFilter配置 #######
#啟用StatFilter
spring.datasource.druid.web-stat-filter.enabled=true
#添加過濾規則
spring.datasource.druid.web-stat-filter.url-pattern=/*
#排除一些不必要的url
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
#開啟session統計功能
spring.datasource.druid.web-stat-filter.session-stat-enable=true
#缺省sessionStatMaxCount是1000個
spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
#spring.datasource.druid.web-stat-filter.principal-session-name=
#spring.datasource.druid.web-stat-filter.principal-cookie-name=
#spring.datasource.druid.web-stat-filter.profile-enable=

##### StatViewServlet配置 #######
#啟用內置的監控頁面
spring.datasource.druid.stat-view-servlet.enabled=true
#內置監控頁面的地址
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
#關閉 Reset All 功能
spring.datasource.druid.stat-view-servlet.reset-enable=false
#設置登錄用戶名
spring.datasource.druid.stat-view-servlet.login-username=admin
#設置登錄密碼
spring.datasource.druid.stat-view-servlet.login-password=123
#白名單(如果allow沒有配置或者為空,則允許所有訪問)
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
#黑名單(deny優先於allow,如果在deny列表中,就算在allow列表中,也會被拒絕)
spring.datasource.druid.stat-view-servlet.deny=

三、數據源和分片配置

  如下代碼,先從配置文件讀取數據源的所需要的屬性,然後生成 Druid 數據源。注意這裏配置語句中的 setFilters,如果不添加 filters,則 Duird 監控界面無法監控到sql。另外,其他諸如最大連接數之類的屬性這裏沒有配,按需配置即可。數據源創建好后,添加到 dataSourceMap 集合中。

  再往下註釋比較清楚,構造 t_user 表的分片規則(包括分庫規則 + 分表規則),然後將所有表的分片規則組裝成 ShardingRuleConfiguration

  最後,將前兩步配好的 dataSourceMapshardingRuleConfiguration 交給 ShardingDataSourceFactory,用來構造數據源。

  到這裏,sharding 、druid 的配置代碼就都寫好了。剩下基本都是業務代碼了。

package com.example.shardingjdbc.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.example.shardingjdbc.sharding.UserShardingAlgorithm;
import org.apache.shardingsphere.api.config.sharding.ShardingRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.TableRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.strategy.StandardShardingStrategyConfiguration;
import org.apache.shardingsphere.shardingjdbc.api.ShardingDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

@Configuration
public class DataSourceConfig {
    @Value("${datasource0.url}")
    private String url0;
    @Value("${datasource0.username}")
    private String username0;
    @Value("${datasource0.password}")
    private String password0;
    @Value("${datasource0.driver-class-name}")
    private String driverClassName0;

    @Value("${datasource1.url}")
    private String url1;
    @Value("${datasource1.username}")
    private String username1;
    @Value("${datasource1.password}")
    private String password1;
    @Value("${datasource1.driver-class-name}")
    private String driverClassName1;

    @Value(("${spring.datasource.druid.filters}"))
    private String filters;

    @Bean("dataSource")
    public DataSource dataSource() {
        try {
            DruidDataSource dataSource0 = new DruidDataSource();
            dataSource0.setDriverClassName(this.driverClassName0);
            dataSource0.setUrl(this.url0);
            dataSource0.setUsername(this.username0);
            dataSource0.setPassword(this.password0);
            dataSource0.setFilters(this.filters);

            DruidDataSource dataSource1 = new DruidDataSource();
            dataSource1.setDriverClassName(this.driverClassName1);
            dataSource1.setUrl(this.url1);
            dataSource1.setUsername(this.username1);
            dataSource1.setPassword(this.password1);
            dataSource1.setFilters(this.filters);

            //分庫設置
            Map<String, DataSource> dataSourceMap = new HashMap<>(2);
            //添加兩個數據庫database0和database1
            dataSourceMap.put("ds0", dataSource0);
            dataSourceMap.put("ds1", dataSource1);

            // 配置 t_user 表規則
            TableRuleConfiguration userRuleConfiguration = new TableRuleConfiguration("t_user", "ds${0..1}.t_user${0..1}");
            // 配置分表規則
            userRuleConfiguration.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("id", UserShardingAlgorithm.tableShardingAlgorithm));
            // 配置分庫規則
            userRuleConfiguration.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("id", UserShardingAlgorithm.databaseShardingAlgorithm));
            // Sharding全局配置
            ShardingRuleConfiguration shardingRuleConfiguration = new ShardingRuleConfiguration();
            shardingRuleConfiguration.getTableRuleConfigs().add(userRuleConfiguration);
            // 創建數據源
            DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, new Properties());
            return dataSource;
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }
}

DataSourceConfig.java

  上面構造分片規則的時候,我定義了User表的分片算法類 UserShardingAlgorithm,並定義了兩個內部類分別實現了數據庫分片和表分片的邏輯。代碼如下:

package com.example.shardingjdbc.sharding;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

public class UserShardingAlgorithm {
    public static final DatabaseShardingAlgorithm databaseShardingAlgorithm = new DatabaseShardingAlgorithm();
    public static final TableShardingAlgorithm tableShardingAlgorithm = new TableShardingAlgorithm();

    static class DatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
        @Override
        public String doSharding(Collection<String> databaseNames, PreciseShardingValue<Long> shardingValue) {
            for (String database : databaseNames) {
                if (database.endsWith(String.valueOf(shardingValue.getValue() % 2))) {
                    return database;
                }
            }

            return "";
        }
    }

    static class TableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
        @Override
        public String doSharding(Collection<String> tableNames, PreciseShardingValue<Long> shardingValue) {
            for (String table : tableNames) {
                if (table.endsWith(String.valueOf(shardingValue.getValue() % 2))) {
                    return table;
                }
            }

            return "";
        }
    }
}

UserShardingAlgorithm.java

  這裏實現分片規則時,實現的接口是 PreciseShardingAlgorithm,即精確分片,將指定的鍵值記錄映射到指定的1張表中(最多1張表)。這個接口基本上能滿足80%的需求了。

  其他的還有 Range、ComplexKey、Hint分片規則,這3種都可以將符合條件的鍵值記錄映射到多張表,即可以將記錄 a 同時插入A、B 或 B、C多張表中。

  其中,

    Range 是範圍篩選分片。我個人理解,比如id尾數1-5插入A表,6-0插入B表,這種情況,使用Range作為篩選條件更方便。也可以根據時間範圍分片。(如有誤請指正)。

    ComplexKey 看名字就是組合鍵分片,可以同時根據多個鍵,制定映射規則。

    Hint 看名字沒看懂,但看源碼其實也是組合鍵分片,但僅支持對組合鍵進行精確篩選。

    而 ComplexKey 支持對組合鍵進行範圍篩選。所以可以理解為 ComplexKey 是 Hint 的高級版本。  

  不管實現哪種分片算法,都要確保算法覆蓋所有可能的鍵值。

四、使用行表達式配置分片策略(對第三步優化,可略過)

    上面第三步,我們通過實現 PreciseShardingValue 接口,來定義分片算法。這樣每有一張表需要分片,都要重新定義一個類,太麻煩。

  Sharding 提供了行表達式配置的方式,對簡單的分片邏輯,直接定義一個行表達式即可。(這種方式其實就是直接在 .yml 文件中配置分片策略的解析方式)

  和上面的代碼類似,這裏之改動了6、8行,直接 new 一個 InlineShardingStrategyConfiguration,省去了定義分片算法類的繁瑣步驟。

 

 1              // .....省略其他代碼
 2  
 3             // 配置 t_user 表規則
 4             TableRuleConfiguration userRuleConfiguration = new TableRuleConfiguration("t_user", "ds${0..1}.t_user${0..1}");
 5             // 行表達式分表規則
 6             userRuleConfiguration.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("id", "t_user${id % 2}"));
 7             // 行表達式分庫規則
 8             userRuleConfiguration.setDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("id", "ds${id % 2}"));
 9 
10             // Sharding全局配置
11             ShardingRuleConfiguration shardingRuleConfiguration = new ShardingRuleConfiguration();
12             shardingRuleConfiguration.getTableRuleConfigs().add(userRuleConfiguration);
13             // 創建數據源
14             DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, new Properties());
15             return dataSource;

五、分佈式主鍵(雪花算法)

  分庫后,不能再使用 mysql 的自增主鍵,否則會產生重複主鍵。自定義主鍵,主要需要解決兩個問題:

  1. 主鍵唯一(必須)
  2. 主鍵單調遞增(可選)(提升索引效率,減少索引重排產生的空間碎片)

  Sharding 內部提供了2個主鍵生成器,一個使用雪花算法 SnowflakeShardingKeyGenerator,一個使用 UUID(考慮上面第2條,因此不使用 UUID)。

  雪花算法的主要原理:用一個 64 bit 的 long 型数字做主鍵。其中,

    第 1 位,1 bit 作為符號位永遠為 0,表示是正數。

    第 2 – 42 位, 41 個 bit 填充時間戳。

    第 43 – 52 位,10 個 bit 填充機器唯一id。舉個例子,可以用前4位標識機房號,后6位標識機器號。

    第 53 – 64 位,12 個 bit 填充id序號。範圍 0 – 4095,即每台機器每 1 毫秒最多生成 4096 個不同的主鍵id。

  雪花算法的主要實現代碼如下

  1. 先判斷時鐘是否回調。這裏默認容忍回調時間為0,如有回調則會產生異常。可以通過配置 max.tolerate.time.difference.milliseconds 屬性,讓其自旋等待時鐘回到上一次執行時間。
  2. 按當前毫秒數,遞增生成id序號。如果時鐘進入了下一毫秒,則從0開始重新生成id序號,範圍 0 – 4095。
  3. 將 時間戳 + 機器序號 + id序號 拼裝成 主鍵id。這裏機器序號默認為0,可以通過 worker.id 屬性進行配置。不同的服務器需要配置成不同的数字,範圍 0 – 1023。

  其中 EPOCH 是時鐘起點,sharding中設置的是2016年11月1日,那麼41位的時間戳差不多可以用70年,一直到2086年。

    public synchronized Comparable<?> generateKey() {
        long currentMilliseconds = timeService.getCurrentMillis();
        if (this.waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
            currentMilliseconds = timeService.getCurrentMillis();
        }

        if (this.lastMilliseconds == currentMilliseconds) {
            if (0L == (this.sequence = this.sequence + 1L & 4095L)) {
                currentMilliseconds = this.waitUntilNextTime(currentMilliseconds);
            }
        } else {
            this.vibrateSequenceOffset();
            this.sequence = (long)this.sequenceOffset;
        }

        this.lastMilliseconds = currentMilliseconds;
        return currentMilliseconds - EPOCH << 22 | this.getWorkerId() << 12 | this.sequence;
    }

六、業務代碼

  使用分佈式的主鍵ID生成器,需要給不同的表注入不同的ID生成器,在config包下加一個KeyIdConfig類,如下:

  (為了保持時鐘的統一,可以專門找一台機器作為時鐘服務,然後給所有主鍵生成器配置統一的時鐘服務。下圖中未配置,如需配置,直接調用setTimeService方法即可)

@Configuration
public class KeyIdConfig {
    @Bean("userKeyGenerator")
    public SnowflakeShardingKeyGenerator userKeyGenerator() {
        return new SnowflakeShardingKeyGenerator();
    }

    @Bean("orderKeyGenerator")
    public SnowflakeShardingKeyGenerator orderKeyGenerator() {
        return new SnowflakeShardingKeyGenerator();
    }
}

  其他業務代碼,整體如下:

package com.example.shardingjdbc.entity;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
public class User implements Serializable {
    private Long id;
    private String name;
    private String phone;
    private String email;
    private String password;
    private Integer cityId;
    private Date createTime;
    private Integer sex;
}

User.java

package com.example.shardingjdbc.mapper;

import com.example.shardingjdbc.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

public interface UserMapper {
    /**
     * 保存
     */
    void save(User user);

    /**
     * 查詢
     * @param id
     * @return
     */
    User get(Long id);
}

UserMapper.java

<?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.example.shardingjdbc.mapper.UserMapper">
    <resultMap id="resultMap" type="com.example.shardingjdbc.entity.User">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="phone" property="phone"  />
        <result column="email" property="email"  />
        <result column="password" property="password"  />
        <result column="city_id" property="cityId"  />
        <result column="create_time" property="createTime"  />
        <result column="sex" property="sex"  />
    </resultMap>

    <insert id="save">
        insert into t_user (id, name, phone, email, password, city_id, create_time, sex)
        values (#{id}, #{name}, #{phone}, #{email}, #{password}, #{cityId}, #{createTime}, #{sex})
    </insert>

    <select id="get" resultMap="resultMap">
        select *
        from t_user
        where id = #{id}
    </select>
</mapper>

UserMapper.xml

 1 package com.example.shardingjdbc.controller;
 2 
 3 import com.example.shardingjdbc.entity.User;
 4 import com.example.shardingjdbc.mapper.UserMapper;
 5 import org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator;
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.stereotype.Controller;
 8 import org.springframework.web.bind.annotation.PathVariable;
 9 import org.springframework.web.bind.annotation.RequestMapping;
10 import org.springframework.web.bind.annotation.ResponseBody;
11 
12 import javax.annotation.Resource;
13 import java.util.Date;
14 
15 @Controller
16 public class UserController {
17     @Autowired
18     private UserMapper userMapper;
19 
20     @Resource
21     SnowflakeShardingKeyGenerator userKeyGenerator;
22 
23     @RequestMapping("/user/save")
24     @ResponseBody
25     public String save() {
26         for (int i = 0; i < 50; i++) {
27             Long id = (Long)userKeyGenerator.generateKey();
28             User user = new User();
29             user.setId(id);
30             user.setName("test" + i);
31             user.setCityId(i);
32             user.setCreateTime(new Date());
33             user.setSex(i % 2 == 0 ? 1 : 2);
34             user.setPhone("11111111" + i);
35             user.setEmail("xxxxx");
36             user.setCreateTime(new Date());
37             user.setPassword("eeeeeeeeeeee");
38             userMapper.save(user);
39         }
40 
41         return "success";
42     }
43 
44     @RequestMapping("/user/get/{id}")
45     @ResponseBody
46     public User get(@PathVariable Long id) {
47         User user = userMapper.get(id);
48         return user;
49     }
50 }

UserController.java

 1 CREATE TABLE `t_user` (
 2   `id` bigint(20) NOT NULL,
 3   `name` varchar(64) DEFAULT NULL COMMENT '名稱',
 4   `city_id` int(12) DEFAULT NULL COMMENT '城市',
 5   `sex` tinyint(1) DEFAULT NULL COMMENT '性別',
 6   `phone` varchar(32) DEFAULT NULL COMMENT '電話',
 7   `email` varchar(32) DEFAULT NULL COMMENT '郵箱',
 8   `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間',
 9   `password` varchar(32) DEFAULT NULL COMMENT '密碼',
10   PRIMARY KEY (`id`)
11 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

t_user.sql

  啟動類如下:

 1 package com.example.shardingjdbc;
 2 
 3 import org.mybatis.spring.annotation.MapperScan;
 4 import org.springframework.boot.SpringApplication;
 5 import org.springframework.boot.autoconfigure.SpringBootApplication;
 6 
 7 @MapperScan("com.example.shardingjdbc.mapper")
 8 @SpringBootApplication
 9 public class ShardingjdbcApplication {
10     public static void main(String[] args) {
11         SpringApplication.run(ShardingjdbcApplication.class, args);
12     }
13 }

ShardingjdbcApplication .java

  注意,這裏我在啟動類上加了 @MapperScan 註解。可能是因為引用依賴的問題,.properties 配置的 mybatis 包掃描目錄不管用了,後面有時間再研究。

七、其他

  除了基本的分庫分表規則以外,還有一些其他的配置,比如綁定表。這裏先不詳細解釋了,舉個簡單的例子:

  現在有 order, order_detail兩張表,1 : 1的關係。

  在配置的時候,應該將相同 order_id 的 order 記錄 和 order_detail 記錄 映射到相同尾號的表中,方便連接查詢。

  比如 id % 2 = 1的,都插入到  order0, order_detail0 中。

  如果配置了綁定關係,那麼查找 id = 1 的記錄,只會產生一次查詢 select * from order0 as o join order_detail0 as d  on o.order_id = d.order_id where o.oder_id = 1。

  否則會產生笛卡兒積查詢, 

    select * from order0 as o join order_detail0 as d  on o.order_id = d.order_id where o.order_id = 1

    select * from order0 as o join order_detail1 as d  on o.order_id = d.order_id where o.order_id = 1

    select * from order1 as o join order_detail0 as d  on o.order_id = d.order_id where o.order_id = 1

    select * from order1 as o join order_detail1 as d  on o.order_id = d.order_id where o.order_id = 1

八、總結

  項目啟動前,先創建數據庫 test0, test1, 然後分別建表 t_user0, t_user1。 可以全部在同一台機器。

  項目啟動后,訪問 http://localhost:8080/user/save, id 是 偶數的都插入到了 test0 庫的 t_user0 表中, 奇數的都插入到了 test1 庫中的 t_user1 表中。

  druid 的後台監控頁面地址: http://localhost:8080/druid/。

  項目啟動后,sharding日誌會將配置已 yml 格式的形式打印出來,也可以省去 java 配置,將其優化到 .yml 配置文件中去,如下圖:

  

  本文原文地址:https://www.cnblogs.com/lyosaki88/p/springboot_shardingjdbc_druid_mybatis.html

  源碼下載地址:https://474b.com/file/14960372-448059323

  作者QQ: 116269651

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Java規則引擎 Easy Rules

1.  Easy Rules 概述

Easy Rules是一個Java規則引擎,靈感來自一篇名為《Should I use a Rules Engine?》的文章 

規則引擎就是提供一種可選的計算模型。與通常的命令式模型(由帶有條件和循環的命令依次組成)不同,規則引擎基於生產規則系統。這是一組生產規則,每條規則都有一個條件(condition)和一個動作(action)———— 簡單地說,可以將其看作是一組if-then語句。

精妙之處在於規則可以按任何順序編寫,引擎會決定何時使用對順序有意義的任何方式來計算它們。考慮它的一個好方法是系統運行所有規則,選擇條件成立的規則,然後執行相應的操作。這樣做的好處是,很多問題都很自然地符合這個模型:

if car.owner.hasCellPhone then premium += 100;
if car.model.theftRating > 4 then premium += 200;
if car.owner.livesInDodgyArea && car.model.theftRating > 2 then premium += 300;

規則引擎是一種工具,它使得這種計算模型編程變得更容易。它可能是一個完整的開發環境,或者一個可以在傳統平台上工作的框架。生產規則計算模型最適合僅解決一部分計算問題,因此規則引擎可以更好地嵌入到較大的系統中。

你可以自己構建一個簡單的規則引擎。你所需要做的就是創建一組帶有條件和動作的對象,將它們存儲在一個集合中,然後遍歷它們以評估條件並執行這些動作。 

Easy Rules它提供Rule抽象以創建具有條件和動作的規則,並提供RuleEngine API,該API通過一組規則運行以評估條件並執行動作。 

Easy Rules簡單易用,只需兩步:

首先,定義規則,方式有很多種

方式一:註解

@Rule(name = "weather rule", description = "if it rains then take an umbrella")
public class WeatherRule {

    @Condition
    public boolean itRains(@Fact("rain") boolean rain) {
        return rain;
    }
    
    @Action
    public void takeAnUmbrella() {
        System.out.println("It rains, take an umbrella!");
    }
}

方式二:鏈式編程

Rule weatherRule = new RuleBuilder()
        .name("weather rule")
        .description("if it rains then take an umbrella")
        .when(facts -> facts.get("rain").equals(true))
        .then(facts -> System.out.println("It rains, take an umbrella!"))
        .build();

方式三:表達式

Rule weatherRule = new MVELRule()
        .name("weather rule")
        .description("if it rains then take an umbrella")
        .when("rain == true")
        .then("System.out.println(\"It rains, take an umbrella!\");");

方式四:yml配置文件

例如:weather-rule.yml

name: "weather rule"
description: "if it rains then take an umbrella"
condition: "rain == true"
actions:
  - "System.out.println(\"It rains, take an umbrella!\");"
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rule weatherRule = ruleFactory.createRule(new FileReader("weather-rule.yml"));

接下來,應用規則

public class Test {
    public static void main(String[] args) {
        // define facts
        Facts facts = new Facts();
        facts.put("rain", true);

        // define rules
        Rule weatherRule = ...
        Rules rules = new Rules();
        rules.register(weatherRule);

        // fire rules on known facts
        RulesEngine rulesEngine = new DefaultRulesEngine();
        rulesEngine.fire(rules, facts);
    }
}

入門案例:Hello Easy Rules

<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-rules-core</artifactId>
    <version>4.0.0</version>
</dependency>

通過骨架創建maven項目:

mvn archetype:generate \
    -DarchetypeGroupId=org.jeasy \
    -DarchetypeArtifactId=easy-rules-archetype \
    -DarchetypeVersion=4.0.0

默認給我們生成了一個HelloWorldRule規則,如下:

package com.cjs.example.rules;

import org.jeasy.rules.annotation.Action;
import org.jeasy.rules.annotation.Condition;
import org.jeasy.rules.annotation.Rule;

@Rule(name = "Hello World rule", description = "Always say hello world")
public class HelloWorldRule {

    @Condition
    public boolean when() {
        return true;
    }

    @Action
    public void then() throws Exception {
        System.out.println("hello world");
    }

}

2.  規則定義

2.1.  定義規則

大多數業務規則可以用以下定義表示:

  • Name : 一個命名空間下的唯一的規則名稱
  • Description : 規則的簡要描述
  • Priority : 相對於其他規則的優先級
  • Facts : 事實,可立即為要處理的數據
  • Conditions : 為了應用規則而必須滿足的一組條件
  • Actions : 當條件滿足時執行的一組動作 

Easy Rules為每個關鍵點提供了一個抽象來定義業務規則。

在Easy Rules中,Rule接口代表規則

public interface Rule {

    /**
    * This method encapsulates the rule's conditions.
    * @return true if the rule should be applied given the provided facts, false otherwise
    */
    boolean evaluate(Facts facts);

    /**
    * This method encapsulates the rule's actions.
    * @throws Exception if an error occurs during actions performing
    */
    void execute(Facts facts) throws Exception;

    //Getters and setters for rule name, description and priority omitted.

}

evaluate方法封裝了必須計算結果為TRUE才能觸發規則的條件。execute方法封裝了在滿足規則條件時應該執行的動作。條件和操作由Condition和Action接口表示。

定義規則有兩種方式:

  • 通過在POJO類上添加註解
  • 通過RuleBuilder API編程

可以在一個POJO類上添加@Rule註解,例如:

@Rule(name = "my rule", description = "my rule description", priority = 1)
public class MyRule {

    @Condition
    public boolean when(@Fact("fact") fact) {
        //my rule conditions
        return true;
    }

    @Action(order = 1)
    public void then(Facts facts) throws Exception {
        //my actions
    }

    @Action(order = 2)
    public void finally() throws Exception {
        //my final actions
    }

}

@Condition註解指定規則條件
@Fact註解指定參數
@Action註解指定規則執行的動作

RuleBuilder支持鏈式風格定義規則,例如:

Rule rule = new RuleBuilder()
                .name("myRule")
                .description("myRuleDescription")
                .priority(3)
                .when(condition)
                .then(action1)
                .then(action2)
                .build();

組合規則

CompositeRule由一組規則組成。這是一個典型地組合設計模式的實現。

組合規則是一個抽象概念,因為可以以不同方式觸發組合規則。

Easy Rules自帶三種CompositeRule實現:

  • UnitRuleGroup : 要麼應用所有規則,要麼不應用任何規則(AND邏輯)
  • ActivationRuleGroup : 它觸發第一個適用規則,並忽略組中的其他規則(XOR邏輯)
  • ConditionalRuleGroup : 如果具有最高優先級的規則計算結果為true,則觸發其餘規則

複合規則可以從基本規則創建並註冊為常規規則:

//Create a composite rule from two primitive rules
UnitRuleGroup myUnitRuleGroup = new UnitRuleGroup("myUnitRuleGroup", "unit of myRule1 and myRule2");
myUnitRuleGroup.addRule(myRule1);
myUnitRuleGroup.addRule(myRule2);

//Register the composite rule as a regular rule
Rules rules = new Rules();
rules.register(myUnitRuleGroup);

RulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.fire(rules, someFacts);

每個規則都有優先級。它代表觸發註冊規則的默認順序。默認情況下,較低的值表示較高的優先級。可以重寫compareTo方法以提供自定義優先級策略。

2.2.  定義事實

在Easy Rules中,Fact API代表事實

public class Fact<T> {
     private final String name;
     private final T value;
}

舉個栗子:

Fact<String> fact = new Fact("foo", "bar");
Facts facts = new Facts();
facts.add(fact);

或者,也可以用這樣簡寫形式

Facts facts = new Facts();
facts.put("foo", "bar");

用@Fact註解可以將Facts注入到condition和action方法中

@Rule
class WeatherRule {

    @Condition
    public boolean itRains(@Fact("rain") boolean rain) {
        return rain;
    }

    @Action
    public void takeAnUmbrella(Facts facts) {
        System.out.println("It rains, take an umbrella!");
        // can add/remove/modify facts
    }

}

2.3.  定義規則引擎

Easy Rules提供兩種RulesEngine接口實現:

  • DefaultRulesEngine : 根據規則的自然順序應用規則
  • InferenceRulesEngine : 持續對已知事實應用規則,直到不再適用任何規則為止 

創建規則引擎:

RulesEngine rulesEngine = new DefaultRulesEngine();

// or

RulesEngine rulesEngine = new InferenceRulesEngine();

然後,註冊規則

rulesEngine.fire(rules, facts);

規則引擎有一些可配置的參數,如下圖所示:

舉個栗子:

RulesEngineParameters parameters = new RulesEngineParameters()
    .rulePriorityThreshold(10)
    .skipOnFirstAppliedRule(true)
    .skipOnFirstFailedRule(true)
    .skipOnFirstNonTriggeredRule(true);

RulesEngine rulesEngine = new DefaultRulesEngine(parameters);

2.4. 定義規則監聽器

通過實現RuleListener接口

public interface RuleListener {

    /**
     * Triggered before the evaluation of a rule.
     *
     * @param rule being evaluated
     * @param facts known before evaluating the rule
     * @return true if the rule should be evaluated, false otherwise
     */
    default boolean beforeEvaluate(Rule rule, Facts facts) {
        return true;
    }

    /**
     * Triggered after the evaluation of a rule.
     *
     * @param rule that has been evaluated
     * @param facts known after evaluating the rule
     * @param evaluationResult true if the rule evaluated to true, false otherwise
     */
    default void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) { }

    /**
     * Triggered on condition evaluation error due to any runtime exception.
     *
     * @param rule that has been evaluated
     * @param facts known while evaluating the rule
     * @param exception that happened while attempting to evaluate the condition.
     */
    default void onEvaluationError(Rule rule, Facts facts, Exception exception) { }

    /**
     * Triggered before the execution of a rule.
     *
     * @param rule the current rule
     * @param facts known facts before executing the rule
     */
    default void beforeExecute(Rule rule, Facts facts) { }

    /**
     * Triggered after a rule has been executed successfully.
     *
     * @param rule the current rule
     * @param facts known facts after executing the rule
     */
    default void onSuccess(Rule rule, Facts facts) { }

    /**
     * Triggered after a rule has failed.
     *
     * @param rule the current rule
     * @param facts known facts after executing the rule
     * @param exception the exception thrown when attempting to execute the rule
     */
    default void onFailure(Rule rule, Facts facts, Exception exception) { }

}

3.  示例

<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.cjs.example</groupId>
    <artifactId>easy-rules-quickstart</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.jeasy</groupId>
            <artifactId>easy-rules-core</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.jeasy</groupId>
            <artifactId>easy-rules-support</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.jeasy</groupId>
            <artifactId>easy-rules-mvel</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>
</project>

4.  擴展

規則本質上是一個函數,如y=f(x1,x2,..,xn)

規則引擎就是為了解決業務代碼和業務規則分離的引擎,是一種嵌入在應用程序中的組件,實現了將業務決策從應用程序代碼中分離。

還有一種常見的方式是Java+Groovy來實現,Java內嵌Groovy腳本引擎進行業務規則剝離。

https://github.com/j-easy/easy-rules/wiki

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

不要小看這款常見的家用車!懂得買這款車的人都不簡單…

為什麼日產要推出這樣的一輛車,並且把重點放在舒適性上,其實和它的“大哥”有一定的聯繫。我們都十分熟悉的風神藍鳥,就是由第九代藍鳥U13衍生而來,為了迎合國內市場做了大量的修改,更加顯得豪華,雖然市場表現很好,但隨着時間的推移,時代的變遷,無論是技術層面還是還是設計層面,都已經被市場淘汰。

2006年,日產推出了一款介於天籟與藍鳥之間的全新中高級轎車,英文名為BLUEBIRD SYLpHY,當時的新車指導價為:14.38萬-21.48,主打2.0L車型,其實日產推出這款車型的目的明確,就是為了替代BLUEBIRD藍鳥。

外觀造型飽滿圓潤,鍍鉻裝飾的中網突顯檔次,前大燈的設計與風雅有相似之處,整車設計給人感覺大方得體,非常流暢,這也是軒逸經典款至今仍在出售的原因之一。

堪稱沙發級別的座椅是軒逸的殺手鐧,座椅寬大、厚實、飽滿,一坐進去軒逸的後排,就像是坐在家裡的沙發一樣,整個人陷入其中,坐墊非常軟,同時還有着非常寬裕的腿部空間,大家要知道,那時候可是2006年,能夠有這麼優秀的舒適性,是非常難得的。

為什麼日產要推出這樣的一輛車,並且把重點放在舒適性上,其實和它的“大哥”有一定的聯繫。我們都十分熟悉的風神藍鳥,就是由第九代藍鳥U13衍生而來,為了迎合國內市場做了大量的修改,更加顯得豪華,雖然市場表現很好,但隨着時間的推移,時代的變遷,無論是技術層面還是還是設計層面,都已經被市場淘汰。

日產需要一輛能夠霸佔家用轎車市場的車型,就這樣,軒逸應運而生,外觀大氣、乘坐舒適、空間寬裕,動力方面使用了MR20DE的2.0L發動機和第三代XTRONIC CVT無極變速箱,側重於追求低油耗和低噪音,這些所有的一切,都是為了舒適性,做出高級感。

最終日產的確憑藉著它在市場中站穩腳跟,聽到日產軒逸就會想起那舒適居家的溫馨感,其實我們可以看得出來,當初軒逸的成功是必然的,因為市場就是需要這樣的產品,同時我們也看到日產似乎對於“藍鳥”這個名字念念不忘。

日產通過LANNIA 藍鳥很全面的詮釋了V-motion家族式設計風格,告訴我們什麼才叫“驚艷”,由一開始的BLUEBIRD,到後來的BLUEBIRD SYLpHY,然後BLUEBIRD消失,剩下SYLpHY,最後LANNIA 藍鳥出現。

第一代藍鳥

“藍鳥”這兩個字包含着很多歷史,很多情懷,還有我們小時候的生活場景,它註定不會消失,這些歷史的沉澱,我們都應該好好記住。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

教授為你盤點去年3-30萬最受關注車型

3英寸液晶儀錶盤,看起來很有科技感。至於動力方面,第三代EA888發動機搭配7速雙離合變速箱,滿滿的德味,行駛品質依舊出色。在售價沒有大漲的前提下進行了這麼大的提升,全新邁騰總算沒有讓車迷失望。小型SUV:上汽通用五菱-寶駿510關注點:和寶駿310一樣有了前面310的成功經驗,那麼上汽通用五菱做起寶駿510這款的時候就更有信心了。

不知不覺已經到2017年了,像這種文藝肌肉男,每年到這個時候總是難免感嘆時間流逝之快。正當我們停下來傷春悲秋的時候,汽車圈卻依然像幻變的風雲一樣,不斷髮展。在逝去的2016年,我們聽到最多的是哪些車的名字?是加價提車一車難求的思域?還是被吐槽萬遍依然銷量爆炸的H6?今天就盤點了這些2016年你們一定關注過的幾款車,看看誰是今年的“大明星”。

小型轎車:上汽通用五菱-寶駿310

關注點:配置齊全、外觀帥氣、價格超低

從今年4月份北京車展的發布之後,時尚靚麗的造型、懸浮式的中控屏、以及比大眾polo還要大的車身尺寸,讓寶駿310受到不少年輕人的關注,再加上傳聞3萬多的起售價,那更是把寶駿310推到了關注的新高度。

不出所料,在上市之後,憑藉3.68-4.98萬元的超低售價,迅速吸引了不少買家,在11月份的銷量已經突破了1.5W輛。其實寶駿310的成功也是不無道理,細想一下,同尺寸的競爭對手就是大眾polo、本田飛度等車,但它們對比310 卻毫無價格優勢,而同價格的自主品牌,卻只有比亞迪F0或者奇瑞QQ這樣的產品,產品力根本就不能和310相提並論。所以寶駿310 的成功,是離不開寶駿對消費者需求的正確解讀的。

緊湊型轎車:東風本田-思域

關注點:高性能、大空間、運動化的外觀

認為思域確實是年度最受關注的緊湊型車,1.5T的發動機,爆發出177ps的馬力,單是這一點,已經讓很多對手都望塵莫及了,更何況它有溜背造型的車尾以及后多連桿獨立懸挂,試問在十五萬級別的車裡面,論產品力,誰能跟思域抗衡?

然而,加價提車卻成了車迷心裏面永恆的痛,小則三四千,多則上萬,從思域上市至今,該車在各大經銷商就一直處於加價提車並且一車難求的狀態。而這一點也是制約着思域銷量的障礙,雖然月銷量才剛剛破萬,但是依然沒有降低廣大車迷對思域的關注,希望有天思域能夠不再加價提車,以回報車迷們的熱情吧。

中型轎車:一汽大眾-邁騰

關注點:新平台、第三代EA888發動機

良久以來,帕薩特車系都一直是國內平民商務轎車的典範,親民的價格,大氣的外觀,舒適的駕乘體驗,想起買中型車,不買日系的話,基本就是往帕薩特/邁騰這方向考慮的了。然而,上一代的邁騰採用的是國外帕薩特B6的平台,而帕薩特B8早在2015年初就已經發布,所以國內的大眾車迷一直都在翹首以盼換代邁騰的到來,因此全新邁騰自然受到不少關注。

實際上,該車的實力是值得肯定的,首先在外觀上改變了圓頭圓腦的造型,採用了扁平硬朗的設計,既時尚又穩重。同時內在的變化也不少,如360度全景攝像、以及跟奧迪類似的12.3英寸液晶儀錶盤,看起來很有科技感。至於動力方面,第三代EA888發動機搭配7速雙離合變速箱,滿滿的德味,行駛品質依舊出色。在售價沒有大漲的前提下進行了這麼大的提升,全新邁騰總算沒有讓車迷失望。

小型SUV:上汽通用五菱-寶駿510

關注點:和寶駿310一樣

有了前面310的成功經驗,那麼上汽通用五菱做起寶駿510這款的時候就更有信心了。雖然該車尚未上市,但是在廣州車展發布以來,其漂亮的外觀以及精緻的內飾對於本來購車預算不大的消費者來說的確是很大的誘惑,可謂是國內車市在今年的一顆重磅炸彈。

從動力系統上看,採用了通用的L2B發動機,排量為1.5L,前期推出的三款車型均搭載手動變速箱,期待後續有自動擋或者AMT車型的加入,畢竟寶駿的AMT其實做得不錯。說發動機的編號大家可能覺得陌生,但說起凱越和英朗這兩款別克車,大家就很有親切感了,沒錯,這個L2B發動機同樣搭載在凱越和英朗身上,在燃油經濟性和可靠性方面的現都很出色,相信這個發動機會在寶駿510身上繼續發光發熱。

緊湊型SUV:哈弗H6

關注點:銷量王、可選車型多

油耗高、動力平庸、底盤也是逆向研發CRV得出來的H6,雖然被不少人吐槽,但是他卻依然熱銷,就在剛過去的11月,月銷量突破7萬輛,這讓古今中外多少車企都望塵莫及啊。

H6的車型確實很多,想購買這輛車的朋友可能一時間真的不知道該怎麼選,其實可以用排除法選,首先在售的有1.5T汽油、2.4L汽油和2.0T柴油三種發動機,然後在根據自己的需要選擇手動或者自動變速箱,再來就是根據可以接受的價錢選擇一款車型即可。實際上,想長安CS75、吉利博越和傳祺GS4這些車型在看來都比哈弗H6要好,值得大家考慮。

中型SUV:眾泰SR9

關注點:十萬元就能買到保時捷……的外觀

眾泰SR9一經推出就引起了熱議,但實際上其實都是調侃多於肯定的,甚至有人說這輩子能不能開上蘭博基尼就要看眾泰了。正當我們以為這輛車會被輿論推上風口浪尖的時候,11月11日上市的SR9當月就賣出了3286輛,不得不承認,眾泰是有他自己一套生存法則的。

撇開外觀來談談這輛車的話,首先內飾上面來看,布局是明顯抄保時捷的了,特別是變速箱周圍密密麻麻的按鍵,遠看像極了保時捷,但是近看之後,粗糙的做工就會展露無遺。動力方面,採用三菱的4G63T發動機搭配手動變速箱或者雙離合變速箱,動力匹配的火候明顯未到家,中低速行駛的平順性乏善可陳,動力調校的功底多年來都沒有明顯的提高,難以形成自身的核心競爭力。但無論怎樣,這輛車在2016年的尾巴的確掀起了一個不小的“十萬買保時捷”風波。

總結

在我們關注的這些車裡面,要麼就是在性能方面有建樹,如新思域、新邁騰,要麼就是自帶賣點,如銷量王H6、十萬保時捷SR9。但相信,想要走得更遠,核心技術始終是最強有力的支撐,希望2017年將會有更多優秀的自主品牌汽車出現吧。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

從7萬到25萬,這些車為了衝量竟然最高降3.7萬!

6L自動擋版本綜合油耗在5毛錢以內。別克君威指導價:17。89-27。99萬降價幅度:3。7萬推薦車型:2015款1。6T領先技術型君威作為別克的一款中高檔轎車,定位介於英朗和君越之間,屬於一款20萬級別的中型車,在乘坐空間,配置,做工用料上,都比代步車上了一個層次,而這款車的優惠幅度也是同級別車型中最大的,最高達到了3。

許多經銷商們為了年底衝量,在7萬-25萬這個區間的車型,許多都放出了最大降價的優惠,從小型車,緊湊型車再到中級車,SUV,羅列了幾款降價幅度很大的好品質車型,供大家參考參考。

雪鐵龍愛麗舍

指導價:8.38-12.08萬元

降價幅度:1.5萬元

車型:2016款1.6L自動豪華型

愛麗舍是法國雪鐵龍推出的一款家用轎車,自2002年誕生以來,用戶好口碑一直不斷積累着,為了針對中國人的審美觀,新款愛麗舍擁有更加動感的車身線條和造型設計,廠家似乎正通過這款車和消費者說:其實我們要造好造型還是不差於別人的。

愛麗舍內飾的整體風格簡潔,清新,高雅,且空調出風口設計得很大,製造出風最好的效率,每個區域分隔清晰,操作非常方便,車型上推薦買自動豪華版,因為配置豐富帶ESp,真皮座椅,倒車影像,安全側氣囊等,優惠完車身價大概在10萬元左右。動力則全系搭載1.6L自然吸氣發動機,匹配5擋手動和6擋手自一體變速器。

車主評價:動力充足,配置夠用,外形沉穩時尚,底盤舒服有韌性,剎車感覺非常線性。

大眾高爾夫

指導價:12.19-23.99萬

降價幅度:2.8萬

推薦車型:2016款 1.6L自動時尚型

從1974年開始,大眾高爾夫就誕生了,直至今年,已經在全球市場推出七代車型,也是大眾最暢銷的車型之一,在中國,兩廂的高爾夫非常受人們歡迎,比起六代,七代高爾夫在外觀造型上更加犀利運動。

內飾風格同樣也是大眾風格,簡約實用,對於家用代步來說,車型版本選擇1.6L自然吸氣排量就夠了,如果不需要天窗的可以選自動時尚型,匹配6擋手自一體,配置安全側氣囊,ESp車身穩定控制,上坡輔助,自動駐車等等,而如果是喜歡追尋動力的年輕人,可以考慮1.4T渦輪增壓發動機版本,匹配7擋雙離合變速器,更有操控和駕駛的樂趣。

車主評價:外觀好看,舒適度滿意,轉向精準,隔音做得蠻好,油耗低,1.6L自動擋版本綜合油耗在5毛錢以內。

別克君威

指導價:17.89-27.99萬

降價幅度:3.7萬

推薦車型:2015款1.6T領先技術型

君威作為別克的一款中高檔轎車,定位介於英朗和君越之間,屬於一款20萬級別的中型車,在乘坐空間,配置,做工用料上,都比代步車上了一個層次,而這款車的優惠幅度也是同級別車型中最大的,最高達到了3.7萬元。

推薦的這款1.6T領先技術型,匹配6速手自一體變速箱,算是各方面相對均衡的一個版本,動力夠用,配置齊全,帶有胎壓監測,ESp,上坡輔助,電動天窗,自動空調等等,再加上美系車蠻好的隔音效果,優質的底盤響應,用料十分厚道,總有一種讓人們覺得這個價格買得值的感覺。

車主口碑:隔音好,外觀公認好看,車身穩重,安全感強烈。

現代ix35

指導價:14.98-22.28萬

降價幅度:2.8萬元

推薦車型:2015款 2.0L自動兩驅智能型國IV

現代ix35的外形設計由德國法蘭克福研發中心完成,帶有明顯歐式風格,時尚前衛,動感強韌,韓系車都喜歡將錢花在大家都看得到的地方,同樣的價格,假如這台車外形更好看的話,中國老百姓就喜歡,就覺得值,同樣車內空間的拓展上做得非常好,後備箱也一樣寬大。

內飾方面,營造了一種溫馨家用的風格,做工細膩手感順滑,並採用大面積的銀色裝飾,方向盤採用真皮縫製,配置非常貼心,配備有安全側氣囊,無鑰匙進入與啟動,ESp,上坡輔助,電動天窗等等,同等價位里這個配置挺高分的了,搭載有2.0L和2.4L自然吸氣發動機,匹配6速手動和6速手自一體變速器。

車主評價:同等級對比后,內部空間大,配置高,性價比強。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

汽車的頭枕為什麼怎麼調都不舒服?

這一后一前兩個猛烈彎折動作,足以導致頸椎折斷。這是重傷或者致死的主要原因。正因為被追尾的主要死/傷原因是揮鞭效應,所以汽車製造商就推出了相關的硬件安全設計。最常見的現行設計就是主動頭枕,防止頸部受傷。主動安全頭枕就是在碰撞的瞬間讓頭部得到支撐,也就是把頭枕往前移動。

很多朋友買來新車以後會裝很多配件,比如座椅套,方向盤套。

而細心的朋友會發現,不管頭枕怎麼調,手動調節還是電動調節都不能調到一個很舒服的位置,總覺得頭靠上去脖子部分空空的,所以很多朋友一般都會額外加裝一個托住脖子的軟墊。難道汽車廠商從來都沒有發現這個問題嗎?

其實汽車頭枕的全稱是汽車座椅安全頭枕,在早些年的時候汽車都沒有頭枕這個東西。

他的出現主要是為提高汽車安全性而設置的一種輔助裝置。

因為在被追尾時, 安全帶是不會發揮作用的.追尾帶來的最大傷害的原因是揮鞭效應, 撞擊一瞬, 被撞者頭部猛烈後仰, 頸椎在巨大的頭部加速作用下,嚴重的頸椎會當場折斷,頭部後仰,在巨大慣性作用下,然後頭部猛烈想前運動,頸椎前折. 這一后一前兩個猛烈彎折動作,足以導致頸椎折斷。

這是重傷或者致死的主要原因.正因為被追尾的主要死/傷原因是揮鞭效應,所以汽車製造商就推出了相關的硬件安全設計。最常見的現行設計就是主動頭枕,防止頸部受傷。

主動安全頭枕就是在碰撞的瞬間讓頭部得到支撐,也就是把頭枕往前移動。其机械結構也不複雜,只要在椅背內設計了一個連桿,發生來自後部的撞擊時,身體重量擠壓在這個連桿上,在聯動機構的作用下,頭枕向前移動,最大限度地防止頭部猛烈後仰,從而保護人員頭部和頸部的安全。

雖然現在也有些廠商將頭枕設計得很舒服,但是汽車的頭枕在絕大多數情況下都只是一項安全配置。

最後祝所有TV的粉絲元旦快樂,在新的一年能開超跑。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

我買中國品牌的車不只是因為便宜!

觀致耗資1600萬與超跑製造商柯尼塞克共同研發的無凸輪軸發動機(代號QamFree),同等排量下,要比傳統的發動機的功率大幅提升45%,最大扭矩大幅提高47%。假如測試通過,進行量產,將成為未來發動機技術的主要發展方向,彷彿為全世界指明了一條比混合動力更為通暢的道路。

自主品牌亮瞎眼睛的地方還多着!

當然了,自主品牌能夠亮瞎眼的地方遠不止這兩點。力壓奔馳、寶馬,在歐洲新車安全評鑒協會Euro-NCAp所進行的2013年最佳碰撞車型評選(小型家用車類)一舉奪得第一名的觀致3,讓一眾歪果仁瞠目結舌。觀致耗資1600萬與超跑製造商柯尼塞克共同研發的無凸輪軸發動機(代號QamFree),同等排量下,要比傳統的發動機的功率大幅提升45%,最大扭矩大幅提高47%。假如測試通過,進行量產,將成為未來發動機技術的主要發展方向,彷彿為全世界指明了一條比混合動力更為通暢的道路。吧唧這麼多,我想說的其實很簡單:自主品牌比你想象中牛逼多了!趕緊去4S店下訂吧!

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

Logstash下字段以及嵌套Json字段類型轉換

 

前言

從filebeat傳輸到Logstash的數據,某個字段需要由string類型裝換成float類型。但是不管怎麼改logstash的配置文件都不生效,其實官方文檔都有,但是具體細節方面的東西就得自己不斷的實踐驗證最後達到自己想要的目標了。整整一天,都在弄這一個,中間實在想放棄了。但是就如張靚穎的“終於等到你,還好沒放棄”,最後在某一篇博文得到了啟發,才解決。

 

這裏類型轉換分兩個類型:

1)字段是單純的字段,也就是直接在_source下的

2)字段是在json里的,在_source下還有嵌套一層json里的字段

 

一、單一字段

可以從下面的圖中看出,字段就在頂層機構_source下,這種情況下的Logstash配置文件設置如下:

filter {
     mutate {
     convert => { "request_time" => "float" }
     convert => { "upstream_response_time" => "float" }
     }
}

 

 

 

 

二、嵌套Json下的字段

如果需要轉換的字段是在非頂級結構下,是在一個JSON里,因為在filebeat做decode的時候指定了,如我需要轉換的字段是在jsonn的json字段里:

processors:
 - decode_json_fields:
    fields: ["message"]    #要進行解析的字段
    process_array: false   #數組是否解碼,默認值:false
    max_depth: 3           #解碼深度,默認值:1
    target: "jsonn"          #json內容解析到指定的字段,如果為空(“”),則解析到頂級結構下
    overwrite_keys: false  #如果解析出的json結構中某個字段在原始的event(在filebeat中傳輸的一條數據為一個event)中也存在,是否覆蓋

 

 

 

這種情況下的Logstash配置文件設置如下:

filter {
    mutate {
      convert => { "[jsonn][request_time]" => "float" }
      convert => { "[jsonn][upstream_response_time]" => "float" }
   }
}

 

注意:

[jsonn][request_time] 不是[jsonn].[request_time],也不是jsonn.request_time沒有點.

 

受啟發的鏈接:

https://stackoverflow.com/questions/30369148/logstash-remove-deep-field-from-json-file

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

CSS中的float和margin的混合使用

在最近的學習中,在GitHub上找了一些布局練習,我發現了我自己對布局超級不熟悉(很難受)。

在以前的學習CSS過程中,感覺使用CSS就記住各個屬性的屬性值以及作用就OK了,但是實際上呢?呵呵一笑。不說了,太傷心了,進入正題吧!

最近我使用float和margin布局,加深了我對這兩個一起使用的理解。(新生可以看一下,大神請忽略

float屬性

float: left | right | none | inherit

當然最常用的還是前面兩個:向左浮動向右浮動

浮動最主要的特點:脫標

脫離標準流,處於更加高級的層面,影響父元素和後面元素的布局,這裏就不具體介紹了。

margin屬性

這裏主要講margin-leftmargin-right

margin-left: 設置元素的左外邊距。
margin-right: 設置元素的右外邊距。

總的來說,這個兩個屬性的字面理解還是很容易的,但是越簡單的東西越不要小看。

重點

以下代碼:

html:

<div class="box">
    <div class="zi_box1">1</div>
    <div class="zi_box2">2</div>
    <div class="zi_box3">3</div>
    <div class="clear"></div>
</div>

CSS:

.box {
            background-color: #555555;
            width: 600px;
            height: 200px;
        }
        .zi_box1 {
            float: left;
            background-color: #c23232;
            width: 200px;
            height: 100px;
        }
        .zi_box2 {
            float: left;
            background-color: chartreuse;
            width: 200px;
            height: 100px;
        }

        .zi_box3 {
            float: left;
            background-color: blue;
            width: 200px;
            height: 100px;
        }
        .clear {
            clear: both;
        }

最後實現的效果圖:

三個子盒子充滿父盒子,因為但他們寬度可以在父盒子裏面撐開。

如果父盒子撐不開呢?

加大一個子盒子的寬度,序號為3的盒子

zi_box3 {
    width: 300px;
}

效果圖如下:

那麼第三個盒子則會另外起一行。

結合margin使用時

在第一代碼的基礎上,增加一個margin值

zi_box1 {
    margin-left: 20px;
}

這時候,由於三個盒子的寬度加上margin值大於父盒子的寬度,所以盒子3就會另起一行

反之,給盒子3設置一個外邊距的值,盒子1和盒子2不設置外邊距,是不是盒子3也會另外起一行呢?答案是肯定的,因為他們的寬度已經超過父盒子的值了。

實現三列布局

在不改變DOM的順序的情況下,使盒子3盒子1盒子2的順序呢?是不是就可以充分使用margin這個屬性了。最開始白痴的我(很少練習布局吧,大神就不要噴我了,我只是個菜鳥)

白痴代碼

.zi_box1 {
     margin-left: 200px;       
}
.zi_box2 {
     margin-left: 200px;       
}

.zi_box3 {
     margin0left: -400px;
}
//這裏很天真的想法,以為每個元素是單獨行動

這樣寫的效果圖:

我當時就傻了,這是什麼玩意。

但是在最後的摸索中,我知道原因了,最最最最重要的就是DOM的執行順序

造成這樣的原因就是:盒子1先解析,margin-left: 200px,那麼這樣盒子3也就去了第二行; 再盒子2解析,margin-left:200px,那麼盒子2也去了第二行,因為第一行已經有600px這麼寬的長度了。最後解析盒子3,margin-left:-400px,盒子向前移動400px,不就造成了這樣的效果圖嘛。

這樣想的,就是指考慮片面的,而不是全局的

實現三列布局的最終代碼

.zi_box1 {
     margin-left: 200px;       
}
.zi_box2 {
     margin-left: 0px;      
}

.zi_box3 {
     margin0left: -600px;
}

效果圖

可以簡單的這樣理解

盒子1向右移動200px,那麼盒子2和盒子3也會向右移動200px,具體的效果圖如下

那麼盒子3移動到前面去,是不是需要600px的距離啊(是不是很容易懂,嘻嘻),當然這隻是我的片面理解,也不完全是對的。
在這種思維模式下,還要注意一點:當超出的部分盒子還是會遵守float的規則的。

那麼float: right和margin-right是一樣的道理。

這是我的第一篇博客,寫的太菜,不要笑我喲。

喜歡我的話,點個關注吧!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

學習ASP.NET Core(10)-全局日誌與xUnit系統測試

上一篇我們介紹了數據塑形,HATEOAS和內容協商,並在制器方法中完成了對應功能的添加;本章我們將介紹日誌和測試相關的概念,並添加對應的功能

一、全局日誌

在第一章介紹項目結構時,有提到.NET Core啟動時默認加載了日誌服務,且在appsetting.json文件配置了一些日誌的設置,根據設置的日誌等級的不同可以進行不同級別的信息的显示,但它無法做到輸出固定格式的log信息至本地磁盤或是數據庫,所以需要我們自己手動實現,而我們可以藉助日誌框架實現。

ps:在第7章節中我們記錄的是數據處理層方法調用的日誌信息,這裏記錄的則是ASP.NET Core WebAPI層級的日誌信息,兩者有所差異

1、引入日誌框架

.NET程序中常用的日誌框架有log4net,serilog 和Nlog,這裏我們使用Serilog來實現相關功能,在BlogSystem.Core層使用NuGet安裝Serilog.AspNetCore,同時還需要搜索Serilog.Skins安裝希望支持的功能,這裏我們希望添加對文件和控制台的輸出,所以選擇安裝的是Serilog.Skins.File和Serilog.Skins.Console

需要注意的是Serilog是不受appsetting.json的日誌設置影響的,且它可以根據命名空間重寫記錄級別。還有一點需要注意的是需要手動對Serilog對象進行資源的釋放,否則在系統運行期間,無法打開日誌文件。

2、系統添加

在BlogSystem.Core項目中添加一個Logs文件夾,並在Program類中進行Serilog對象的添加和使用,如下:

3、全局添加

1、這個時候其實系統已經使用Serilog替換了系統自帶的log對象,如下圖,Serilog會根據相關信息進行高亮显示:

2、這個時候問題就來了,我們怎麼才能進行全局的添加呢,總不能一個方法一個方法的添加吧?還記得之前我們介紹AOP時提到的過濾器Filter嗎?ASP.NET Core中一共有五類過濾器,分別是:

  • 授權過濾器Authorization Filter:優先級最高,用於確定用戶是否獲得授權。如果請求未被授權,則授權過濾器會使管道短路;
  • 資源過濾器Resource Filter:授權后運行,會在Authorization之後,Model Binding之前執行,可以實現類似緩存的功能;
  • 方法過濾器Action Filter:在控制器的Action方法執行之前和之後被調用,可以更改傳遞給操作的參數或更改從操作返回的結果;
  • 異常過濾器Exception Filter:當Action方法執行過程中出現了未處理的異常,將會進入這個過濾器進行統一處理;
  • 結果過濾器Result Filter:執行操作結果之前和之後運行,僅在action方法成功執行后才運行;

過濾器的具體執行順序如下:

3、這裏我們可以藉助異常過濾器實現全局日誌功能的添加;在在BlogSystem.Core項目添加一個Filters文件夾,添加一個名為ExceptionFilter的類,繼承IExceptionFilter接口,這裡是參考老張的哲學的簡化版本,實現如下:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Serilog;
using System;

namespace BlogSystem.Core.Filters
{
    public class ExceptionsFilter : IExceptionFilter
    {
        private readonly ILogger<ExceptionsFilter> _logger;

        public ExceptionsFilter(ILogger<ExceptionsFilter> logger)
        {
            _logger = logger;
        }

        public void OnException(ExceptionContext context)
        {
            try
            {
                //錯誤信息
                var msg = context.Exception.Message;
                //錯誤堆棧信息
                var stackTraceMsg = context.Exception.StackTrace;
                //返回信息
                context.Result = new InternalServerErrorObjectResult(new { msg, stackTraceMsg });
                //記錄錯誤日誌
                _logger.LogError(WriteLog(context.Exception));
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
            finally
            {
                //記得釋放,否則運行時無法打開日誌文件
                Log.CloseAndFlush();
            }

        }

        //返回500錯誤
        public class InternalServerErrorObjectResult : ObjectResult
        {
            public InternalServerErrorObjectResult(object value) : base(value)
            {
                StatusCode = StatusCodes.Status500InternalServerError;
            }
        }

        //自定義格式內容
        public string WriteLog(Exception ex)
        {
            return $"【異常信息】:{ex.Message} \r\n 【異常類型】:{ex.GetType().Name} \r\n【堆棧調用】:{ex.StackTrace}";
        }
    }
}

4、在Startup類的ConfigureServices方法中進行異常處理過濾器的註冊,如下:

5、我們在控制器方法中拋出一個異常,分別查看效果如下,如果覺得信息太多,可調整日誌記錄級別:

二、系統測試

這裏我們從測試的類別出發,了解下測試相關的內容,並添加相關的測試(介紹內容大部分來自微軟官方文檔,為了更易理解,從個人習慣的角度進行了修改,如有形容不當之處,可在評論區指出)

1、測試說明及分類

1、自動測試是確保軟件應用程序按照作者期望執行操作的一種絕佳方式。軟件應用有多種類型的測試,包括單元測試、集成測試、Web測試、負載測試和其他測試。單元測試用於測試個人軟件的組件或方法,並不包括如數據庫、文件系統和網絡資源類的基礎結構測試。

當然我們可以使用編寫測試的最佳方法,如測試驅動開發(TDD)所指的先編寫單元測試,再編寫該單元測試要檢查的代碼,就好比先編寫書籍的大綱,再編寫書籍。其主要目的是為了幫助開發人員編寫更簡單,更具可讀性的高效代碼。兩者區別如下(來自Edison Zhou)

2、以深度(測試的細緻程度)和廣度(測試的覆蓋程度)區分, 測試分類如下(此處內容來自solenovex):

Unit Test 單元測試:它可以測試一個類或者一個類的某個功能,但其覆蓋程度較低;

Integration Test 集成測試:它的細緻程度沒有單元測試高,但是有較好的覆蓋程度,它可以測試功能的組合,以及像數據庫或文件系統這樣的外部資源;

Subcutaneous Test 皮下測試 :其作用區域為UI層的下一層,有較好的覆蓋程度,但是深度欠佳;

UI測試:直接從UI層進行測試,覆蓋程度很高,但是深度欠佳

3、在編寫單元測試時,盡量不要引入基礎結構依賴項,這些依賴項會降低測試速度,使測試更加脆弱,我們應當將其保留供集成測試使用。可以通過遵循显示依賴項原則和使用依賴項注入避免應用程序中的這些依賴項,還可以將單元測試保留在單獨的項目中與集成測試相分離,以確保單元測試項目沒有引用或依賴於基礎結構包。

總結下常用的單元測試和集成測試,單元測試會與外部資源隔離,以保證結果的一致性;而集成測試會依賴外部資源,且覆蓋面更廣。

2、測試的目的及特徵

1、為什麼需要測試?我們從以單元測試為例從4個方面進行說明:

  • 時間人力成本:進行功能測試時,通常涉及打開應用程序,執行一系列需要遵循的步驟來驗證預期的行為,這意味着測試人員需要了解這些步驟或聯繫熟悉該步驟的人來獲取結果。對於細微的更改或者是較大的更改,都需要重複上述過程,而單元測試只需要按一下按鈕即可運行,無需測試人員了解整個系統,測試結果也取決於測試運行程序而非測試人員。
  • 防止錯誤回歸:程序更改後有時會出現舊功能異常的問題,所以測試時不僅要測試新功能還要確保舊功能的正常運行。而單元測試可以確保在更改一行代碼后重新運行整套測試,確保新代碼不會破壞現有的功能。
  • 可執行性:在給定某個輸入的情況下,特定方法的作用或行為可能不會很明顯。比如,輸入或傳遞空白字符串、null后,該方法會有怎樣的行為?而當我們使用一套命名正確的單元測試,並清楚的解釋給定的輸入和預期輸出,那麼它將可以驗證其有效性。
  • 減少代碼耦合:當代碼緊密耦合時,會難以進行單元測試,所以以創建單元測試為目的時,會在一定程度上要求我們注意代碼的解耦

2、優質的測試需要符合哪些特徵,同樣以單元測試為例:

  • 快速:成熟的項目會進行數千次的單元測試,所以應當花費非常少的時間來運行單元測試,一般來說在幾毫秒
  • 獨立:單元測試應當是獨立的,可以單獨運行,不依賴文件系統或數據庫等外部因素
  • 可重複:單元測試的結果應當保持一致,即運行期間不進行更改,返回的結果應該相同
  • 自檢查:測試應當在沒有人工交互的情況下,自動檢測是否通過
  • 及時:編寫單元測試不應該花費過多的時間,如果花費時間較長,應當考慮另外一種更易測試的設計

在具體的執行時,我們應當遵循一些最佳實踐規則,具體請參考微軟官方文檔單元測試最佳做法

3、xUnit框架介紹

常用的單元測試框架有MSTestxUnitNUnit,這裏我們以xUnit為例進行相關的說明

3.1、測試操作

首先我們要明確如何編寫測試代碼,一般來說,測試分為三個主要操作:

  • Arrange:意為安排或準備,這裏可以根據需求進行對象的創建或相關的設置;
  • Act:意為操作,這裏可以執行獲取生產代碼返回的結果或者是設置屬性;
  • Assert:意為斷言,這裏可以用來判斷某些項是否按預期進行,即測試通過還是失敗

3.2、Assert類型

Assert時通常會對不同類型的返回值進行判斷,而在xUnit中是支持多種返回值類型的,常用的類型如下:

boolean:針對方法返回值為bool的結果,可以判斷結果是true或false

string:針對方法返回值為string的結果,可以判斷結果是否相等,是否以某字符串開頭或結尾,是否包含某些字符,並支持正則表達式

數值型:針對方法返回值為數值的結果,可以判斷數值是否相等,數值是否在某個區間內,數值是否為null或非null

Collection:針對方法返回值為集合的結果,可以針對集合內所有元素或至少一個元素判斷其是否包含某某字符,兩個集合是否相等

ObjectType:針對方法返回值為某種類型的情況,可以判斷是否為預期的類型,一個類是否繼承於另一個類,兩個類是否為同一實例

Raised event:針對事件是否執行的情況,可以判斷方法內部是否執行了預期的事件

3.3、常用特性

在xUnit中還有一些常用的特性,可作用於方法或類,如下:

[Fact]:用來標註該方法為測試方法

[Trait(“Name”,”Value”)]:用來對測試方法進行分組,支持標註多個不同的組名

[Fact(Skip=”忽略說明…”)]:用來修飾需要忽略測試的方法

3.4 、性能相關

在測試時我們應當注意性能上的問題,針對一個對象供多個方法使用的情況,我們可以使用共享上下文

  • 針對一個對象供同一類中的多個方法使用時,可以將該對象提取出來,使用IClassFixture 對象將其注入到構造函數中
  • 針對一個對象供多個測試類使用的情況,可以使用ICollectionFixture 對象和[CollectionDefinition(“…”)]定義該對象

需要注意在使用IClassFixtureICollectionFixture對象時應當避免多個測試方法之間相互影響的情況

3.5、數據驅動測試

在進行測試方法時,通常我們會指定輸入值和輸出值,如希望多測試幾種情況,我們可以定義多個測試方法,但這顯然不是一個最佳的實現;在合理的情況下,我們可以將參數和數據分離,如何實現?

  • 方法一:使用[Theory]替換[Fact],將輸入輸出參數提取為方法參數,並使用多個[InlineData(“輸入參數”,”輸出參數)]來標註方法
  • 方法二:使用[Theory]替換[Fact],針對測試方法新增一個測試數據類,該類包含一個靜態屬性IEumerable<object[]>,將數據封裝為一個list后賦值給該屬性,並使用[MemberData(nameof(數據類的屬性),MemberType=typeof(數據類))]標註測試方法即可;
  • 方法三:使用外部數據如數據庫數據/Excel數據/txt數據等,其實現原理與方法二相同,只是多了一個數據獲取封裝為list的步驟;
  • 方法四:自定義一個Attribute,繼承自DataAttribute,實現其對應的方法,使用yield返回object類型的數組;使用時只需要在測試方法上方添加[Theory][自定義Attribute]即可

4、測試項目添加

4.1、添加測試項目

首先我們右鍵項目解決方案選擇添加一個項目,輸入選擇xUnit後進行添加,項目命名為BlogSystem.Core.Test,如下:

項目添加完成后我們需要添加對測試項目的引用,在解決方案中右擊依賴項選擇添加BlogSystem.Core;這裏我們預期對Controller進行測試,但後續有可能會添加其他項目的測試,所以我們建立一個Controller_Test文件夾保證項目結構相對清晰。

4.2、添加測試方法

在BlogSystem.Core.Test項目的Controller_Test文件夾下新建一個命名為UserController_Should的方法;在微軟的《單元測試的最佳做法》文檔中有提到,測試命名應該包括三個部分:①被測試方法的名稱②測試的方案③方案預期行為;實際使用時也可以對照測試的方法進行命名,這裏我們先不考慮最佳命名原則,僅對照測試方法進行命名,如下:

using Xunit;

namespace BlogSystem.Core.Test.Controller_Test
{
    public class UserController_Should
    {
        [Fact]
        public void Register_Test()
        {
            
        }
    }
}

4.3、方案選擇

1、在進行測試時,我們可以根據實際情況使用以下方案來進行測試:

  • 方案一:直接new一個Controller對象,調用其Action方法直接進行測試;適用於Controller沒有其他依賴項的情況;
  • 方案二:當有多個依賴項時,可以藉助工具來模擬實例化時的依賴項,如Moq就是一個很好的工具;當然這需要一定的學習成本;
  • 方案三:模擬Http請求的方式來調用API進行測試;NuGet中的Microsoft.AspNetCore.TestHost就支持這類情況;
  • 方案四:自定義方法實例化所有依賴項;將測試過程種需要用到的對象放到容器中並加載,其實現較為複雜;

這裏我們以測試UserController為例,其構造函數包含了接口服務實例和HttpContext對象實例,Action方法內部又有數據庫連接操作,從嚴格意義上來講測試這類方法已經脫離了單元測試的範疇,屬於集成測試,但這類測試一定程度上可以節省我們大量的重複勞動。這裏我們選擇方案三進行相關的測試。

2、如何使用TestHost對象?先來看看它的工作流程,首先它會創建一個IHostBuilder對象,並用它創建一個TestServer對象,TestServer對象可以創建HttpClient對象,該對象支持發送及響應請求,如下圖所示(來自solenovex):

在嘗試使用該對象的過程中我們會發現一個問題,創建IHostBuilder對象時需要指明類似Startup的配置項,因為這裡是測試環境,所以實際上會與BlogSystem.Core中的配置類StartUp存在一定的差異,因而這裏我們需要為測試新建立一個Startup配置類。

4.4、方法實現

1、我們在測試項目中添加名為TestServerFixture 的類和名為TestStartup的類,TestServerFixture 用來創建HttpClient對象並做一些準備工作,TestStartup類為配置類。然後使用Nuget安裝Microsoft.AspNetCore.TestHost;TestServerFixture 和TestStartup實現如下:

using Autofac.Extensions.DependencyInjection;
using BlogSystem.Core.Helpers;
using BlogSystem.Model;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using System;
using System.Net.Http;

namespace BlogSystem.Core.Test
{
    public static class TestServerFixture
    {
        public static IHostBuilder GetTestHost()
        {
            return Host.CreateDefaultBuilder()
           .UseServiceProviderFactory(new AutofacServiceProviderFactory())//使用autofac作為DI容器
           .ConfigureWebHostDefaults(webBuilder =>
           {
               webBuilder.UseTestServer()//建立TestServer——測試的關鍵
               .UseEnvironment("Development")
               .UseStartup<TestStartup>();
           });
        }

        //生成帶token的httpclient
        public static HttpClient GetTestClientWithToken(this IHost host)
        {
            var client = host.GetTestClient();
            client.DefaultRequestHeaders.Add("Authorization", $"Bearer {GenerateJwtToken()}");//把token加到Header中
            return client;
        }

        //生成JwtToken
        public static string GenerateJwtToken()
        {
            TokenModelJwt tokenModel = new TokenModelJwt { UserId = userData.Id, Level = userData.Level.ToString() };
            var token = JwtHelper.JwtEncrypt(tokenModel);
            return token;
        }

        //測試用戶的數據
        private static readonly User userData = new User
        {
            Account = "jordan",
            Id = new Guid("9CF2DAB5-B9DC-4910-98D8-CBB9D54E3D7B"),
            Level = Level.普通用戶
        };

    }
}
using Autofac;
using Autofac.Extras.DynamicProxy;
using BlogSystem.Common.Helpers;
using BlogSystem.Common.Helpers.SortHelper;
using BlogSystem.Core.AOP;
using BlogSystem.Core.Filters;
using BlogSystem.Core.Helpers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace BlogSystem.Core.Test
{
    public class TestStartup
    {
        private readonly IConfiguration _configuration;

        public TestStartup(IConfiguration configuration)
        {
            _configuration = GetConfig(null);
            //傳遞Configuration對象
            JwtHelper.GetConfiguration(_configuration);
        }

        public void ConfigureServices(IServiceCollection services)
        {
            //控制器服務註冊
            services.AddControllers(setup =>
            {
                setup.ReturnHttpNotAcceptable = true;//開啟不存在請求格式則返回406狀態碼的選項
                var jsonOutputFormatter = setup.OutputFormatters.OfType<SystemTextJsonOutputFormatter>()?.FirstOrDefault();//不為空則繼續執行
                jsonOutputFormatter?.SupportedMediaTypes.Add("application/vnd.company.hateoas+json");
                setup.Filters.Add(typeof(ExceptionsFilter));//添加異常過濾器
            }).AddXmlDataContractSerializerFormatters()//開啟輸出輸入支持XML格式

            //jwt授權服務註冊
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(x =>
            {
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true, //驗證密鑰
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["JwtTokenManagement:secret"])),

                    ValidateIssuer = true, //驗證發行人
                    ValidIssuer = _configuration["JwtTokenManagement:issuer"],

                    ValidateAudience = true, //驗證訂閱人
                    ValidAudience = _configuration["JwtTokenManagement:audience"],

                    RequireExpirationTime = true, //驗證過期時間
                    ValidateLifetime = true, //驗證生命周期
                    ClockSkew = TimeSpan.Zero, //緩衝過期時間,即使配置了過期時間,也要考慮過期時間+緩衝時間
                };
            });

            //註冊HttpContext存取器服務
            services.AddHttpContextAccessor();

            //自定義判斷屬性隱射關係
            services.AddTransient<IPropertyMappingService, PropertyMappingService>();

            services.AddTransient<IPropertyCheckService, PropertyCheckService>();
        }

        //configureContainer訪問AutoFac容器生成器
        public void ConfigureContainer(ContainerBuilder builder)
        {
            //獲取程序集並註冊,採用每次請求都創建一個新的對象的模式
            var assemblyBll = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "BlogSystem.BLL.dll"));
            var assemblyDal = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "BlogSystem.DAL.dll"));

            builder.RegisterAssemblyTypes(assemblyDal).AsImplementedInterfaces().InstancePerDependency();

            //註冊攔截器
            builder.RegisterType<LogAop>();
            //對目標類型啟用動態代理,並注入自定義攔截器攔截BLL
            builder.RegisterAssemblyTypes(assemblyBll).AsImplementedInterfaces().InstancePerDependency()
           .EnableInterfaceInterceptors().InterceptedBy(typeof(LogAop));
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler(builder =>
                {
                    builder.Run(async context =>
                    {
                        context.Response.StatusCode = 500;
                        await context.Response.WriteAsync("Unexpected Error!");
                    });
                });
            }

            app.UseRouting();

           //添加認證中間件
            app.UseAuthentication();

            //添加授權中間件
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

        private IConfiguration GetConfig(string environmentName)
        {
            var path = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;

            IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(path)
               .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

            if (!string.IsNullOrWhiteSpace(environmentName))
            {
                builder = builder.AddJsonFile($"appsettings.{environmentName}.json", optional: true);
            }

            builder = builder.AddEnvironmentVariables();

            return builder.Build();
        }
    }
}

2、這裏對UserController中的註冊、登錄、獲取用戶信息方法進行測試,實際上這裏的斷言並不嚴謹,會產生什麼後果?請繼續往下看

using BlogSystem.Model.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace BlogSystem.Core.Test.Controller_Test
{
    public class UserController_Should
    {
        const string _mediaType = "application/json";
        readonly Encoding _encoding = Encoding.UTF8;


        /// <summary>
        /// 用戶註冊
        /// </summary>
        [Fact]
        public async Task Register_Test()
        {
            // 1、Arrange
            var data = new RegisterViewModel { Account = "test", Password = "123456", RequirePassword = "123456" };

            StringContent content = new StringContent(JsonConvert.SerializeObject(data), _encoding, _mediaType);

            using var host = await TestServerFixture.GetTestHost().StartAsync();//啟動TestServer

            // 2、Act
            var response = await host.GetTestClient().PostAsync($"http://localhost:5000/api/user/register", content);

            var result = await response.Content.ReadAsStringAsync();

            // 3、Assert
            Assert.DoesNotContain("用戶已存在", result);
        }

        /// <summary>
        /// 用戶登錄
        /// </summary>
        [Fact]
        public async Task Login_Test()
        {
            var data = new LoginViewModel { Account = "jordan", Password = "123456" };

            StringContent content = new StringContent(JsonConvert.SerializeObject(data), _encoding, _mediaType);

            var host = await TestServerFixture.GetTestHost().StartAsync();//啟動TestServer

            var response = await host.GetTestClientWithToken().PostAsync($"http://localhost:5000/api/user/Login", content);

            var result = await response.Content.ReadAsStringAsync();

            Assert.DoesNotContain("賬號或密碼錯誤!", result);
        }

        /// <summary>
        /// 獲取用戶信息
        /// </summary>
        [Fact]
        public async Task UserInfo_Test()
        {
            string id = "jordan";

            using var host = await TestServerFixture.GetTestHost().StartAsync();//啟動TestServer

            var client = host.GetTestClient();

            var response = await client.GetAsync($"http://localhost:5000/api/user/{id}");

            var result = response.StatusCode;

            Assert.True(Equals(HttpStatusCode.OK, result)|| Equals(HttpStatusCode.NotFound, result));
        }
    }
}

4.5、異常及解決

1、添加完上述的測試方法后,我們使用打開Visual Studio自帶的測試資源管理器,點擊運行所有測試,發現提示錯誤無法加載BLL?在原先的BlogSystem.Core的StartUp類中我們是加載BLL和DAL項目的dll來達到解耦的目的,所以做了一個將dll輸出到Core項目bin文件夾的動作,但是在測試項目的TestStarup類中,我們是無法加載到BLL和DAL的。我嘗試將BLL和DAL同時輸出到兩個路徑下,但未找到對應的方法,所以這裏我採用了最簡單的解決方法,測試項目添加了對DAL和BLL的引用。再次運行,如下圖,似乎成功了??

2、我們在測試方法內部打上斷點,右擊測試方法,選擇調試測試,結果發現response參數為空,只應Assert不嚴謹導致看上去沒有問題;在各種查找后,我終於找到了解決辦法,在TestStarup類的ConfigureServices方法內部service.AddControllers方法最後加上這麼一句話即可解決 .AddApplicationPart(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "BlogSystem.Core.dll")))

3、再次運行測試方法,成功!但是又發現了另外一個問題,這裏我們只是測試,但是數據庫中卻出現了我們測試添加的test賬號,如何解決?我們可以使用Microsoft.EntityFrameworkCore.InMemory庫 ,它支持使用內存數據庫進行測試,這裏暫未添加,有興趣的朋友可以自行研究。

本章完~

本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。

本文部分內容參考了網絡上的視頻內容和文章,僅為學習和交流,視頻地址如下:

老張的哲學,系列教程一目錄:.netcore+vue 前後端分離

我想吃晚飯,ASP.NET Core搭建多層網站架構【12-xUnit單元測試之集成測試】

solenovex,使用 xUnit.NET 對 .NET Core 項目進行單元測試

solenovex,ASP.NET Core Web API 集成測試

微軟官方文檔,.NET Core 和 .NET Standard 中的單元測試

Edison Zhou,.NET單元測試的藝術

聲明

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!