重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」

作者:小傅哥
博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

你感受到的容易,一定有人為你承擔不容易

這句話更像是描述生活的,許許多多的磕磕絆絆總有人為你提供躲雨的屋檐和避風的港灣。其實編程開發的團隊中也一樣有人只負責CRUD中的簡單調用,去使用團隊中高級程序員開發出來的核心服務和接口。這樣的編程開發對於初期剛進入程序員行業的小夥伴來說鍛煉鍛煉還是不錯的,但隨着開發的日子越來越久一直做這樣的事情就很難得到成長,也想努力的去做一些更有難度的承擔,以此來增強個人的技術能力。

沒有最好的編程語言,語言只是工具

刀槍棍棒、斧鉞鈎叉、包子油條、盒子麻花,是語言。五郎八卦棍、十二路彈腿、洪家鐵線拳,是設計。記得恭弘=叶 恭弘問里有一句台詞是:金山找:今天我北方拳術,輸給你南方拳術了。恭弘=叶 恭弘問:你錯了,不是南北拳的問題,是你的問題。所以當你編程開發寫的久了,就不會再特別在意用的語言,而是為目標服務,用最好的設計能力也就是編程的智慧做出做最完美的服務。這也就是編程人員的價值所在!

設計與反設計以及過渡設計

設計模式是解決程序中不合理、不易於擴展、不易於維護的問題,也是幹掉大部分ifelse的利器,在我們常用的框架中基本都會用到大量的設計模式來構建組件,這樣也能方便框架的升級和功能的擴展。但!如果不能合理的設計以及亂用設計模式,會導致整個編程變得更加複雜難維護,也就是我們常說的;反設計過渡設計。而這部分設計能力也是從實踐的項目中獲取的經驗,不斷的改造優化摸索出的最合理的方式,應對當前的服務體量。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. SpringBoot 2.1.2.RELEASE
  4. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-10-00 場景模擬工程;模擬一個提供接口服務的SpringBoot工程
itstack-demo-design-10-01 使用一坨代碼實現業務需求
itstack-demo-design-10-02 通過設計模式開發為中間件,包裝通用型核心邏輯

三、外觀模式介紹

外觀模式也叫門面模式,主要解決的是降低調用方的使用接口的複雜邏輯組合。這樣調用方與實際的接口提供方提供方提供了一个中間層,用於包裝邏輯提供API接口。有些時候外觀模式也被用在中間件層,對服務中的通用性複雜邏輯進行中間件層包裝,讓使用方可以只關心業務開發。

那麼這樣的模式在我們的所見產品功能中也經常遇到,就像幾年前我們註冊一個網站時候往往要添加很多信息,包括;姓名、昵稱、手機號、QQ、郵箱、住址、單身等等,但現在註冊成為一個網站的用戶只需要一步即可,無論是手機號還是微信也都提供了這樣的登錄服務。而對於服務端應用開發來說以前是提供了一個整套的接口,現在註冊的時候並沒有這些信息,那麼服務端就需要進行接口包裝,在前端調用註冊的時候服務端獲取相應的用戶信息(從各個渠道),如果獲取不到會讓用戶後續進行補全(營銷補全信息給獎勵),以此來拉動用戶的註冊量和活躍度。

四、案例場景模擬

在本案例中我們模擬一個將所有服務接口添加白名單的場景

在項目不斷壯大發展的路上,每一次發版上線都需要進行測試,而這部分測試驗證一般會進行白名單開量或者切量的方式進行驗證。那麼如果在每一個接口中都添加這樣的邏輯,就會非常麻煩且不易維護。另外這是一類具備通用邏輯的共性需求,非常適合開發成組件,以此來治理服務,讓研發人員更多的關心業務功能開發。

一般情況下對於外觀模式的使用通常是用在複雜或多個接口進行包裝統一對外提供服務上,此種使用方式也相對簡單在我們平常的業務開發中也是最常用的。你可能經常聽到把這兩個接口包裝一下,但在本例子中我們把這種設計思路放到中間件層,讓服務變得可以統一控制。

1. 場景模擬工程

itstack-demo-design-10-00
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── domain
    │   │       │	└── UserInfo.java
    │   │       ├── web	
    │   │       │	└── HelloWorldController.java
    │   │       └── HelloWorldApplication.java
    │   └── resources	
    │       └── application.yml	
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java
  • 這是一個SpringBootHelloWorld工程,在工程中提供了查詢用戶信息的接口HelloWorldController.queryUserInfo,為後續擴展此接口的白名單過濾做準備。

2. 場景簡述

2.1 定義基礎查詢接口

@RestController
public class HelloWorldController {

    @Value("${server.port}")
    private int port;

    /**
     * key:需要從入參取值的屬性字段,如果是對象則從對象中取值,如果是單個值則直接使用
     * returnJson:預設攔截時返回值,是返回對象的Json
     *
     * http://localhost:8080/api/queryUserInfo?userId=1001
     * http://localhost:8080/api/queryUserInfo?userId=小團團
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
    }

}
  • 這裏提供了一個基本的查詢服務,通過入參userId,查詢用戶信息。後續就需要在這裏擴展白名單,只有指定用戶才可以查詢,其他用戶不能查詢。

2.2 設置Application啟動類

@SpringBootApplication
@Configuration
public class HelloWorldApplication {

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

}
  • 這裡是通用的SpringBoot啟動類。需要添加的是一個配置註解@Configuration,為了後續可以讀取白名單配置。

五、用一坨坨代碼實現

一般對於此種場景最簡單的做法就是直接修改代碼

累加if塊幾乎是實現需求最快也是最慢的方式,是修改當前內容很快,是如果同類的內容幾百個也都需要如此修改擴展和維護會越來越慢。

1. 工程結構

itstack-demo-design-10-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── HelloWorldController.java
  • 以上的實現是模擬一個Api接口類,在裏面添加白名單功能,但類似此類的接口會有很多都需要修改,所以這也是不推薦使用此種方式的重要原因。

2. 代碼實現

public class HelloWorldController {

    public UserInfo queryUserInfo(@RequestParam String userId) {

        // 做白名單攔截
        List<String> userList = new ArrayList<String>();
        userList.add("1001");
        userList.add("aaaa");
        userList.add("ccc");
        if (!userList.contains(userId)) {
            return new UserInfo("1111", "非白名單可訪問用戶攔截!");
        }

        return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
    }

}
  • 在這裏白名單的代碼佔據了一大塊,但它又不是業務中的邏輯,而是因為我們上線過程中需要做的開量前測試驗證。
  • 如果你日常對待此類需求經常是這樣開發,那麼可以按照此設計模式進行優化你的處理方式,讓後續的擴展和摘除更加容易。

六、外觀模式重構代碼

接下來使用外觀器模式來進行代碼優化,也算是一次很小的重構。

這次重構的核心是使用外觀模式也可以說門面模式,結合SpringBoot中的自定義starter中間件開發的方式,統一處理所有需要白名單的地方。

後續接下來的實現中,會涉及的知識;

  1. SpringBoot的starter中間件開發方式。
  2. 面向切面編程和自定義註解的使用。
  3. 外部自定義配置信息的透傳,SpringBoot與Spring不同,對於此類方式獲取白名單配置存在差異。

1. 工程結構

itstack-demo-design-10-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design.door
    │   │       ├── annotation
    │   │       │	└── DoDoor.java	
    │   │       ├── config
    │   │       │	├── StarterAutoConfigure.java
    │   │       │	├── StarterService.java
    │   │       │	└── StarterServiceProperties.java
    │   │       └── DoJoinPoint.java
    │   └── resources	
    │       └── META_INF
    │           └── spring.factories
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

門面模式模型結構

  • 以上是外觀模式的中間件實現思路,右側是為了獲取配置文件,左側是對於切面的處理。
  • 門面模式可以是對接口的包裝提供出接口服務,也可以是對邏輯的包裝通過自定義註解對接口提供服務能力。

2. 代碼實現

2.1 配置服務類

public class StarterService {

    private String userStr;

    public StarterService(String userStr) {
        this.userStr = userStr;
    }

    public String[] split(String separatorChar) {
        return StringUtils.split(this.userStr, separatorChar);
    }

}
  • 以上類的內容較簡單隻是為了獲取配置信息。

2.2 配置類註解定義

@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {

    private String userStr;

    public String getUserStr() {
        return userStr;
    }

    public void setUserStr(String userStr) {
        this.userStr = userStr;
    }

}
  • 用於定義好後續在 application.yml 中添加 itstack.door 的配置信息。

2.3 自定義配置類信息獲取

@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {

    @Autowired
    private StarterServiceProperties properties;

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
    StarterService starterService() {
        return new StarterService(properties.getUserStr());
    }

}
  • 以上代碼是對配置的獲取操作,主要是對註解的定義;@Configuration@ConditionalOnClass@EnableConfigurationProperties,這一部分主要是與SpringBoot的結合使用。

2.4 切面註解定義

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {

    String key() default "";

    String returnJson() default "";

}
  • 定義了外觀模式門面註解,後續就是此註解添加到需要擴展白名單的方法上。
  • 這裏提供了兩個入參,key:獲取某個字段例如用戶ID、returnJson:確定白名單攔截后返回的具體內容。

2.5 白名單切面邏輯

@Aspect
@Component
public class DoJoinPoint {

    private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);

    @Autowired
    private StarterService starterService;

    @Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
    public void aopPoint() {
    }

    @Around("aopPoint()")
    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
        //獲取內容
        Method method = getMethod(jp);
        DoDoor door = method.getAnnotation(DoDoor.class);
        //獲取字段值
        String keyValue = getFiledValue(door.key(), jp.getArgs());
        logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
        if (null == keyValue || "".equals(keyValue)) return jp.proceed();
        //配置內容
        String[] split = starterService.split(",");
        //白名單過濾
        for (String str : split) {
            if (keyValue.equals(str)) {
                return jp.proceed();
            }
        }
        //攔截
        return returnObject(door, method);
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

    private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
        return jp.getTarget().getClass();
    }

    //返回對象
    private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
        Class<?> returnType = method.getReturnType();
        String returnJson = doGate.returnJson();
        if ("".equals(returnJson)) {
            return returnType.newInstance();
        }
        return JSON.parseObject(returnJson, returnType);
    }

    //獲取屬性值
    private String getFiledValue(String filed, Object[] args) {
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (null == filedValue || "".equals(filedValue)) {
                    filedValue = BeanUtils.getProperty(arg, filed);
                } else {
                    break;
                }
            } catch (Exception e) {
                if (args.length == 1) {
                    return args[0].toString();
                }
            }
        }
        return filedValue;
    }

}
  • 這裏包括的內容較多,核心邏輯主要是;Object doRouter(ProceedingJoinPoint jp),接下來我們分別介紹。

@Pointcut(“@annotation(org.itstack.demo.design.door.annotation.DoDoor)”)

定義切面,這裏採用的是註解路徑,也就是所有的加入這個註解的方法都會被切面進行管理。

getFiledValue

獲取指定key也就是獲取入參中的某個屬性,這裏主要是獲取用戶ID,通過ID進行攔截校驗。

returnObject

返回攔截后的轉換對象,也就是說當非白名單用戶訪問時則返回一些提示信息。

doRouter

切面核心邏輯,這一部分主要是判斷當前訪問的用戶ID是否白名單用戶,如果是則放行jp.proceed();,否則返回自定義的攔截提示信息。

3. 測試驗證

這裏的測試我們會在工程:itstack-demo-design-10-00中進行操作,通過引入jar包,配置註解的方式進行驗證。

3.1 引入中間件POM配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>itstack-demo-design-10-02</artifactId>
</dependency>
  • 打包中間件工程,給外部提供jar包服務

3.2 配置application.yml

# 自定義中間件配置
itstack:
  door:
    enabled: true
    userStr: 1001,aaaa,ccc #白名單用戶ID,多個逗號隔開
  • 這裏主要是加入了白名單的開關和白名單的用戶ID,逗號隔開。

3.3 在Controller中添加自定義註解

/**
 * http://localhost:8080/api/queryUserInfo?userId=1001
 * http://localhost:8080/api/queryUserInfo?userId=小團團
 */
@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
    return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
}
  • 這裏核心的內容主要是自定義的註解的添加@DoDoor,也就是我們的外觀模式中間件化實現。
  • key:需要從入參取值的屬性字段,如果是對象則從對象中取值,如果是單個值則直接使用。
  • returnJson:預設攔截時返回值,是返回對象的Json。

3.4 啟動SpringBoot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.2.RELEASE)

2020-06-11 23:56:55.451  WARN 65228 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2020-06-11 23:56:55.531  INFO 65228 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-11 23:56:55.533  INFO 65228 --- [           main] o.i.demo.design.HelloWorldApplication    : Started HelloWorldApplication in 1.688 seconds (JVM running for 2.934)
  • 啟動正常,SpringBoot已經啟動可以對外提供服務。

3.5 訪問接口接口測試

白名單用戶訪問

http://localhost:8080/api/queryUserInfo?userId=1001

{"code":"0000","info":"success","name":"蟲蟲:1001","age":19,"address":"天津市南開區旮旯衚衕100號"}
  • 此時的測試結果正常,可以拿到接口數據。

非白名單用戶訪問

http://localhost:8080/api/queryUserInfo?userId=小團團

{"code":"1111","info":"非白名單可訪問用戶攔截!","name":null,"age":null,"address":null}
  • 這次我們把userId換成小團團,此時返回的信息已經是被攔截的信息。而這個攔截信息正式我們自定義註解中的信息:@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")

七、總結

  • 以上我們通過中間件的方式實現外觀模式,這樣的設計可以很好的增強代碼的隔離性,以及復用性,不僅使用上非常靈活也降低了每一個系統都開發這樣的服務帶來的風險。
  • 可能目前你看這隻是非常簡單的白名單控制,是否需要這樣的處理。但往往一個小小的開始會影響着後續無限的擴展,實際的業務開發往往也要複雜的很多,不可能如此簡單。因而使用設計模式來讓代碼結構更加乾淨整潔。
  • 很多時候不是設計模式沒有用,而是自己編程開發經驗不足導致即使學了設計模式也很難駕馭。畢竟這些知識都是經過一些實際操作提煉出來的精華,但如果你可以按照本系列文章中的案例方式進行學習實操,還是可以增強這部分設計能力的。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式(多種類型商品發獎場景)
  • 2. 重學 Java 設計模式:實戰抽象工廠模式(替換Redis雙集群升級場景)
  • 3. 重學 Java 設計模式:實戰建造者模式(裝修物料組合套餐選配場景)
  • 4. 重學 Java 設計模式:實戰原型模式(多套試每人題目和答案亂序場景)
  • 5. 重學 Java 設計模式:實戰橋接模式(多支付渠道「微信、支付寶」與多支付模式「刷臉、指紋」場景)
  • 6. 重學 Java 設計模式:實戰組合模式(營銷差異化人群發券,決策樹引擎搭建場景)

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

【其他文章推薦】

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

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

Jmeter(九) – 從入門到精通 – JMeter邏輯控制器 – 上篇(詳解教程)

1.簡介

Jmeter官網對邏輯控制器的解釋是:“Logic Controllers determine the order in which Samplers are processed.”。

意思是說,邏輯控制器可以控制採樣器(samplers)的執行順序。由此可知,控制器需要和採樣器一起使用,否則控制器就沒有什麼意義了。放在控制器下面的所有的採樣器都會當做一個整體,執行時也會一起被執行。

JMeter邏輯控制器可以對元件的執行邏輯進行控制,除僅一次控制器外,其他可以嵌套別的種類的邏輯控制器。

2.邏輯控制器分類

JMeter中的Logic Controller分為兩類:
(1)控制測試計劃執行過程中節點的邏輯執行順序,如:Loop Controller、If Controller等;
(2)對測試計劃中的腳本進行分組、方便JMeter統計執行結果以及進行腳本的運行時控制等,如:Throughput Controller、Transaction Controller。

3.預覽邏輯控制器家族

首先我們來看一下JMeter的邏輯控制器,路徑:線程組(用戶)->添加->邏輯控制器(Logic Controller);我們可以清楚地看到JMeter5中共有17個邏輯控制器,如下圖所示:

如果上圖您看得不是很清楚的話,宏哥總結了一個思維導圖,關於JMeter5的邏輯控制器類型,如下圖所示: 

 通過以上的了解,我們對邏輯控制器有了一個大致的了解和認識。下面宏哥就給小夥伴或則童鞋們分享講解一些通常在工作中會用到的邏輯控制器。 

4.常用邏輯控制器詳解

  這一小節,宏哥就由上而下地詳細地講解一下常用的邏輯控制器。由於時間關係,宏哥將這部分分為上、中、下三個部分講解。

4.1if Controller

在實際工作中,當使用Jmeter進行接口測試或者性能測試時,有時需要根據不同條件做不同的操作,為了解決這個問題,Jmeter提供了IF控制器。顧名思義,IF控制器實現了代碼中IF的功能,通過判斷表達式的True/False來判定是否執行相應的操作。通過條件判斷下邊的節點執行不執行。

1、我們先來看看這個if Controller長得是啥樣子,路徑:線程組 > 添加 > 邏輯控制器 > 如果 (if) 控制器,如下圖所示:

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空。

Expression (must evaluate to true or false) :表達式(值必須是true或false),也就是說,在右邊文本框中輸入的條件值必須是true 或 false,(默認情況下)

Interpret Condition as Variable Expression?:默認勾選項,將條件解釋為變量表達式(需要使用__jexl3 or __groovy 表達式)

Evaluate for all children?:條件作用於每個子項(具體理解宏哥會在後邊實戰篇列舉例子說明),判斷條件是否針對所有子節點,默認不勾選,只在if Controller的入口處判斷一次。

注意:敲黑板!!!敲腦殼啦!!!

1、文本框上的黃色感嘆號,就是提示你,建議採用__jexl3 or __groovy 表達式,以提高性能,也就是默認的方式。

2、if 控制器 只能作用於其下的子項

4.1.1默認用法

1、默認用法,就是採用__jexl3 or __groovy 表達式if 控制器下有一個 訪問北京宏哥的博客園首頁的取樣器,只有if條件滿足時,才會執行該取樣器。採用默認方式,將條件’北京宏哥’==’北京宏哥’,放入 __jexl3表達式中。如下圖所示:

 2、如果不知道表達式如何使用,可使用Jmeter 的函數助手,函數助手圖標 > 選擇_jexl3 > 在值的輸入框輸入’北京宏哥’==’北京宏哥’  > 點擊‘生成’ > 全選Ctrl+C複製 > Ctrl+V粘貼到表達式處 如下圖所示:

3、配置好以後,運行JMeter,選擇HTML,然後查看結果樹,如下圖所示:

4.1.2直接輸入條件

1、直接輸入只需要去掉 “Interpret Condition as Variable Expression?” 前面複選框,直接輸入條件: ‘北京宏哥’==’北京宏哥’ 。訪問北京宏哥的博客園的首頁的取樣器將被執行。如下圖所示:

2、配置好以後,運行JMeter,選擇HTML,然後查看結果樹,如下圖所示:

4.13條件中使用變量

我們在日常工作中在很多的測試場景下,需要根據用戶變量或者上一個取樣器的返回值來進行條件判斷,從而決定是否需要執行某一個的取樣器。

1、首先我們新增一個用戶變量:北京宏哥。條件:北京宏哥 的值為 宏哥 的時候,才執行訪問北京宏哥博客園的首頁的取樣器。如下圖所示:

用戶變量及配置,如下圖所示:

2、IF Controller及配置,或者可以用表達式:${__jexl3(‘${北京宏哥}’==’宏哥’ ,)}。如下圖所示: 

4.1.4Evaluate for all children? 的用法

1、宏哥在上面的小節中講解和分享了在條件中如何使用變量,我們假設一種測試場景:如果 if 控制器下的取樣器執行后,改變了該變量的值,if 控制器下 其後的取樣器還會被繼續執行嗎?跟隨宏哥一起來看看下面的列子:

2、改變“北京宏哥”變量的值為“北京宏哥”,如下圖所示:

3、JMeter執行過程的邏輯分析:

(1) if 控制器下 有 3 個取樣器,變量 北京宏哥 的初始值為 宏哥,if 控制器的條件為:${__jexl3(“${北京宏哥}”==”宏哥”,)}

(2)開始執行的時候滿足條件,那麼按理說應該執行 訪問博客園首頁訪問北京宏哥的博客園首頁訪問宏哥的JMeter系列文章 3個取樣器,

(3)但是 訪問北京宏哥的博客園首頁 執行后,將 北京宏哥 的值變了 北京宏哥,已經不能滿足 “${北京宏哥}”==”宏哥” 條件。

(4)所以 訪問宏哥的JMeter系列文章 這個取樣器不會被執行。

4、運行JMeter,查看結果樹,對比運行結果和宏哥分析的一致,如下圖所示:

5、如果這個時候,去掉  Evaluate for all children? 的勾選,會發生什麼呢,大家可以自己動手試試。修改後記得點擊“保存”。下邊是宏哥的執行結果,如下下圖所示:

另外,如果時字符串必須要用引號,變量都認為是字符串的形式,如:${__jexl3(“${北京宏哥}”==”宏哥”,)}。

4.2Transaction Controller

  事務響應時間是我們衡量業務性能的主要指標,事務控制器可以把其他節點下的取樣器執行消耗時間累加在一起,便於統計。同時對每一個取樣器的執行時間進行統計。
  如果事務控制器下的取樣器有多個,只有當所有的取樣器都運行成功,整個事務控制器定義的事物才算成功。
  用於將Test Plan中的特定部分組織成一個Transaction,JMeter中Transaction的作用在於,可以針對Transaction統計其響應時間、吞吐量等。比如說,一個用戶操作可能需要多個Sampler來模擬,此時使用Transaction Controller,可以更準確地得到該用戶操作的性能指標,如響應時間等。這個時間包含該控制器範圍內的所有處理時間,而不僅僅是採樣器的。

這個就非常有用了。我們前面有提到過事務的概念,有時候我們不關心單個請求的響應時間,而是關心一組相關請求的整體響應時間,怎麼來統計呢?就需要藉助事務這個概念,把這組請求,放到一個事務控制器下面。

1、我們先來看看這個Transaction Controller長得是啥樣子,路徑:線程組 > 添加 > 邏輯控制器 > 事務控制器,如下圖所示:

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

generate parent sample:選擇是否生成一個父取樣器;

include duration of timer and pre-post processors in generated samle:是否包含定時器,選擇將在取樣器前與后加上延時。(宏哥建議大家不要勾選,否則統計就比較麻煩了,還需要你扣除延時)

4.2.1generate parent sample用法

1、宏哥列舉一個測試場景:我們需要了解 訪問博客園首頁  訪問北京宏哥的博客園首頁這兩個請求的單個請求的響應時間,那麼就來看看如下實例。

(1)單個請求,那麼不勾選generate parent sample,如下圖所示:

2、運行JMeter,查看聚合報告的單個請求的響應時間,如下圖所示:

1、宏哥列舉一個測試場景:我們需要了解 訪問博客園首頁  訪問北京宏哥的博客園首頁這兩個請求作為一組請求的響應時間,那麼就來看看如下實例。

(1)一組請求,那麼勾選generate parent sample,如下圖所示:

2、運行JMeter,查看聚合報告的一組請求的響應時間,如下圖所示:

4.3Loop Controller

循環控制器可以控制在其節點下的元件的執行次數,可以是具體数字,也可以是變量。

1、我們先來看看這個Loop Controller長得是啥樣子,默認循環一次。路徑:線程組 > 添加 > 邏輯控制器 > 循環控制器,如下圖所示:

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Forever:勾選上這一項表示一直循環下去。

注意:敲黑板,敲腦殼!!! 

如果同時設置了線程組的循環次數和循環控制器的循環次數,那循環控制器的子節點運行的次數為兩個數值相乘的結果。

4.3.1Thread Group和循環控制器的區別

1、現在宏哥準備兩個請求,設置線程組1個線程,5次loop,下邊有一個請求:訪問北京宏哥的博客園首頁  一個Loop Controller(設置2次loop),下邊有一個請求:訪問博客園首頁  

(1)線程組,如下圖所示:

(2)循環控制器,如下圖所示:

 

2、運行JMeter,查看結果樹,為了清楚地看出結果,宏哥將第一個請求故意配置成失敗的;如下圖所示:

從上邊的結果可以看出:

(1)如果同時設置了線程組的循環次數和循環控制器的循環次數,那循環控制器的子節點運行的次數為兩個數值相乘的結果。

(2)運行順序是:先執行線程組裡的循環,再執行循環控制器里的循環。

4.4While Controller

While條件控制器,其節點下的元件將一直運行直到While 條件為false。

1、我們先來看看這個While Controller長得是啥樣子,默認循環一次。路徑:線程組 > 添加 > 邏輯控制器 > While控制器,如下圖所示:

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Condition:接受變量表達式與變量。條件為 Flase 的時候,才會跳出 While 循環,否則一直執行 While 控制器下的元件。

3、While控制器提供三個常量

(1)Blank:當循環中最後一個取樣器失敗后停止

(2)LAST:當循換前有取樣器失敗,不進入循環

(3)Otherwise:當判斷條件為false時,停止循環

4.4.1Blank

1、不填(空):當 While 控制器下最後一個樣例執行失敗后 跳出循環,如下圖所示:

2、運行JMeter,查看結果樹,(你可以通過鼠標拖動最後失敗的取樣器,移動到第一個或者第二個位置的時候,運行JMeter后,會發現在一直運行);如下圖所示:

4.4.2LAST

LAST :當 While 控制器下最後一個樣例執行失敗后 跳出循環,如果 While 控制器 前一個樣例執行失敗,則不會進入While循環,也就是不會執行While控制器下的樣例。

1、取樣器樹還是上邊的位置和順序。這次我們在While控制器表達式處填寫:LAST,如下圖所示:

2、運行JMeter,查看結果樹,(你可以通過鼠標拖動最後失敗的取樣器,移動到第一個或者第二個位置的時候,運行JMeter后,會發現在一直運行);細心的你可以發現循環只跑一遍,與不填 的結果是一樣的如下圖所示:

3、但是輸入LAST的時候,還會出現一個結果,那就是:如果While 控制器 的前一個樣例執行失敗,則不會進入While 控制器

在While 控制器 前面 添加兩個取樣器:取樣器1 訪問百度,取樣器2 訪問北京宏哥 使取樣器2 訪問北京宏哥 執行失敗。取樣器2必須在While控制器前邊且執行失敗。如下圖所示:

 4、運行JMeter,查看結果樹,執行結果發現,取樣器1、取樣器2 執行了,但沒有進入While 控制器,如下圖所示:

4.4.3Otherwise

自定義條件:值為True 或 False的函數/變量/屬性 表達式;類似前邊講解的IF控制器,宏哥這裏就照貓畫虎的舉個例子。

1、用戶自定義變量,變量名:北京宏哥,變量值:true,如下圖所示:

2、While控制器配置,取到變量的值:${北京宏哥},填寫到表達式的地方,如下圖所示: 

3、JMeter執行過程的邏輯分析:

(1)北京宏哥用戶(線程組)下 有 1 個用戶自定義變量,變量 北京宏哥 的值為 true,While控制器的條件為:${北京宏哥} 取到的值始終是 true

(2)所以一旦開始執行始終滿足條件,那麼按理說就應該一直執行 訪問博客園首頁 、訪問北京宏哥的博客園首頁訪問宏哥的JMeter系列文章 3個取樣器,

4、運行JMeter,查看結果樹,(運行JMeter后,會發現在一直運行),對比一下,與宏哥的分析是不是高度一致哈;如下圖所示:

5.小結

 好了,今天關於邏輯控制器的上篇就講解到這裏,這一篇主要介紹了 IF控制器Transaction ControllerLoop ControllerWhile控制器

 

您的肯定就是我進步的動力。如果你感覺還不錯,就請鼓勵一下吧!記得隨手點波  推薦  不要忘記哦!!!

別忘了點 推薦 留下您來過的痕迹

 

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

【華為雲技術分享】數據庫開發:MySQL Seconds_Behind_Master簡要分析

【摘要】對於mysql主備實例,seconds_behind_master是衡量master與slave之間延時的一個重要參數。通過在slave上執行”show slave status;”可以獲取seconds_behind_master的值。

Seconds_Behind_Master

對於mysql主備實例,seconds_behind_master是衡量master與slave之間延時的一個重要參數。通過在slave上執行”show slave status;”可以獲取seconds_behind_master的值。

原始實現

Definition:The number of seconds that the slave SQL thread is behind processing the master binary log.

Type:time_t(long)

計算方式如下:

rpl_slave.cc::show_slave_status_send_data()
if ((mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()) &&
       (!strcmp(mi->get_master_log_name(),
                mi->rli->get_group_master_log_name()))) {
     if (mi->slave_running == MYSQL_SLAVE_RUN_CONNECT)
       protocol->store(0LL);
     else
       protocol->store_null();
   } else {
     long time_diff = ((long)(time(0) - mi->rli->last_master_timestamp) -
                       mi->clock_diff_with_master);
     protocol->store(
         (longlong)(mi->rli->last_master_timestamp ? max(0L, time_diff) : 0));
   }

主要分為以下兩種情況:

• SQL線程等待IO線程獲取主機binlog,此時seconds_behind_master為0,表示備機與主機之間無延時;

• SQL線程處理relay log,此時seconds_behind_master通過(long)(time(0) – mi->rli->last_master_timestamp) – mi->clock_diff_with_master計算得到;

last_master_timestamp

定義:

• 主庫binlog中事件的時間。

• type: time_t (long)

計算方式:

last_master_timestamp根據備機是否并行複製有不同的計算方式。

非并行複製:

rpl_slave.cc:exec_relay_log_event()
if ((!rli->is_parallel_exec() || rli->last_master_timestamp == 0) &&
    !(ev->is_artificial_event() || ev->is_relay_log_event() ||
     (ev->common_header->when.tv_sec == 0) ||
     ev->get_type_code() == binary_log::FORMAT_DESCRIPTION_EVENT ||
     ev->server_id == 0))
{
 rli->last_master_timestamp= ev->common_header->when.tv_sec +
                             (time_t) ev->exec_time;
 DBUG_ASSERT(rli->last_master_timestamp >= 0);
}

在該模式下,last_master_timestamp表示為每一個event的結束時間,其中when.tv_sec表示event的開始時間,exec_time表示事務的執行時間。該值的計算在apply_event之前,所以event還未執行時,last_master_timestamp已經被更新。由於exec_time僅在Query_log_event中存在,所以last_master_timestamp在應用一個事務的不同event階段變化。以一個包含兩條insert語句的事務為例,在該代碼段的調用時,打印出event的類型、時間戳和執行時間

create table t1(a int PRIMARY KEY AUTO_INCREMENT ,b longblob) engine=innodb;
begin;
insert into t1(b) select repeat('a',104857600);
insert into t1(b) select repeat('a',104857600);
commit;
10T06:41:32.628554Z 11 [Note] [MY-000000] [Repl] event_type: 33 GTID_LOG_EVENT
2020-02-10T06:41:32.628601Z 11 [Note] [MY-000000] [Repl] event_time: 1581316890
2020-02-10T06:41:32.628614Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0
2020-02-10T06:41:32.628692Z 11 [Note] [MY-000000] [Repl] event_type: 2   QUERY_EVENT
2020-02-10T06:41:32.628704Z 11 [Note] [MY-000000] [Repl] event_time: 1581316823
2020-02-10T06:41:32.628713Z 11 [Note] [MY-000000] [Repl] event_exec_time: 35
2020-02-10T06:41:32.629037Z 11 [Note] [MY-000000] [Repl] event_type: 19   TABLE_MAP_EVENT
2020-02-10T06:41:32.629057Z 11 [Note] [MY-000000] [Repl] event_time: 1581316823
2020-02-10T06:41:32.629063Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0
2020-02-10T06:41:33.644111Z 11 [Note] [MY-000000] [Repl] event_type: 30    WRITE_ROWS_EVENT
2020-02-10T06:41:33.644149Z 11 [Note] [MY-000000] [Repl] event_time: 1581316823
2020-02-10T06:41:33.644156Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0
2020-02-10T06:41:43.520272Z 0 [Note] [MY-011953] [InnoDB] Page cleaner took 9185ms to flush 3 and evict 0 pages
2020-02-10T06:42:05.982458Z 11 [Note] [MY-000000] [Repl] event_type: 19   TABLE_MAP_EVENT
2020-02-10T06:42:05.982488Z 11 [Note] [MY-000000] [Repl] event_time: 1581316858
2020-02-10T06:42:05.982495Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0
2020-02-10T06:42:06.569345Z 11 [Note] [MY-000000] [Repl] event_type: 30    WRITE_ROWS_EVENT
2020-02-10T06:42:06.569376Z 11 [Note] [MY-000000] [Repl] event_time: 1581316858
2020-02-10T06:42:06.569384Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0
2020-02-10T06:42:16.506176Z 0 [Note] [MY-011953] [InnoDB] Page cleaner took 9352ms to flush 8 and evict 0 pages
2020-02-10T06:42:37.202507Z 11 [Note] [MY-000000] [Repl] event_type: 16    XID_EVENT
2020-02-10T06:42:37.202539Z 11 [Note] [MY-000000] [Repl] event_time: 1581316890
2020-02-10T06:42:37.202546Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0

并行複製:

rpl_slave.cc   mts_checkpoint_routine
ts = rli->gaq->empty()
          ? 0
          : reinterpret_cast<Slave_job_group *>(rli->gaq->head_queue())->ts;
 rli->reset_notified_checkpoint(cnt, ts, true);
 /* end-of "Coordinator::"commit_positions" */

在該模式下備機上存在一個分發隊列gaq,如果gaq為空,則設置last_commit_timestamp為0;如果gaq不為空,則此時維護一個checkpoint點lwm,lwm之前的事務全部在備機上執行完成,此時last_commit_timestamp被更新為lwm所在事務執行完成后的時間。該時間類型為time_t類型。

ptr_group->ts = common_header->when.tv_sec +
                   (time_t)exec_time;  // Seconds_behind_master related
rli->rli_checkpoint_seqno++;
if (update_timestamp) {
 mysql_mutex_lock(&data_lock);
 last_master_timestamp = new_ts;
 mysql_mutex_unlock(&data_lock);
}

在并行複製下,event執行完成之後才會更新last_master_timestamp,所以非并行複製和并行複製下的seconds_behind_master會存在差異。

clock_diff_with_master

定義:

• The difference in seconds between the clock of the master and the clock of the slave (second – first). It must be signed as it may be <0 or >0. clock_diff_with_master is computed when the I/O thread starts; for this the I/O thread does a SELECT UNIX_TIMESTAMP() on the master.

• type: long

rpl_slave.cc::get_master_version_and_clock()
if (!mysql_real_query(mysql, STRING_WITH_LEN("SELECT UNIX_TIMESTAMP()")) &&
     (master_res= mysql_store_result(mysql)) &&
     (master_row= mysql_fetch_row(master_res)))
 {
   mysql_mutex_lock(&mi->data_lock);
   mi->clock_diff_with_master=
     (long) (time((time_t*) 0) - strtoul(master_row[0], 0, 10));
   DBUG_EXECUTE_IF("dbug.mts.force_clock_diff_eq_0",
     mi->clock_diff_with_master= 0;);
   mysql_mutex_unlock(&mi->data_lock);
 }

該差值僅被計算一次,在master與slave建立聯繫時處理。

其他

exec_time

定義:

• the difference from the statement’s original start timestamp and the time at which it completed executing.

• type: unsigned long

struct timeval end_time;
ulonglong micro_end_time = my_micro_time();
my_micro_time_to_timeval(micro_end_time, &end_time);
exec_time = end_time.tv_sec - thd_arg->query_start_in_secs();

時間函數

(1)time_t time(time_t timer) time_t為long類型,返回的數值僅精確到秒;

(2)int gettimeofday (struct timeval *tv, struct timezone *tz) 可以獲得微秒級的當前時間;

(3)timeval結構

#include <time.h>
stuct timeval {
   time_t tv_sec; /*seconds*/
   suseconds_t tv_usec; /*microseconds*/
}

總結

使用seconds_behind_master衡量主備延時只能精確到秒級別,且在某些場景下,seconds_behind_master並不能準確反映主備之間的延時。主備異常時,可以結合seconds_behind_master源碼進行具體分析。

 

點擊關注,第一時間了解華為雲新鮮技術~

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

DRY原則的一個簡單實踐

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

原文出處:https://dzone.com/articles/dry-dont-repeat-yourself

 

我們之前就發過一篇相關的文章:https://www.cnblogs.com/powertoolsteam/p/12758496.html 其中也提到了包括DRY在內的一些軟件開發的原則。

DRY 是軟件開發的原則之一,其目的主要是為了避免代碼重複,指導開發者盡量以抽象的思維去解決重複,基本上是,當您發現自己一遍又一遍地編寫相同的代碼時,可能會有更好的方法。 

實際案例

讓我們先看一個例子,看看這個例子是否可以改進,以及如何通過重構來避免代碼重複。

這裡有一個簡單的Report類,該類接收一些數據並通過控制台以格式化的方式直接輸出。

我們這裏使用php的一個代碼片段來舉例,相信大家對代碼的結構和想要完成的工作都不難理解,所以為了大家更容易理解,我只對一些下面用到的php函數定義做一個解釋:

  1. echo()  函數輸出一個或多個字符串
  2. ucwords()函數把字符串中每個單詞的首字符轉換為大寫。
  3. strtolower() 函數把字符串轉換為小寫。
  4. file_put_contents() 函數把一個字符串寫入文件中。
  5. floor() 函數向下舍入為最接近的整數。
class Report
{
   public function show(array $data)
   {
       echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
       echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
       echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       echo "Total: " . $data["total"] . "\n";
       echo "Average x day: " . floor($data["total"] / 365) . "\n";
       echo "Average x week: " . floor($data["total"] / 52) . "\n";
   }
}

可以看到,上面的代碼完成目標是沒有任何問題的。

這時我們對Report類提出一個新的需求:把所有字符串也可以保存到文件中。

我們經過一通複製和粘貼上面的代碼,新建一個名為saveToFile的函數,就可以很快的完成這個需求,代碼如下:

class Report
{
   public function show(array $data)
   {
       echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
       echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
       echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       echo "Total: " . $data["total"] . "\n";
       echo "Average x day: " . floor($data["total"] / 365) . "\n";
       echo "Average x week: " . floor($data["total"] / 52) . "\n";
       echo "Average x month: " . floor($data["total"] / 12) . "\n";
   }
   public function saveToFile(array $data)
   {
       $report = '';
       $report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
       $report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
       $report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       $report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       $report .= "Total: " . $data["total"] . "\n";
       $report .= "Average x day: " . floor($data["total"] / 365) . "\n";
       $report .= "Average x week: " . floor($data["total"] / 52) . "\n";
       $report .= "Average x month: " . floor($data["total"] / 12) . "\n";
       file_put_contents("./report.txt", $report);
   }
}

那麼,上面的代碼能夠滿足我們提出的需求嗎?答案當然“是的”。但是從技術角度來看,這段代碼似乎是有些問題的,它的重複代碼到處都是。無論是對代碼閱讀及後期維護來講,這都是一場噩夢。

所以我們需要進行一些重構,抽象能抽象的方法,讓冗繁的代碼變得更簡潔。

首先,我們對Report類進行功能上的抽象,生成報告並輸出一共可以分為兩個功能,一個只負責創建Report,一個只負責如何處理Report,那麼讓我們開始重構吧。  

class Report
{
   public function show(array $data)
   {
       echo $this->createReport($data);
   }
   public function saveToFile(array $data)
   {
       file_put_contents("./report.txt", $this->createReport($data));
   }
   private function createReport(array $data): string
   {
       $report = '';
       $report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
       $report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
       $report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       $report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       $report .= "Total: " . $data["total"] . "\n";
       $report .= "Average x day: " . floor($data["total"] / 365) . "\n";
       $report .= "Average x week: " . floor($data["total"] / 52) . "\n";
       $report .= "Average x month: " . floor($data["total"] / 12) . "\n";
       return $report;
   }
}

現在看起來更清楚一些,對嗎?

下面我們還有函數使用重複的問題要解決,例如,Report和Products的名稱函數使用重複:

$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
$report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";

我們可以將這些轉換抽象為一個新的函數:

private function normalizeName($name): string
{
   return ucwords(strtolower($name));
}

另一個重複:日期格式。

$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";

讓我們將其抽象為:

private function formatDate($date): string
{
   return date("Y/m/d", $date);
}

最後一個:平均值計算。

$report .= "Average x day: " . floor($data["total"] / 365) . "\n";
$report .= "Average x week: " . floor($data["total"] / 52) . "\n";
$report .= "Average x month: " . floor($data["total"] / 12) . "\n";

儘管計算結果並不完全相同,但執行的操作大家是一致的,所以可以抽象為如下:

private function calculateAverage(array $data, $period): string
{
   return floor($data["total"] / $period);
}

所以,經過了一番重構,最終的Report類變為了如下:

class Report
{
   public function show(array $data)
   {
       echo $this->createReport($data);
   }
   public function saveToFile(array $data)
   {
       file_put_contents("./report.txt", $this->createReport($data));
   }
   private function createReport(array $data)
   {
       $report = '';
       $report .= "Report: " . $this->normalizeName($data["name"]) . "\n";
       $report .= "Product: " . $this->normalizeName($data["product"]) . "\n";
       $report .= "Start date: " . $this->formatDate($data["startDate"]) . "\n";
       $report .= "End date: " . $this->formatDate($data["endDate"]) . "\n";
       $report .= "Total: " . $data["total"] . "\n";
      $report .= "Average x day: " . $this->calculateAverage($data, 365) . "\n";
      $report .= "Average x week: " . $this->calculateAverage($data, 52) . "\n";
      $report .= "Average x month: " . $this->calculateAverage($data, 12) . "\n";
      return $report;
  }
  private function formatDate($date): string
  {
       return date("Y/m/d", $date);
   }
  private function calculateAverage(array $data, $period): string
  {
      return floor($data["total"] / $period);
  }
  private function normalizeName($name): string
  {
      return ucwords(strtolower($name));
  }
}

這是一個簡單的例子,實際情況可能比這要更加複雜的多,但我僅想通過這個實例向大家說明一個問題,那就是避免重複代碼的重要性及我們如何通過重構去處理重複代碼。

有時候重複一次相同的代碼可能沒問題,但是當第三次​​我們寫出相同的代碼時,那就說明是時候重構你的代碼了。

結論:

請記住DRY原則,並隨時抱着不要重複自己代碼的想法去完成開發工作。

 

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

【Spring註解驅動開發】在@Import註解中使用ImportSelector接口導入bean

寫在前面

在上一篇關於Spring的@Import註解的文章《【Spring註解驅動開發】使用@Import註解給容器中快速導入一個組件》中,我們簡單介紹了如何使用@Import註解給容器中快速導入一個組件,而我們知道,@Import註解總共包含三種使用方法,分別為:直接填class數組方式;ImportSelector方法(重點);ImportBeanDefinitionRegistrar方式。那麼,今天,我們就一起來學習關於@Import註解非常重要的第二種方式:ImportSelector方式。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

ImportSelector接口概述

ImportSelector接口是至spring中導入外部配置的核心接口,在SpringBoot的自動化配置和@EnableXXX(功能性註解)都有它的存在。我們先來看一下ImportSelector接口的源碼,如下所示。

package org.springframework.context.annotation;

import java.util.function.Predicate;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;

public interface ImportSelector {
	String[] selectImports(AnnotationMetadata importingClassMetadata);
	@Nullable
	default Predicate<String> getExclusionFilter() {
		return null;
	}
}

該接口文檔上說的明明白白,其主要作用是收集需要導入的配置類,selectImports()方法的返回值就是我們向Spring容器中導入的類的全類名。如果該接口的實現類同時實現EnvironmentAware, BeanFactoryAware ,BeanClassLoaderAware或者ResourceLoaderAware,那麼在調用其selectImports方法之前先調用上述接口中對應的方法,如果需要在所有的@Configuration處理完在導入時可以實現DeferredImportSelector接口。

在ImportSelector接口的selectImports()方法中,存在一個AnnotationMetadata類型的參數,這個參數能夠獲取到當前標註@Import註解的類的所有註解信息。

注意:如果ImportSelector接口展開講的話,可以單獨寫一篇文章,那我就放在下一篇文章中講吧,這裏就不贅述了,嘿嘿。

ImportSelector接口實例

首先,我們創建一個MyImportSelector類實現ImportSelector接口,如下所示。

package io.mykit.spring.plugins.register.selector;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Import註解中使用ImportSelector
 *              自定義邏輯,返回需要導入的組件
 */
public class MyImportSelector implements ImportSelector {
    /**
     * 返回值為需要導入到容器中的bean的全類名數組
     * AnnotationMetadata:當前標註@Import註解的類的所有註解信息
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[0];
    }
}

接下來,我們在PersonConfig2類的@Import註解中,導入MyImportSelector類,如下所示。

@Configuration
@Import({Department.class, Employee.class, MyImportSelector.class})
public class PersonConfig2 {

至於使用MyImportSelector導入哪些bean,就需要在MyImportSelector類的selectImports()方法中進行設置了,只要在MyImportSelector類的selectImports()方法中返回要導入的類的全類名(包名+類名)即可。

我們繼承創建兩個Java bean對象,分別為User和Role,如下所示。

  • User類
package io.mykit.spring.plugins.register.bean;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試ImportSelector
 */
public class User {
}
  • Role類
package io.mykit.spring.plugins.register.bean;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試ImportSelector
 */
public class Role {
}

接下來,我們將User類和Role類的全類名返回到MyImportSelector類的selectImports()方法中,此時,MyImportSelector類的selectImports()方法如下所示。

/**
 * 返回值為需要導入到容器中的bean的全類名數組
 * AnnotationMetadata:當前標註@Import註解的類的所有註解信息
 */
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    return new String[]{
        User.class.getName(),
        Role.class.getName()
    };
}

接下來,我們運行SpringBeanTest類的testAnnotationConfig7()方法,輸出的結果信息如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
io.mykit.spring.plugins.register.bean.Employee
io.mykit.spring.plugins.register.bean.User
io.mykit.spring.plugins.register.bean.Role
person
binghe001

可以看到,輸出結果中多出了io.mykit.spring.plugins.register.bean.User和io.mykit.spring.plugins.register.bean.Role。

說明使用ImportSelector已經成功將User類和Role類導入到了Spring容器中。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

MySQL觸發器的詳細教學與實戰分析

所有知識體系文章,GitHub已收錄,歡迎老闆們前來Star!

GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual

MySQL觸發器

一、什麼是觸發器

觸發器(trigger)是MySQL提供給程序員和數據分析員來保證數據完整性的一種方法,它是與表事件相關的特殊的存儲過程,它的執行不是由程序調用,也不是手工啟動,而是由事件來觸發,比如當對一個表進行操作(insert,delete, update)時就會激活它執行。簡單理解為:你執行一條sql語句,這條sql語句的執行會自動去觸發執行其他的sql語句。

二、觸發器的作用

  • 可在寫入數據表前,強制檢驗或轉換數據。
  • 觸發器發生錯誤時,異動的結果會被撤銷。
  • 部分數據庫管理系統可以針對數據定義語言(DDL)使用觸發器,稱為DDL觸發器。
  • 可依照特定的情況,替換異動的指令 (INSTEAD OF)。

三、觸發器創建的四要素

  • 監視地點(table)
  • 監視事件(insert、update、delete)
  • 觸發時間(after、before)
  • 觸發事件(insert、update、delete)

四、觸發器的使用語法

語法:

before/after: 觸發器是在增刪改之前執行,還是之後執行

delete/insert/update: 觸發器由哪些行為觸發(增、刪、改)

on 表名: 觸發器監視哪張表的(增、刪、改)操作

觸發SQL代碼塊: 執行觸發器包含的SQL語句

1CREATE TRIGGER 觸發器名
2BEFORE|AFTER DELETE|INSERT|UPDATE
3ON 表名 FOR EACH ROW
4BEGIN
5觸發SQL代碼塊;
6END;

注意: 觸發器也是存儲過程程序的一種,而觸發器內部的執行SQL語句是可以多行操作的,所以在MySQL的存儲過程程序中,要定義結束符。

如果MySQL存儲過程不了解的小夥伴,可以參考此文面向MySQL存儲過程編程,文章中詳細講解了MySQL存儲過程的優勢和語法等等,相信你會在其中得以收穫。

1# 設置MySQL執行結束標誌,默認為;
2delimiter //

五、觸發器的基本使用

5.1 基本使用步驟

首先,我先展示一下創建的兩張表,因為創建的表很簡單,這裏我沒有提供庫表操作的SQL命令。

tb_class

image-20200611205404311

employee

image-20200611205435284

其次,創建了一個含有update操作的存儲過程

1delimiter //
2create procedure update_emp(in i intin p int)
3begin
4    update employee set phone = p where id = i;
5end //

再創建一個觸發器

分析: 觸發器名稱為t1,觸發時間為after,監視動作為update,監視表為employee表。匯總一起解釋這個觸發器就是:創建一個觸發器名稱為t1的觸發器,觸發器監視employee表執行update(更新)操作后,就開始執行觸發器內部SQL語句update tb_class set num = num + 1 where id = 1;

簡單來說就是一個監視一個表的增、刪、改操作並設置操作前後時間,在設置時間的範圍內對另外一個表進行其他操作。

如果你學到這裏還是一知半解,後面我會講解一個訂單與庫存的數據關係,到那時候你就會明白了!

 1delimiter //
2# 創建觸發器,觸發器名稱為t1
3create trigger t1
4    # 觸發器執行在update操作之後
5    after update
6    # 監視employee表
7    on employee
8    for each row
9begin
10    # 觸發執行的SQL語句
11    update tb_class set num = num + 1 where id = 1;
12end //

最後調用函數,並查看、分析結果

1call update_emp(2110);

觸發器在此場景的作用分析

當employee表發生update操作時,觸發器就對tb_class表中的num值做修改。

執行結果發現,我們在使用函數將employee表中id為2員工的phone修改為110后,觸發器監視到employee表中發生了update更新操作,就執行了內部SQL語句,也就是將tb_class表中id為1的num值自增1。

image-20200611213411229 image-20200611213432459

5.2 查看和刪除已有的觸發器

查看已有觸發器: show triggers

刪除已有觸發器: drop trigger 觸發器名稱

5.3 for each row

這裏擴展,在oracle觸發器中,觸發器分為行觸發器和語句觸發器。也就是說,假設你監視一個修改操作,它修改了1000行代碼,在Oracle中觸發器會觸發1000次。

在oracle中,for each row如果不寫,無論update語句一次影響了多少行,都只執行一次觸發事件。

而MySQL中,不支持語句級觸發器,所以在MySQL中並不需要在意。

六、訂單與庫存關係場景

訂單與庫存的關係: 用戶下訂單,意味着創建該商品訂單,該商品訂單中的商品數量為1,庫存中的該商品數量-1。往往訂單表和庫存表中的數量是同時操作的,所以我們這裏可以用觸發器。

觸發器應用: 關於訂單表,下訂單肯定是涉及到insert插入數據數量的操作。我們可以創建一個監視訂單表insert操作后執行庫存表數量-1的觸發器來完成訂單與庫存表的同時修改。

創建表,並在表中添加幾條數據:

 1create table goods(
2  gid int,
3  name varchar(20),
4  num smallint
5);
6create table ord(
7  oid int,
8  gid int,
9  much smallint
10);
11insert into goods values(1,'cat',40);
12insert into goods values(2,'dog',63);
13insert into goods values(3,'pig',87);

創建觸發器

1create trigger t1 
2after
3insert
4on ord
5for each row
6begin
7 update goods set num = num - 1 where gid = 1;
8end$

該觸發器意為,用戶不管下什麼訂單,都會把商品編號為1的商品的庫存減去1。

七、觸發器中引用行變量

7.1 old和new對象語法

  • 在觸發目標上執行insert操作後會有一個新行,如果在觸發事件中需要用到這個新行的變量,可以用new關鍵字表示
  • 在觸發目標上執行delete操作後會有一箇舊行,如果在觸發事件中需要用到這箇舊行的變量,可以用old關鍵字表示
  • 在觸發目標上執行update操作后原紀錄是舊行,新記錄是新行,可以使用new和old關鍵字來分別操作
觸發語句 old new
insert 所有字段都為空 將要插入的數據
update 更新以前該行的值 更新后的值
delete 刪除以前該行的值 所有字段都為空

7.2 old和new對象應用

關於old和new對象的應用,我在這裏沒有展開演示。只是將第八章的綜合案例結合了old和new對象實現。綜合案例中詳細講解了MySQL觸發器的使用!

八、綜合案例

8.1 創建表、插入表數據

tb_class為幼兒園班級表,其中cid為唯一主鍵,cname為大、中、小班班級標準,stuNo為班級標準內的學生個數。插入大、中、小班標準,初始化兩名學生在大班。

tb_stu為幼兒園學生表,其中sid為唯一主鍵,sname為學生性名,cno為所在班級標準的外鍵。插入兩條數據並初始化這兩名學生在大班,因為我們在班級表中初始化了兩名學生在大班嘛,所以要做此操作。

 1create table tb_class
2(
3    cid   int auto_increment
4        primary key,
5    cname varchar(32not null,
6    stuNo int         not null
7);
8
9INSERT INTO temp.tb_class (cname, stuNo) VALUES ('大班'2)
10INSERT INTO temp.tb_class (cname, stuNo) VALUES ('中班'0)
11INSERT INTO temp.tb_class (cname, stuNo) VALUES ('小班'0)
12
13create table tb_stu
14(
15    sid   int auto_increment
16        primary key,
17    sname varchar(32not null,
18    cno   int         not null
19);
20
21INSERT INTO temp.tb_stu (sname, cno) VALUES ('Ziph'1)
22INSERT INTO temp.tb_stu (sname, cno) VALUES ('Join'1)

8.2 添加學生案例

在此表結構中,如果一位新同學來到學校學習,意味着某一個班級中會多出一名學生。假設Marry同學去小班學習,其表結構的變化為:tb_stu表中添加一條Marry的記錄(注:cno = 3),tb_class表中小班記錄的stuNo = 0修改為stuNo = 1

先創建一個添加學生的存儲過程

1# 添加學生函數
2delimiter //
3# 創建存儲過程,傳入學生性名和班級參數
4create procedure add_stu(in in_sname varchar(32), in in_cno int)
5begin
6    # 插入記錄
7    insert into tb_stu (sname, cno) values (in_sname, in_cno);
8end //

創建觸發器

注意: 在更新學生數量SQL語句中,有一段cid = new.cno的SQL語句。這裏我解釋一下,new代表產生的新對象,將cid主鍵與添加Marry記錄后產生的新紀錄對象的cno外鍵關聯。(因為insert后產生的是新紀錄對象嘛,所以用new)

 1# 觸發器
2# 創建名稱為t_add_stu的觸發器
3create trigger t_add_stu
4    # 設置在insert操作之後觸發
5    after
6        insert
7    # 監視tb_stu的insert操作
8    on tb_stu
9    for each row
10begin
11    # 更新學生數量(cid為tb_class表中主鍵,cno為tb_stu表中外鍵)
12    update tb_class set stuNo = stuNo + 1 where cid = new.cno;
13end //

聲明回結束符

1delimiter ;

插入Marry學生記錄到數據庫表中

1call add_stu('Marry'3);

執行結果就是當插入Marry學生記錄的同時也修改了班級表中的小班學生數量。

8.3 刪除學生案例

刪除學生與添加學生十分相似,刪除學生相當於是添加學生的逆過程。如果以為學生退學了或者讀完了幼兒園離開學校了,就意味着班級中少了一位學生。假設Join同學讀完了大班結束了幼兒園階段的學習將要幼兒園去上小學,其表結構變化為:tb_stu刪除Join這條記錄(注:sid = 2),tb_class將修改Join所在大班班級級別的stuNo,即stuNo = stuNo – 1

先創建一個刪除學生的存儲過程

1# 刪除學生
2delimiter //
3create procedure delete_stu(in in_sid int)
4begin
5    delete from tb_stu where sid = in_sid;
6end //

創建觸發器

注意: 在更新學生數量的時候,書寫了此段SQL語句cid = OLD.cno。該語句使用old對象,意為Join學生的記錄沒有了,但是使用觸發器同步修改tb_class表中的大班學生數量還需要用到關聯Join學生所在記錄的外鍵cno,使用old來句點出來的cno就是刪除之前Join那一條學生記錄的cno。(如果我們用new,該記錄還存在嗎?該記錄的cno還存在嗎?答案是都不存在了!)

 1# 觸發器
2# 創建觸發器名稱為t_delete_stu的觸發器
3create trigger t_delete_stu
4    # 設置在delete操作之後觸發
5    after
6        delete
7    # 監視tb_stu表的delete操作
8    on tb_stu
9    for each row
10begin
11    # 更新學生數量(cid為tb_class表中主鍵,cno為tb_stu表中外鍵)
12    update tb_class set stuNo = stuNo - 1 where cid = OLD.cno;
13end //

聲明回結束符

1delimiter ;

刪除Jion學生記錄

1call delete_stu(2);

執行結果為Join記錄在數據庫的表中消失了,而大班的學生數量也減掉了1。

8.4 刪除班級案例

因為我已經詳細講解了添加學生與刪除學生,所以刪除班級我就不再作過多的贅述了。那就直接說核心內容吧。刪除一個班級級別比如:刪除小班之前要把小班內的所有學生也被刪除了,因為兩個表是主外鍵關聯的。如果只刪除了小班,而沒有刪除小班內的所有學生,那麼原小班內的所有學生現在屬於哪個班級呢,就不知道了吧!所以要在刪除小班之前刪除小班內的所有學生。

 1# 創建刪除班級的存儲過程
2delimiter //
3create procedure delete_class(in in_cid int)
4begin
5    delete from tb_class where cid = in_cid;
6end //
7
8# 創建觸發器名稱為t_delete_class的觸發器
9create trigger t_delete_class
10    # 作用在delete操作之前
11    before
12        delete
13    # 監視tb_class表中的delete操作
14    on tb_class
15    for each row
16begin
17    # 同時刪除所有該原班級cid的所有學生
18    delete from tb_stu where cno = OLD.cid;
19end //
20
21# 將結束符聲明為;
22delimiter ;
23
24# 刪除小班班級別
25call delete_class(3);

執行結果為既刪除了小班,又刪除小班內的所有學生。

8.5 觸發器衝突問題

觸發器衝突問題其實就是關聯問題。為什麼這麼說呢?就說以下剛才這三個案例中出現的觸發器衝突問題。

如果我們在寫觸發器的時候,將添加學生、刪除學生和刪除班級的觸發器都寫在一個查詢模板中。你會發現當你在刪除班級的時候,會報錯。显示如下信息:

image-20200612004546204

這是為什麼呢?

仔細想想,我們將在案例中有兩個是同一個表中的刪除觸發器。刪除班級的觸發器中定義的是刪除班級時觸發刪除學生,而刪除學生的觸發器中定義的是班級人數減一。你發現了沒,觸發器被連着觸發了。如下變化:

image-20200612005312835

我們通過刪除班級案例了解了,刪除班級之前需要把班級內所有學生刪除掉。正因為如此,我們在刪除班級之前已經把所有學生都刪除了,導致在刪除學生的時候觸發了班級人數減一的觸發器,該觸發器在執行過程中修改了已經被刪除班級的學生人數。這問題就出在這裏了,班級已經刪除了,怎麼修改一個本就沒有的班級內的人數呢?對吧!

解決觸發器衝突

為解決這個場景的觸發器衝突問題,我們只能取捨一個觸發器。於是,就通過命令刪除了刪除學生案例中使用的那個觸發器,刪除后刪除班級就可以成功執行觸發了!

1drop trigger t_delete_stu;

注意: 由於存在觸發器衝突問題,我們在實際開發中需要認真考量定義觸發器!

九、觸發器性能和使用分析(必讀)

各大論壇等等,相信在大家的文章中都不推薦使用觸發器,而是推薦使用存儲過程程序,這是為什麼呢?

首先,存儲過程程序分為存儲過程、儲存過程函數和觸發器。也就是說這三種都是存儲過程的使用都是存儲過程的表現形式。

如果場景在數據量和併發量都很大的情況下,使用觸發器、存儲過程再加上幾個事務等等,很容易出現死鎖。而且在使用觸發器的時候,也會出現衝突,出現問題時,我們需要追溯的代碼就需要從一個觸發器到另一個觸發器……從而影響開發效率。從性能上看,觸發器也是存儲過程程序的一種,它也並沒有展現出多少性能上的優勢。由於觸發器寫起來比較隱蔽,容易被開發人員忽略,而且隱式調用觸發器不易於排除依賴,對後期維護不是很友好!

所以在開發中,觸發器是很少用到的。那為什麼我還花時間大篇幅的講解MySQL觸發器呢?原因很簡單,是因為需要擴展自己的知識儲備。開發中的使用問題和是否被大家摒棄,不是你拒絕學習知識的理由。之所以存在就有它存在的道理,我們在學習的道路中不斷擴充自己的知識儲備即可。

假如有一天你的同事聊起觸發器,你也能和他們聊聊你對觸發器的見解是哈?如果你根據從未了解過此知識呢?那性質就不一樣了,相信大家都懂吧!

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

日本援助隊稱觸礁貨輪或導致模里西斯珊瑚死亡

摘錄自2020年8月25日共同網報導

在模里西斯近海發生日本貨輪燃油洩漏事故,在當地的日本國際緊急援助隊25日舉行記者會,稱觸礁的船體後半部分隨波浪擺動,把下方的珊瑚削成細顆粒狀,有可能因此使海水變渾濁,導致其他地方珊瑚死亡。

船體斷為兩截,前半部分已在24日被沉入深海。緊急援助隊強調有必要迅速撤走後半部分,但模里西斯當局難以撈起船體,已決定到11月1日前在事故現場進行解體。

緊急援助隊表示,通常珊瑚礁淺灘的水中能見度約30公尺,但模里西斯近海一些地方的能見度降至約3公尺。據稱,還發生防止油污漂流的圍油欄損傷珊瑚的情況;燃油還漂流到了紅樹林中,地表的油污可能會因人的踩踏而進入土壤中,有必要慎重開展去除油污的工作。

污染治理
國際新聞
模里西斯
珊瑚
油污攔阻作業
漏油污染
紅樹林

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

用社群媒體對抗珊瑚白化 菲律賓鼓勵潛客拍照上傳App 民間自發響應監測

環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

助世界農業遺產「水山葵」度武肺危機 日本食材宅配公司推地產地銷

文:宋瑞文

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

捕獵野生動物充飢! 誤殺烏干達明星大猩猩被判11年

摘錄自2020年8月26日民視新聞報導

非洲烏干達保育區的25歲明星大猩猩「拉飛奇」,六月被盜獵者射殺,7月1日保育員發現牠失蹤,搜救隊隔天就發現牠的遺體。驗屍發現,牠腹部和內臟有遭利器穿刺的痕跡。

嫌犯自稱是因為武漢肺炎觀光收入銳減、沒錢買食物吃,盜獵小動物的時候以為猩猩要攻擊,出於自衛以長茅殺死大猩猩。警方發現他已獵殺一隻羚羊,麂羚和野豬,住處還發現多種野生動物的肉,被判處11年有期徒刑,成為該國首例。

大猩猩屬瀕危動物,剛果,烏干達和盧安達保護區內僅剩約1000隻,去年光是大猩猩觀光收入就多達2550萬美金。今年97%行程因疫情邊境封鎖而取消,多數業者不堪負荷倒閉。

生物多樣性
國際新聞
烏干達
大猩猩
盜獵
武漢肺炎
動物與大環境變遷

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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