ASP.NET Core gRPC 使用 Consul 服務註冊發現

一. 前言

gRPC 在當前最常見的應用就是在微服務場景中,所以不可避免的會有服務註冊與發現問題,我們使用gRPC實現的服務可以使用 Consul 或者 etcd 作為服務註冊與發現中心,本文主要介紹Consul。

二. Consul 介紹

Consul是一種服務網絡解決方案,可跨任何運行平台以及公共或私有雲來連接和保護服務。它可以讓你發現服務並保護網絡流量。它可以在Kubernetes中使用,實現服務發現和服務網格功能(k8s默認etcd)。 提供安全服務通訊,保護和觀察服務之間的通信,而無需修改其代碼。 提供動態負載平衡, 使用Consul和HAProxy,Nginx或F5自動執行負載均衡器配置。 Consul 可以用於服務發現和服務網格。

翻譯自官網

三. Consul 安裝配置

安裝

Consul 下載地址:

根據自己的系統來選擇,我這裏選擇的是 Windows 版本的,直接解壓即可運行。

啟動

consul agent -dev -ui

本文不詳細介紹Consul使用,如需請自行查看相關資料

四. .NET Core Consul 客戶端的選擇

Consul 提供了 HTTP API 的方式來進行通訊,我們可以直接調用API或者是使用第三方封裝好的客戶端組件,通過Nuget搜索可以發現許多。

這裏面我沒有一一測試,但是目前使用量最多的 Consul 組件是不支持設置 GRPC 健康檢查的,而且 github 也停止了更新。

所以我 Fork 了這個倉庫,然後添加了 GRPC 的健康檢查支持,本文也將使用這個庫,歡迎大家使用:

因為原倉庫已經 Archived 了,所以我才 Fork 了自己改一下,改動很小,不影響原來的穩定性。

Nuget:

Github:

求個star

關於支持 GPRC 健康檢查的好處:

偷個懶,不翻譯了,摘自GRPC官方文檔

五. 註冊GRPC服務與健康檢查

基於前文()的Demo

1.為服務端項目安裝 NConsul.AspNetCore ( )

這裏面對 AspNetCore 做了適配,使用簡單。

2.在 Startup 的 ConfigureServices方法內進行配置

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();

    services.AddConsul("http://localhost:8500")
        .AddGRPCHealthCheck("localhost:5000")
        .RegisterService("grpctest","localhost",5000,new []{"xc/grpc/test"});
}

AddConsul 添加 Consul Server 地址。

AddGRPCHealthCheck 添加 GRPC 健康檢查,即健康檢查走的是 GRPC 協議,該值為 GRPC 服務的地址,不需要path不需要提供 http/https

RegisterService 註冊服務

到這步,還不能啟動運行,如果運行健康檢查是會失敗的。

3.編寫 Health Check 服務 **

對於 GRPC 的健康檢查,官方有標準的定義,新建一個 proto 文件,命名為 HealthCheck.proto

syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
    string service = 1;
}

message HealthCheckResponse {
    enum ServingStatus {
        UNKNOWN = 0;
        SERVING = 1;
        NOT_SERVING = 2;
    }
    ServingStatus status = 1;
}

service Health {
    rpc Check(HealthCheckRequest) returns (HealthCheckResponse);

    rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

這裏面的內容不得更改,是官方標準,資料見後文

這裏編譯一下項目,以便自動生成代碼。

然後,添加一個服務的實現類 HealthCheckService

public class HealthCheckService:Health.HealthBase
{
    public override Task<HealthCheckResponse> Check(HealthCheckRequest request, ServerCallContext context)
    {
        //TODO:檢查邏輯
        return Task.FromResult(new HealthCheckResponse(){Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }

    public override async Task Watch(HealthCheckRequest request, IServerStreamWriter<HealthCheckResponse> responseStream, ServerCallContext context)
    {
        //TODO:檢查邏輯
        await responseStream.WriteAsync(new HealthCheckResponse()
            {Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }
}

示例代碼直接返回了檢查結果,實際使用中應該在這裏編寫檢查邏輯,然後根據情況返回相應的檢查結果。檢查結果有3種情況:

結果類型 說明
Unknown 未知狀態
Serving 正常
NotServing 異常,不能提供服務

最後別忘了註冊服務:

4.測試運行

啟動 GRPC 服務

然後訪問 http://localhost:8500/ui 訪問 Consul 控制台

可以看到服務成功註冊,並且健康檢查也是通過了的。通過控制台日誌,還可以看到健康檢查的請求:

六. 客戶端使用服務發現

客戶端項目安裝 Consul 組件,然後改造下代碼:

static async Task Main(string[] args)
{
    var serviceName = "grpctest";
    var consulClient = new ConsulClient(c => c.Address = new Uri("http://localhost:8500"));
    var services = await consulClient.Catalog.Service(serviceName);
    if (services.Response.Length == 0)
    {
        throw new Exception($"未發現服務 {serviceName}");
    }

    var service = services.Response[0];
    var address = $"http://{service.ServiceAddress}:{service.ServicePort}";

    Console.WriteLine($"獲取服務地址成功:{address}");

    //啟用通過http使用http2.0
    AppContext.SetSwitch(
        "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    var channel = GrpcChannel.ForAddress(address);
    var catClient = new LuCat.LuCatClient(channel);
    var catReply = await catClient.SuckingCatAsync(new Empty());
    Console.WriteLine("調用擼貓服務:"+ catReply.Message);
    Console.ReadKey();
}

通過服務名稱獲取服務地址,然後來進行訪問。

運行測試:

可以看到,成功的從Consul獲取了我們的服務地址,然後調用。

六. 參考資料

  • gRPC in Asp.Net Core :

  • GPRC Health Check Doc:

  • 本文 Demo:

  • 本系列文章目錄:

  • NConsul:

  • by Edison Zhou

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

MySQL權限管理

目錄

設置用戶與權限

一個MySQL系統可能有許多用戶。為了安全起見,root用戶通常只用管理目的。對於每個需要使用該系統的用戶,應該為他們創建一個賬號和密碼。這些用戶名和密碼不必與MySQL之外的用戶名稱和密碼(例如,Linux或NT用戶名和密碼)相同。同樣的原則也適合於root用戶。對於系統用戶和MySQL用戶最好使用不同的密碼,這一點對root用戶尤其應該這樣。
為用戶設置密碼不是必須的,但是強烈建議為所有創建的用戶設定密碼。要建立一個Web數據庫,最好為每個網站應用程序建立一個用戶。你可能會問,“為什麼要這麼做呢?”—-答案在於權限。

用戶管理

我們的MySQL的用戶,都被記錄在了mysql數據庫的user表中,如下圖:

在MySQL數據庫中,一個完整的用戶賬號應該包含用戶名登錄主機,也就是說 用戶名@主機名才是一個完整的用戶賬號。

創建用戶基本語法:

CREATE USER 用戶名@主機名 IDENTIFIED BY '密碼';

刪除用戶基本語法:

DROP USER 用戶名@主機名;

修改用戶密碼:

- 修改自己的密碼
set password = password('新密碼');
- 修改別人的密碼(需要有該權限)
set password for 用戶名@主機名 = password('新密碼');

MySQL權限系統介紹

MySQL的最好特性之一是支持複雜的權限系統權限是對特定對象執行特定操作的權力,它與特定用戶相關。其概念非常類似於文件的權限。擋在MySQL中創建一個用戶時,就賦予了該用戶一定的權限,這些權限指定了用戶在本系統中可以做什麼和不可以做什麼。

最少權限原則

最少權限原則可以用來提高任何計算機系統的安全性。它是一個基本的、但又是非常重要的而且容易為我們忽略的原則。該原則包含如下內容:

一個用戶(或一個進程)應該擁有能夠執行分配給他的任務的最低級別的權限。

該原則同樣適用於MySQL,就像它應用於其他地方一樣。例如,要在網站上運行查詢,用戶並不需要root用戶所擁有的所有權限。因此,我們應該創建另一個用戶,這個用戶只有訪問我們剛剛建立的數據庫的必要權限。

創建用戶:GRANT命令

GRANT和REVOKE命令分別用來授予取消MySQL用戶的權限,這些權限分4個級別。它們分別是:

  • 全局
  • 數據庫

GRANT命令用來創建用戶並賦予他們權限。GRANT命令的常見形式是:

GRANT privileges [columns]
ON item
TO user_name [IDENTIFIED BY 'password']
[REQUIRE ssl_options]
[WITH [GRANT OPTION | limit_options]]

普通寫法可以簡略如下:

GRANT 權限列表 ON 數據庫.表 TO 用戶名@主機名 [IDENTIFIED BY '密碼'];

identified by可以省略,也可以寫出
(1)如果寫了,用戶存在,就是修改用戶的密碼
(2)如果寫了,該用戶不存在,就是創建用戶,同時指定密碼

方括號內的子句是可選的。在本語法中,出現了許多佔位符。第一個佔位符是 privileges,應該是由逗號分開的一組權限。MySQL已經有一組已定義的權限。它們在下一節詳細介紹。
佔位符 columns是可選的。可以用它對每一個列指定權限。也可以使用單列的名稱或者逗號分開的一組列的名稱。

佔位符 item是新權限的所應用於的數據庫或表。可以將項目指定為“.”,而將權限應用於所有數據庫。這叫做賦予全局權限。如果沒有使用在特定的數據庫,也可以通過只指定*完成賦予全局權限。更常見的是,以dbname.*的形式指定數據庫中所有的表,以dbname.tablename的形式指定單個表,或者通過指定tablename來指定特定的列。這些分別表示其他3個可以利用的權限:數據庫、表、列。如果在輸入命令的時候正在使用一個數據庫,tablename本身將被解釋成當前數據庫中的一個表。

user_name應該是用戶登錄MySQL所使用的用戶名。請注意,它不必與登錄系統時所使用的用戶名相同。MySQL中的user_name也可以包含一個主機名。可以用它來區分如itbsl(解釋成itbsl@localhost)和itbsl@somewhere.com。這是非常有用的一項能力,因為來自不同域的用戶經常可能使用同一個名字。這也提高了安全性能,因為可以指定用戶從什麼地方連接到本機,甚至可以指定它們在特定的地方可以訪問那些表和數據庫。

password應該是用戶登錄時使用的密碼。常見的密碼選擇規則在這裏都適用。我們後面將更詳細地講述安全問題,但是密碼應該不容易被猜出來。這意味着,密碼不應該是一個字段單詞或與用戶名相同。理想的密碼應該是大、小寫字母和非字母的組合。

REQUIRE子句允許指定用戶是否必須通過加密套接字連接,或者指定其它的SSL選項,關於SSL到MySQL連接的更多信息,請參閱MySQL手冊。

WITH GRANT OPTION選項,如果指定,表示允許指定的用戶向別人授予自己所擁有的權限。
我們也可以指定如下所示的WITH子句:
MAX_QUERIES_PER_HOUR n
或者
MAX_UPDATES_PER_HOUR n
或者
MAX_CONNECTIONS_PER_HOUR n
這些子句可以指定每個用戶每小時執行的查詢、更新和鏈接的數量。在共享的系統上限制單個用戶的負載時,這些子句是非常有用的。
權限存儲在名為mysql的數據庫中的5個系統中。這些表分別是mysql.user、mysql.db、mysql.host、mysql.tables_priv和mysql.columns_priv。作為GRANT命令的替代,可以直接修改這些表。

權限的類型和級別

MySQL中存在3個基本類型的權限:適用於賦予一般用戶的權限、適用於賦予管理員的權限和幾個特定的權限。任何用戶都可以被賦予這3類權限,但是根據最少權限原則,最好嚴格限定只將管理員類型的權限賦予管理員。

我們應該只賦予用戶訪問他們必須使用的數據庫和表的權限。而不應該將訪問mysql的權限賦予不是管理員的人。mysql數據庫是所有用戶名、密碼等信息存儲的地方。

常規用戶的權限直接與特定的SQL命令類型以及用戶是否被允許運行它們相關。下錶所示的是基本用戶權限。“應用於”列下面的對象給出了該類型權限可以授予的對象。

用戶的權限

權限 應用於 描述
SELECT 表、列 允許用戶從表中查詢行(記錄)
INSERT 表、列 允許用戶在表中插入新行
UPDATE 表、列 允許用戶修改現存表裡行中的值
DELETE 允許用戶刪除現存表的行
INDEX 允許用戶創建和拖動特定表索引
ALTER 允許用戶改變現存表的結構,例如,可添加列、重命名列或表、修改列的數據類型
CREATE 數據庫、表 允許用戶創建新數據庫或表。如果在GRANT中指定了一個特定的數據庫或表,它們只能夠創建該數據庫或表,即它們必須首先刪除(drop)它
DROP 數據庫、表 允許用戶拖動(刪除)數據庫或表

從系統的安全性方面考慮,適於常規用戶的權限大多數是相對無害的。ALTER權限通過重命名表可能會影響權限系統,但是大多數用戶需要它。安全性常常是可用性與保險性的折中。遇到ALTER的時候,應當做出自己的選擇,但是通常還是會將這個權限授予用戶。

下面的表給出了適用於管理員用戶使用的權限。
可以將這些權限授予非管理員用戶,這樣做的時候要非常小心。
FILE權限有些不同,它對普通用戶非常有用,因為它可以將數據從文件載入數據庫,從而可以節省許多時間,否則,每次將數據輸入數據庫都需要重新輸入,這很浪費時間。
然而,文件載入可以用來載入MySQL可識別的任何文件,包括屬於其他用戶的數據庫和潛在的密碼文件。授予該權限的時候需要小心,或者自己為用戶載入數據。

管理員權限

權限 描述
CREATE TEMPORARY TABLES 允許管理員在CREATE TABLE語句中使用TEMPORARY關鍵字
FILE 允許將數據從文件讀入表,或從表讀入文件
LOCK TABLES 允許使用LOCK TABLES語句
PROCESS 允許管理員查看屬於所有用戶的服務器進程
RELOAD 允許管理員重新載入授權表、清空授權、主機、日誌和表
REPLICATION CLIENT 允許管理員重新載入授權表、和從機(Slave)上使用SHOW STATUS
REPLICATION SLAVE 允許複製從服務器連接到主服務器
SHOW DATABASES 允許使用SHOW DATABASES語句查看所有數據庫列表。沒有這個權限,用戶只能看到他們能夠看到的數據庫
SHUTDOWN 允許管理員關閉MySQL服務器
SUPER 允許管理員關閉屬於任何用戶的的線程

特別的權限

權限 描述
ALL 授予上面兩個表列表的所有權限。也可以將ALL寫成ALL PRIVILEGES
USAGE 不授予權限。這創建一個用戶並允許他登錄,但是不允許進行任何操作。通常會在以後授予該用戶更多的權限

REVOKE命令

與GRANT相反的命令是REVOKE。它用來從一個用戶收回權限。在語法上與GRANT非常相似:

REVOKE privileges [(columns)]
ON item
FROM user_name

中文翻譯:

REVOKE 權限列表 ON 數據庫.對象 FROM 用戶名@主機名;

如果已經給出了WITH GRANT OPTION子句,可以按如下方式撤銷它(以及所有其他權限):

REVOKE ALL PRIVILEGES, GRANT
FROM user_name

權限立即生效

當我們修改用戶權限之後,如果想不重啟立即生效,需要執行以下flush privileges,這樣能快速刷新權限

FULSH PRIVILEGES;

使用GRANT和REVOKE的例子

要創建一個管理員,可以輸入如下所示的命令:

grant all on * to fred identified by 'mnb123' with grant option;

以上命令授予了用戶名為fred、密碼為mnb123的用戶使用所有數據庫的所有權限,並允許他向其他人授予這些權限。

如果不希望用戶在系統中存在,可以按如下方式撤銷:

revoke all privileges, grant from red;

現在,我們可以按如下方式創建一個沒有任何權限的常規用戶:

grant usage on books.* to sally identified by 'magic123';

在與sally交談后,我們對她需要進行的操作有了一定的了解,因此按如下方式可以授予她適當的權限:

grant select, insert, update, delete, index, alter, create, drop on books.* to sally;

請注意,要完成這些,並不需要指定sally密碼。

如果我們認為sally權限過高,可能會決定按如下方式減少一些權限:

revoke alter, create, drop on books.* from sally;

後來,當她不再需要使用數據庫時,可以按如下方式撤銷所有的權限:

revoke all on books.* from sally;

參考資料:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Spring Boot (一) 校驗表單重複提交

一、前言

在某些情況下,由於網速慢,用戶操作有誤(連續點擊兩下提交按鈕),頁面卡頓等原因,可能會出現表單數據重複提交造成數據庫保存多條重複數據。

存在如上問題可以交給前端解決,判斷多長時間內不能再次點擊保存按鈕,當然,如果存在聰明的用戶能夠繞過前端驗證,後端更應該去進行攔截處理,下面小編將基於SpringBoot 2.1.8.RELEASE環境通過AOP切面+ 自定義校驗註解+ Redis緩存來解決這一問題。

二、Spring Boot 校驗表單重複提交操作

1、pom.xml中引入所需依賴

<!-- ==================  校驗表單重複提交所需依賴 ===================== -->
<!-- AOP依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、application.yml中引入Redis配置

spring:
  redis:
    # Redis數據庫索引(默認為0)
    database: 0
    # Redis服務器地址
    host: 127.0.0.1
    # Redis服務器連接端口
    port: 6379
    timeout: 6000
    # Redis服務器連接密碼(默認為空)
    #      password:
    jedis:
      pool:
        max-active: 1000  # 連接池最大連接數(使用負值表示沒有限制)
        max-wait: -1      # 連接池最大阻塞等待時間(使用負值表示沒有限制)
        max-idle: 10      # 連接池中的最大空閑連接
        min-idle: 5       # 連接池中的最小空閑連接

3、自定義註解 @NoRepeatSubmit

// 作用到方法上
@Target(ElementType.METHOD)
// 運行時有效
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
    /**
     * 默認時間3秒
     */
    int time() default 3 * 1000;
}

4、AOP 攔截處理

注:這裏redis存儲的key值可由個人具體業務靈活發揮,這裏只是示例
ex:單用戶登錄情況下可以組合token + url請求路徑,多個用戶可以同時登錄的話,可以再加上ip地址

@Slf4j
@Aspect
@Component
public class NoRepeatSubmitAop {

    @Autowired
    RedisUtil redisUtil;

    /**
     * <p> 【環繞通知】 用於攔截指定方法,判斷用戶表單保存操作是否屬於重複提交 <p>
     *
     *      定義切入點表達式: execution(public * (…))
     *      表達式解釋: execution:主體    public:可省略   *:標識方法的任意返回值  任意包+類+方法(…) 任意參數
     *
     *      com.zhengqing.demo.modules.*.api : 標識AOP所切服務的包名,即需要進行橫切的業務類
     *      .*Controller : 標識類名,*即所有類
     *      .*(..) : 標識任何方法名,括號表示參數,兩個點表示任何參數類型
     *
     * @param pjp:切入點對象
     * @param noRepeatSubmit:自定義的註解對象
     * @return: java.lang.Object
     */
    @Around("execution(* com.zhengqing.demo.modules.*.api.*Controller.*(..)) && @annotation(noRepeatSubmit)")
    public Object doAround(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

            // 拿到ip地址、請求路徑、token
            String ip = IpUtils.getIpAdrress(request);
            String url = request.getRequestURL().toString();
            String token = request.getHeader(Constants.REQUEST_HEADERS_TOKEN);

            // 現在時間
            long now = System.currentTimeMillis();

            // 自定義key值方式
            String key = "REQUEST_FORM_" + ip;
            if (redisUtil.hasKey(key)) {
                // 上次表單提交時間
                long lastTime = Long.parseLong(redisUtil.get(key));
                // 如果現在距離上次提交時間小於設置的默認時間 則 判斷為重複提交  否則 正常提交 -> 進入業務處理
                if ((now - lastTime) > noRepeatSubmit.time()) {
                    // 非重複提交操作 - 重新記錄操作時間
                    redisUtil.set(key, String.valueOf(now));
                    // 進入處理業務
                    ApiResult result = (ApiResult) pjp.proceed();
                    return result;
                } else {
                    return ApiResult.fail("請勿重複提交!");
                }
            } else {
                // 這裡是第一次操作
                redisUtil.set(key, String.valueOf(now));
                ApiResult result = (ApiResult) pjp.proceed();
                return result;
            }
        } catch (Throwable e) {
            log.error("校驗表單重複提交時異常: {}", e.getMessage());
            return ApiResult.fail("校驗表單重複提交時異常!");
        }

    }

}

5、其中用到的Redis工具類

由於太多,這裏就不直接貼出來了,可參考文末給出的案例demo源碼

三、測試

在需要校驗的方法上加上自定義的校驗註解@NoRepeatSubmit即可

@RestController
public class IndexController extends BaseController {

    @NoRepeatSubmit
    @GetMapping(value = "/index", produces = "application/json;charset=utf-8")
    public ApiResult index() {
        return ApiResult.ok("Hello World ~ ");
    }

}

這裏重複訪問此indexapi請求以模擬提交表單測試

第一次訪問

多次刷新此請求,則提示請勿重複提交!

四、總結

實現思路
  1. 首先利用AOP切面在進入方法前攔截進行表單重複提交校驗邏輯處理
  2. 通過Rediskey-value鍵值對存儲需要的邏輯判斷數據【ex:key存儲用戶提交表單的api請求路徑,value存儲提交時間】
  3. 邏輯處理
    第一次提交時存入相應數據到redis中
    當再次提交保存時從redis緩存中取出上次提交的時間與當前操作時間做判斷,
    如果當前操作時間距離上次操作時間在我們設置的’判斷為重複提交的時間(3秒內)’則為重複提交直接返回重複提交提示語句或其它處理,
    否則為正常提交,進入業務方法處理…
補充

如果api遵從的是嚴格的Restful風格@PostMapping用於表單提交操作,則可不用自定義註解方式去判斷需要校驗重複提交的路徑,直接在aop切面攔截該請求路徑後,通過反射拿到該方法上的註解是否存在@PostMapping如果存在則是提交表單的api,即進行校驗處理,如果不存在即是其它的@GetMapping@PutMapping@DeleteMapping操作…

本文案例demo源碼

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

二、從零開始搭建自己的靜態博客 — 主題篇

我們已經成功地在本地搭建了一個博客網站,它使用的是pelican默認的notmyidea主題;

如果你不太記得了,可以再看看這篇文章:;

其實,pelican擁有眾多的開源主題庫,我們可以在上選擇一個自己喜歡的主題應用到項目中;

網站提供在線預覽主題的功能;

我選擇的是主題,它的在線Demo是:;

下面,我們來一步一步的將其應用到我們的項目中;

1. 下載主題

我粗略的瀏覽了一下pelican-alchemy的文檔和issue列表,考慮到後續有可能會做一些修改,所以我決定先將其fork到自己的倉庫;

然後,我在項目根目錄新建一個目錄themes/用於存放所有下載的主題,然後將fork後的pelican-alchemy作為一個獨立的子倉庫克隆到目錄下:

λ mkdir themes
λ git submodule add git@github.com:luizyao/pelican-alchemy.git themes/pelican-alchemy

注意:

git submodule add <url> <path>命令是將一個倉庫添加到指定的目錄下作為獨立的子倉庫;

如果你仔細觀察,會發現我們的根目錄下多了一個文件:.gitmodules,它記錄了子倉庫的信息;

例如:我們項目中這個文件的內容是:

[submodule "themes/pelican-alchemy"]
    path = themes/pelican-alchemy
    url = git@github.com:luizyao/pelican-alchemy.git

常用的和子倉庫的相關的操作有下面幾個:

  • 克隆父倉庫時,連同子倉庫一起克隆:

    git clone --recurse-submodules <URL> <directory>
  • 查看父倉庫中所有子倉庫的狀態:

    λ git submodule status
    3381c5031bf30d3b1212619b662898f178d695f1 themes/pelican-alchemy (v2.1-43-g3381c50)

    3381c5031bf30d3b1212619b662898f178d695f1是對當前Commit IdSHA-1加密字串;

  • 刪除子倉庫:

    git rm <submodule path> && git commit

    再手動刪除.git/modules/<name>/目錄

如果你想了解更多關於git submodule的內容,可以通過git submodule --help閱讀它的官方文檔;

2. 使用主題

2.1. 基本配置

# pelicanconf.py

# 主題所在的相對目錄
THEME = 'themes/pelican-alchemy/alchemy'

# 副標題
SITESUBTITLE = '戒驕戒躁 砥礪前行'

# 頭像
SITEIMAGE = '/images/profile.png width=200 height=200'

# 友鏈
LINKS = (
    ('pytest-chinese-doc', 'https://luizyao.github.io/pytest-chinese-doc/'),
)

# 代碼高亮的樣式
PYGMENTS_STYLE = 'friendly'

# 使用 Bootswatch 樣式:https://bootswatch.com/
BOOTSTRAP_CSS = 'https://cdn.bootcss.com/bootswatch/4.3.1/lux/bootstrap.min.css'

# 生成 sitemap.xml 文件,它是一個對爬蟲友好的文件,方便搜索引擎抓取網站頁面
DIRECT_TEMPLATES = ['index', 'tags', 'categories', 'authors', 'archives', 'sitemap']
SITEMAP_SAVE_AS = 'sitemap.xml'

# 構建后的 html 文件路徑和 URL 標識
ARTICLE_URL = 'posts/{date:%Y}/{date:%m}/{slug}.html'
ARTICLE_SAVE_AS = ARTICLE_URL
DRAFTS_URL = 'drafts/{date:%Y}/{date:%m}/{slug}.html'
DRAFTS_SAVE_AS = ARTICLE_URL
PAGE_URL = 'pages/{slug}.html'
PAGE_SAVE_AS = PAGE_URL

# RSS 訂閱
FEED_ALL_RSS = 'feeds/all.rss.xml'

具體細節可以參考:

2.2. 高級配置

2.2.1. 配置網站圖標

通過在線工具可以生成適配各種平台和瀏覽器的favicon文件:

下載上面生成的favicon包,並解壓到項目content/extras目錄下:

λ ls content/extras/
android-chrome-192x192.png  favicon.ico         safari-pinned-tab.svg
android-chrome-384x384.png  favicon-16x16.png   site.webmanifest
apple-touch-icon.png        favicon-32x32.png
browserconfig.xml           mstile-150x150.png

修改模版中的base.html文件:

<!-- themes/pelican-alchemy/alchemy/templates/base.html --> 

{% if RFG_FAVICONS %}
  <link rel="apple-touch-icon" href="{{ SITEURL }}/apple-touch-icon.png" sizes="180x180">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-32x32.png" sizes="32x32">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-16x16.png" sizes="16x16">
  <link rel="manifest" href="{{ SITEURL }}/manifest.json">
  <meta name="theme-color" content="#333333">
{% endif %}

<!-- 改成 --> 

{% if RFG_FAVICONS %}
  <link rel="apple-touch-icon" href="{{ SITEURL }}/apple-touch-icon.png" sizes="180x180">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-32x32.png" sizes="32x32">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-16x16.png" sizes="16x16">
  <link rel="manifest" href="{{ SITEURL }}/site.webmanifest">
  <link rel="mask-icon" href="{{ SITEURL }}/safari-pinned-tab.svg" color="#5bbad5">
  <meta name="msapplication-TileColor" content="#da532c">
  <meta name="theme-color" content="#ffffff">
{% endif %}

修改pelicanconf.py配置文件:

# pelicanconf.py

# 在構建中,它們會無損的拷貝到 output 的同名目錄下
STATIC_PATHS = ['extras', 'images', 'css']

# 構建時,extras/android-chrome-192x192.png文件,拷貝到output/android-chrome-192x192.png,不再是output/extras/android-chrome-192x192.png
EXTRA_PATH_METADATA = {
    'extras/android-chrome-192x192.png': {'path': 'android-chrome-192x192.png'},
    'extras/android-chrome-512x512.png': {'path': 'android-chrome-512x512.png'},
    'extras/apple-touch-icon.png': {'path': 'apple-touch-icon.png'},
    'extras/browserconfig.xml': {'path': 'browserconfig.xml'},
    'extras/favicon-16x16.png': {'path': 'favicon-16x16.png'},
    'extras/favicon-32x32.png': {'path': 'favicon-32x32.png'},
    'extras/favicon.ico': {'path': 'favicon.ico'},
    'extras/manifest.json': {'path': 'manifest.json'},
    'extras/mstile-150x150.png': {'path': 'mstile-150x150.png'},
    'extras/safari-pinned-tab.svg': {'path': 'safari-pinned-tab.svg'},
    # 自定義樣式
    'css/custom.css': {'path': 'theme/css/custom.css'},
}

# 自定義樣式的URL目錄
THEME_CSS_OVERRIDES = ('theme/css/custom.css',)

RFG_FAVICONS = True

2.2.2.更新Font Awesome的版本

pelican-alchemy使用Font Awesome 4.7.0版本,並且使用的是靜態資源的相對引用;

我們將其修改為最新的5.11.2版本的CDN引入,修改主題模版中的base.html文件:

<!-- themes/pelican-alchemy/alchemy/templates/base.html --> 

<link rel="stylesheet" href="{{ SITEURL }}/theme/css/font-awesome.min.css">

<!-- 改成 --> 

<link href="https://cdn.bootcss.com/font-awesome/5.11.2/css/fontawesome.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/font-awesome/5.11.2/css/solid.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/font-awesome/5.11.2/css/brands.css" rel="stylesheet">

除了上面的步驟,我們還有一個額外的工作要做:因為5.x的版本已經不使用fa前綴,取而代之的是fas()和fab();

所以,對於主題中那些類似class="fa fa-github"的樣式,應該修改為class="fab fa-github",主要涉及article.htmlindex.htmlheader.html這些文件;

最後,修改pelicanconf.py文件中關於ICONS配置的格式,需要額外指定樣式類別:

# pelicanconf.py

# 社交屬性,請到<https://fontawesome.com/icons>網站確定圖標樣式的類別
ICONS = [
    ('fab', 'github', 'https://github.com/luizyao'),
    ('fas', 'blog', 'https://www.cnblogs.com/luizyao/'),
    ('fas', 'rss', 'feeds/all.rss.xml')
]

pelican-alchemy有一個openissue:是關於Font Awesome版本的,後續可能會更新到5.x版本,目前issue處於接收反饋的狀態;

至於為什麼不使用CDN,貌似還和偉大的防火牆有關呢。

I’m sure you’ve heard of the Great Firewall of China; India, Russia, some African countries are doing similar things. You never know which URL or IP might become inaccessible

2.2.3.使用Bootstrap的樣式

我們可以為特定類型的元素添加Bootstrap的官方樣式;例如:為每個img元素添加class = "img-fluid"的樣式;

首先,安裝依賴包:

# beautifulsoup4為插件所依賴的第三方包
λ pipenv install beautifulsoup4

然後,下載插件:

λ mkdir plugins
λ git submodule add git@github.com:ingwinlu/pelican-bootstrapify.git plugins/pelican-bootstrapify

最後,修改pelicanconf.py配置文件:

# 到哪裡尋找插件
PLUGIN_PATHS = ['plugins']

# 想要使用的插件名
PLUGINS = ['pelican-bootstrapify']

# 想要添加的 Bootstrap 樣式
BOOTSTRAPIFY = {
    'table': ['table', 'table-striped', 'table-hover'],
    'img': ['img-fluid'],
}

2.3. 定製主題

下面我們為pelican-alchemy做一些定製化的操作,添加一些新的功能;

2.3.1. 添加返回頂部鏈接

修改base.html文件,在<head>中添加如下部分:

<!-- themes/pelican-alchemy/alchemy/templates/base.html --> 

<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/scrollup/2.4.1/jquery.scrollUp.min.js"></script>

<script>
  $(function () {
    $.scrollUp({
      scrollText: '<i class="fas fa-2x fa-chevron-circle-up"></i>'
    });
  });
</script>

2.3.2. 支持目錄

我自己寫了一個的插件,用於替代pelican默認的MarkdownReader,它有以下功能:

  • 使用增強的markdown解析

    • 代替markdown.extensions.extra
    • 代替markdown.extensions.codehilite
  • 支持以下方式生成文章目錄:

    1. markdown文本內的[TOC]標記處生成目錄;

    2. 通過元數據toc自定義目錄樣式;例如:

      {% if article.toc %}
        <aside class="col-md-4">
          <div class="widget widget-content">
            <h3 class="widget-title">文章目錄</h3>
            <div class="toc">
              <ul>
                {{ article.toc | safe }}
              </ul>
            </div>
          </div>
        </aside>
      {% endif %}
  • 如果沒配summary或者summary為空,支持自動截取開頭部分字符作為摘要;

使用方法:

  1. 作為一個子倉庫下載

    # 項目根目錄創建目錄
    λ mkdir plugins
    # 下載
    λ git submodule add git@github.com:luizyao/pelican-md-reader.git plugins/pelican-md-reader
  2. 修改pelicanconf.py配置文件

    # pelicanconf.py
    
    # 到哪裡尋找插件
    PLUGIN_PATHS = ['plugins']
    
    # 想要使用的插件名
    PLUGINS = ['pelican-md-reader']

更多細節可以參考:

2.3.3. 漢化

主要關鍵字漢化;

3.完整的pelicanconf.py文件

#!/usr/bin/env python
# -*- coding: utf-8 -*- #
from __future__ import unicode_literals

AUTHOR = 'luizyao'
SITENAME = "luizyao's blog"
SITEURL = ''

PATH = 'content'

DEFAULT_LANG = 'en'

# Feed generation is usually not desired when developing
FEED_ALL_ATOM = None
CATEGORY_FEED_ATOM = None
TRANSLATION_FEED_ATOM = None
AUTHOR_FEED_ATOM = None
AUTHOR_FEED_RSS = None

DEFAULT_PAGINATION = 10

# Uncomment following line if you want document-relative URLs when developing
# RELATIVE_URLS = True

# 修改時區
TIMEZONE = 'Asia/Shanghai'

# 修改默認的時間格式('%a %d %B %Y')
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M"

# 為元數據定義默認值
DEFAULT_METADATA = {
    # 默認發布的文章都是草稿,除非在文章元數據中明確指定:Status: published
    'status': 'draft',
}

# pelican-alchemy 原有的配置

# 主題所在的相對目錄
THEME = 'themes/pelican-alchemy/alchemy'

# 副標題
SITESUBTITLE = '戒驕戒躁 砥礪前行'

# 頭像
SITEIMAGE = '/images/profile.png width=200 height=200'

# 友鏈
LINKS = (
    ('pytest-chinese-doc', 'https://luizyao.github.io/pytest-chinese-doc/'),
)

# 代碼高亮的樣式
PYGMENTS_STYLE = 'friendly'

# 使用 Bootswatch 樣式:https://bootswatch.com/
BOOTSTRAP_CSS = 'https://cdn.bootcss.com/bootswatch/4.3.1/lux/bootstrap.min.css'

# 生成 sitemap.xml 文件
DIRECT_TEMPLATES = ['index', 'tags', 'categories', 'authors', 'archives', 'sitemap']
SITEMAP_SAVE_AS = 'sitemap.xml'

# 構建后的 html 文件路徑和 URL 標識
ARTICLE_URL = 'posts/{date:%Y}/{date:%m}/{slug}.html'
ARTICLE_SAVE_AS = ARTICLE_URL
DRAFTS_URL = 'drafts/{date:%Y}/{date:%m}/{slug}.html'
DRAFTS_SAVE_AS = ARTICLE_URL
PAGE_URL = 'pages/{slug}.html'
PAGE_SAVE_AS = PAGE_URL

# RSS 訂閱
FEED_ALL_RSS = 'feeds/all.rss.xml'

# 在構建中,它們會無損的拷貝到 output 的同名目錄下
STATIC_PATHS = ['extras', 'images', 'css']

# 構建時,extras/android-chrome-192x192.png文件,拷貝到output/android-chrome-192x192.png,不再是output/extras/android-chrome-192x192.png
EXTRA_PATH_METADATA = {
    'extras/android-chrome-192x192.png': {'path': 'android-chrome-192x192.png'},
    'extras/android-chrome-512x512.png': {'path': 'android-chrome-512x512.png'},
    'extras/apple-touch-icon.png': {'path': 'apple-touch-icon.png'},
    'extras/browserconfig.xml': {'path': 'browserconfig.xml'},
    'extras/favicon-16x16.png': {'path': 'favicon-16x16.png'},
    'extras/favicon-32x32.png': {'path': 'favicon-32x32.png'},
    'extras/favicon.ico': {'path': 'favicon.ico'},
    'extras/manifest.json': {'path': 'manifest.json'},
    'extras/mstile-150x150.png': {'path': 'mstile-150x150.png'},
    'extras/safari-pinned-tab.svg': {'path': 'safari-pinned-tab.svg'},
    # 自定義樣式
    'css/custom.css': {'path': 'theme/css/custom.css'},
}

# 自定義樣式的URL目錄
THEME_CSS_OVERRIDES = ('theme/css/custom.css',)

RFG_FAVICONS = True

# 到哪裡尋找插件
PLUGIN_PATHS = ['plugins']

# 想要使用的插件名
PLUGINS = ['pelican-bootstrapify', 'pelican-md-reader']

# 想要添加的 Bootstrap 樣式
BOOTSTRAPIFY = {
    'table': ['table', 'table-striped', 'table-hover'],
    'img': ['img-fluid'],
}

# 社交屬性,請到<https://fontawesome.com/icons>網站確定圖標樣式的類別
ICONS = [
    ('fab', 'github', 'https://github.com/luizyao'),
    ('fas', 'blog', 'https://www.cnblogs.com/luizyao/'),
    ('fas', 'rss', 'feeds/all.rss.xml')
]

4. 預覽

Github:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

dom4j的測試例子和源碼詳解(重點對比和DOM、SAX的區別)

目錄

簡介

dom4j用於創建和解析XML文件,不是純粹的DOMSAX,而是兩者的結合和改進,另外,dom4j支持Xpath來獲取節點。目前,由於其出色的性能和易用性,目前dom4j已經得到廣泛使用,例如SpringHibernate就是使用dom4j來解析xml配置。

注意,dom4j使用Xpath需要額外引入jaxen的包。

DOM、SAX、JAXP和DOM4J

其實,JDK已經帶有可以解析xml的api,如DOMSAXJAXP,但為什麼dom4j會更受歡迎呢?它們有什麼區別呢?在學習dom4j之前,需要先理解下DOMSAX等概念,因為dom4j就是在此基礎上改進而來。

xerces解釋器

先介紹下xerces解釋器,下面介紹的SAXDOMJAXP都只是接口,而xerces解釋器就是它們的具體實現,在com.sun.org.apache.xerces.internal包。xerces被稱為性能最好的解釋器,除了xerces外,還有其他的第三方解釋器,如crimson

SAX

JDK針對解析xml提供的接口,不是具體實現,在org.xml.sax包。SAX基於事件處理,解析過程中根據當前的XML元素類型,調用用戶自己實現的回調方法,如:startDocument();,startElement()。下面以例子說明,通過SAX解析xml並打印節點名:

    /*這裏解釋下四個的接口:
    EntityResolver:需要實現resolveEntity方法。當解析xml需要引入外部數據源時觸發,通過這個方法可以重定向到本地數據源或進行其他操作。
    DTDHandler:需要實現notationDecl和unparsedEntityDecl方法。當解析到"NOTATION", "ENTITY"或 "ENTITIES"時觸發。
    ContentHandler:最常用的一個接口,需要實現startDocument、endDocument、startElement、endElement等方法。當解析到指定元素類型時觸發。
    ErrorHandler:需要實現warning、error或fatalError方法。當解析出現異常時會觸發。
    */
    @Test
    public void test04() throws Exception {
        //DefaultHandler實現了EntityResolver, DTDHandler, ContentHandler, ErrorHandler四個接口     
        DefaultHandler handler = new DefaultHandler() {
            @Override
            //當解析到Element時,觸發打印該節點名
            public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                System.out.println(qName);
            }
        };
        //獲取解析器實例
        XMLReader xr = XMLReaderFactory.createXMLReader();
        //設置處理類
        xr.setContentHandler(handler);
        /*
         * xr.setErrorHandler(handler); 
         * xr.setDTDHandler(handler); 
         * xr.setEntityResolver(handler);
         */
        xr.parse(new InputSource("members.xml"));
    }

因為SAX是基於事件處理的,不需要等到整個xml文件都解析完才執行我們的操作,所以效率較高。但SAX存在一個較大缺點,就是不能隨機訪問節點,因為SAX不會主動地去保存處理過的元素(優點就是內存佔用小、效率高),如果想要保存讀取的元素,開發人員先構建出一個xml樹形結構,再手動往裡面放入元素,非常麻煩(其實dom4j就是通過SAX來構建xml樹)。

DOM

JDK針對解析xml提供的接口,不是具體實現,在org.w3c.dom包。DOM採用了解析方式是一次性加載整個XML文檔,在內存中形成一個樹形的數據結構,開發人員可以隨機地操作元素。見以下例子:

    @SuppressWarnings("restriction")
    @Test
    public void test05() throws Exception {
        //獲得DOMParser對象
        com.sun.org.apache.xerces.internal.parsers.DOMParser domParser = new com.sun.org.apache.xerces.internal.parsers.DOMParser();
        //解析文件
        domParser.parse(new InputSource("members.xml"));
        //獲得Document對象
        Document document=domParser.getDocument();
        // 遍歷節點
        printNodeList(document.getChildNodes());        
    }

通過DOM解析,我們可以獲取任意節點進行操作。但是,DOM有兩個缺點:

  1. 由於一次性加載整個XML文件到內存,當處理較大文件時,容易出現內存溢出。
  2. 節點的操作還是比較繁瑣。

以上兩點,dom4j都進行了相應優化。

JAXP

封裝了SAXDOM兩種接口,它並沒有為JAVA解析XML提供任何新功能,只是對外提供更解耦、簡便操作的API。如下:

DOM解析器

    @Test
    public void test02() throws Exception {
        // 獲得DocumentBuilder對象
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        // 解析xml文件,獲得Document對象
        Document document = builder.parse("members.xml");
        // 遍歷節點
        printNodeList(document.getChildNodes());
    }

獲取SAX解析器

    @Test
    public void test03() throws Exception {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser saxParser = factory.newSAXParser();
        saxParser.parse("members.xml", new DefaultHandler() {
            @Override
            public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                System.out.println(qName);
            }
        });
    }

其實,JAXP並沒有很大程度提高DOM和SAX的易用性,更多地體現在獲取解析器時實現解耦。完全沒有解決SAXDOM的缺點。

DOM4j

對比過dom4jJAXP就會發現,JAXP本質上還是將SAXDOM當成兩套API來看待,而dom4j就不是,它將SAXDOM結合在一起使用,取長補短,並對原有的api進行了改造,在使用簡便性、性能、面向接口編程等方面都要優於JDK自帶的SAXDOM

以下通過使用例子和源碼分析將作出說明。

項目環境

工程環境

JDK:1.8

maven:3.6.1

IDE:sts4

dom4j:2.1.1

創建項目

項目類型Maven Project,打包方式jar。

引入依賴

注意:dom4j使用XPath,必須引入jaxen的jar包。

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- dom4j的jar包 -->
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.1</version>
</dependency>
<!-- dom4j使用XPath需要的jar包 -->
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>
<!-- 配置BeanUtils的包,這個我自定義工具類用的,如果只是簡單使用dom4j可以不引入 -->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.3</version>
</dependency>

使用例子–生成xml文件

本例子將分別使用dom4j和JDK的DOM接口生成xml文件(使用JDK的DOM接口時會使用JAXP的API)。

需求

構建xml樹,添加節點,並生成xml文件。格式如下:

<?xml version="1.0" encoding="UTF-8"?>
<members>
  <students>
    <student name="張三" location="河南" age="18"/>
    <student name="李四" location="新疆" age="26"/>
    <student name="王五" location="北京" age="20"/>
  </students>
  <teachers>
    <teacher name="zzs" location="河南" age="18"/>
    <teacher name="zzf" location="新疆" age="26"/>
    <teacher name="lt" location="北京" age="20"/>
  </teachers>
</members>

生成xml文件–使用w3c的DOM接口

主要步驟

  1. 通過JAXP的API獲得Document對象,這個對象可以看成xml的樹;

  2. 將對象轉化為節點,並添加在Document這棵樹上;

  3. 通過Transformer對象將樹輸出到文件中。

編寫測試類

路徑:test目錄下的cn.zzs.dom4j

注意:因為使用的是w3cDOM接口,所以節點對象導的是org.w3c.dom包,而不是org.dom4j包。

    @Test
    public void test02() throws Exception {
        // 創建工廠對象
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        // 創建DocumentBuilder對象
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        // 創建Document對象
        Document document = documentBuilder.newDocument();

        // 創建根節點
        Element root = document.createElement("members");
        document.appendChild(root);

        // 添加一級節點
        Element studentsElement = (Element)root.appendChild(document.createElement("students"));
        Element teachersElement = (Element)root.appendChild(document.createElement("teachers"));

        // 添加二級節點並設置屬性
        Element studentElement1 = (Element)studentsElement.appendChild(document.createElement("student"));
        studentElement1.setAttribute("name", "張三");
        studentElement1.setAttribute("age", "18");
        studentElement1.setAttribute("location", "河南");
        Element studentElement2 = (Element)studentsElement.appendChild(document.createElement("student"));
        studentElement2.setAttribute("name", "李四");
        studentElement2.setAttribute("age", "26");
        studentElement2.setAttribute("location", "新疆"); 
        Element studentElement3 = (Element)studentsElement.appendChild(document.createElement("student"));
        studentElement3.setAttribute("name", "王五");
        studentElement3.setAttribute("age", "20");
        studentElement3.setAttribute("location", "北京");     
        Element teacherElement1 = (Element)teachersElement.appendChild(document.createElement("teacher"));
        teacherElement1.setAttribute("name", "zzs");
        teacherElement1.setAttribute("age", "18");
        teacherElement1.setAttribute("location", "河南"); 
        Element teacherElement2 = (Element)teachersElement.appendChild(document.createElement("teacher"));
        teacherElement2.setAttribute("name", "zzf");
        teacherElement2.setAttribute("age", "26");
        teacherElement2.setAttribute("location", "新疆");     
        Element teacherElement3 = (Element)teachersElement.appendChild(document.createElement("teacher"));
        teacherElement3.setAttribute("name", "lt");
        teacherElement3.setAttribute("age", "20");
        teacherElement3.setAttribute("location", "北京"); 
            
        // 獲取文件對象
        File file = new File("members.xml");
        if(!file.exists()) {
            file.createNewFile();
        }
        // 獲取Transformer對象
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        // 設置編碼、美化格式
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        // 創建DOMSource對象
        DOMSource domSource = new DOMSource(document);
        // 將document寫出
        transformer.transform(domSource, new StreamResult(new PrintWriter(new FileOutputStream(file))));    
    }   

測試結果

此時,在項目路徑下會生成members.xml,文件內容如下,可以看到,使用w3cDOM接口輸出的內容沒有縮進格式。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<members>
<students>
<student age="18" location="河南" name="張三"/>
<student age="26" location="新疆" name="李四"/>
<student age="20" location="北京" name="王五"/>
</students>
<teachers>
<teacher age="18" location="河南" name="zzs"/>
<teacher age="26" location="新疆" name="zzf"/>
<teacher age="20" location="北京" name="lt"/>
</teachers>
</members>

生成xml文件–使用dom4j的DOM接口

主要步驟

  1. 通過DocumentHelper獲得Document對象,這個對象可以看成xml的樹;

  2. 將對象轉化為節點,並添加在Document這棵樹上;

  3. 通過XMLWriter對象將樹輸出到文件中。

編寫測試類

路徑:test目錄下的cn.zzs.dom4j。通過對比,可以看出,dom4j的API相比JDK的還是要方便很多。

注意:因為使用的是dom4jDOM接口,所以節點對象導的是org.dom4j包,而不是org.w3c.dom包(dom4j一個很大的特點就是改造了w3cDOM接口,極大地簡化了我們對節點的操作)。

    @Test
    public void test02() throws Exception {
        // 創建Document對象
        Document document = DocumentHelper.createDocument();

        // 添加根節點
        Element root = document.addElement("members");

        // 添加一級節點
        Element studentsElement = root.addElement("students");
        Element teachersElement = root.addElement("teachers");

        // 添加二級節點並設置屬性,dom4j改造了w3c的DOM接口,極大地簡化了我們對節點的操作
        studentsElement.addElement("student").addAttribute("name", "張三").addAttribute("age", "18").addAttribute("location", "河南");
        studentsElement.addElement("student").addAttribute("name", "李四").addAttribute("age", "26").addAttribute("location", "新疆");
        studentsElement.addElement("student").addAttribute("name", "王五").addAttribute("age", "20").addAttribute("location", "北京");
        teachersElement.addElement("teacher").addAttribute("name", "zzs").addAttribute("age", "18").addAttribute("location", "河南");
        teachersElement.addElement("teacher").addAttribute("name", "zzf").addAttribute("age", "26").addAttribute("location", "新疆");
        teachersElement.addElement("teacher").addAttribute("name", "lt").addAttribute("age", "20").addAttribute("location", "北京");

        // 獲取文件對象
        File file = new File("members.xml");
        if(!file.exists()) {
            file.createNewFile();
        }
        // 創建輸出格式,不設置的話不會有縮進效果
        OutputFormat format = OutputFormat.createPrettyPrint();
        format.setEncoding("UTF-8");
        // 獲得XMLWriter
        XMLWriter writer = new XMLWriter(new FileWriter(file), format);
        // 打印Document
        writer.write(document);
        // 釋放資源
        writer.close();
    }

測試結果

此時,在項目路徑下會生成members.xml,文件內容如下,可以看出dom4j輸出文件會進行縮進處理,而JDK的不會:

<?xml version="1.0" encoding="UTF-8"?>

<members>
  <students>
    <student name="張三" age="18" location="河南"/>
    <student name="李四" age="26" location="新疆"/>
    <student name="王五" age="20" location="北京"/>
  </students>
  <teachers>
    <teacher name="zzs" age="18" location="河南"/>
    <teacher name="zzf" age="26" location="新疆"/>
    <teacher name="lt" age="20" location="北京"/>
  </teachers>
</members>

使用例子–解析xml文件

需求

  1. 解析xml:解析上面生成的xml文件,將學生和老師節點按以下格式遍歷打印出來(當然也可以再封裝成對象返回給調用者,這裏就不擴展了)。
student:name=張三,location=河南,age=18
student:name=李四,location=新疆,age=26
student:name=王五,location=北京,age=20
teacher:name=zzs,location=河南,age=18
teacher:name=zzf,location=新疆,age=26
teacher:name=lt,location=北京,age=20
  1. dom4j結合XPath查找指定節點

主要步驟

  1. 通過SAXReader對象讀取和解析xml文件,獲得Document對象,即xml樹;

  2. 調用Node的方法遍歷打印xml樹的節點;

  3. 使用XPath查詢指定節點。

測試遍歷節點

考慮篇幅,這裏僅給出一種節點遍歷方式,項目源碼中還給出了其他的幾種。

    /**
     *  測試解析xml
     */
    @Test
    public void test03() throws Exception {
        // 創建指定文件的File對象
        File file = new File("members.xml");
        // 創建SAXReader
        SAXReader saxReader = new SAXReader();
        // 將xml文件讀入成document
        Document document = saxReader.read(file);
        // 獲得根元素
        Element root = document.getRootElement();
        // 遞歸遍歷節點
        list1(root);
    }

    /**
     * 遞歸遍歷節點
     */
    private void list1(Element parent) {
        if(parent == null) {
            return;
        }
        // 遍歷當前節點屬性並輸出
        printAttr(parent);
        // 遞歸打印子節點
        Iterator<Element> iterator2 = parent.elementIterator();
        while(iterator2.hasNext()) {
            Element son = (Element)iterator2.next();
            list1(son);
        }
    }

測試結果如下:

-------第一種遍歷方式:Iterator+遞歸--------
student:name=張三,location=河南,age=18
student:name=李四,location=新疆,age=26
student:name=王五,location=北京,age=20
teacher:name=zzs,location=河南,age=18
teacher:name=zzf,location=新疆,age=26
teacher:name=lt,location=北京,age=20

測試XPath獲取指定節點

    @Test
    public void test04() throws Exception {
        // 創建指定文件的File對象
        File file = new File("members.xml");
        // 創建SAXReader
        SAXReader saxReader = new SAXReader();
        // 將xml文件讀入成document
        Document document = saxReader.read(file);
        // 使用xpath隨機獲取節點
        List<Node> list = document.selectNodes("//members//students/student");
        // List<Node> list = xmlParser.getDocument().selectSingleNode("students");
        // 遍歷節點
        Iterator<Node> iterator = list.iterator();
        while(iterator.hasNext()) {
            Element element = (Element)iterator.next();
            printAttr(element);
        }
    }

測試結果如下:

student:age=18,location=河南,name=張三
student:age=26,location=新疆,name=李四
student:age=20,location=北京,name=王五

XPath語法

利用XPath獲取指定節點,平時用的比較多,這裏列舉下基本語法。

表達式 結果
/members 選取根節點下的所有members子節點
//members 選取根節點下的所有members節點
//students/student[1] 選取students下第一個student子節點
//students/student[last()] 選取students下的最後一個student子節點
//students/student[position()<3] 選取students下前兩個student子節點
//student[@age] 選取所有具有age屬性的student節點
//student[@age=’18’] 選取所有age屬性為18的student節點
//students/* 選取students下的所有節點
//* 選取文檔中所有節點
//student[@*] 選取所有具有屬性的節點
//members/students\ //members/teachers

源碼分析

本文會先介紹dom4j如何將xml元素抽象成具體的對象,再去分析dom4j解析xml文件的過程(注意,閱讀以下內容前需要了解和使用過JDK自帶的DOMSAX)。

dom4j節點的類結構

先來看下一個完整xml的元素組成,可以看出,一個xml文件包含了DocumentElementCommentAttributeDocumentTypeText等等。

DOM的思想就是將xml元素解析為具體對象,並構建樹形數據結構。基於此,w3c提供了xml元素的接口規範,dom4j基本借用了這套規範(如下圖),只是改造了接口的方法,使得我們操作時更加簡便。

SAXReader.read(File file)

通過使用例子可知,我們解析xml文件的入口是SAXReader對象的read方法,入參可以是文件路徑、url、字節流、字符流等,這裏以傳入文件路徑為例。

注意:考慮篇幅和可讀性,以下代碼經過刪減,僅保留所需部分。

    public Document read(File file) throws DocumentException {
        //不管是URI,path,character stream還是byte stream,都會包裝成InputSource對象
        InputSource source = new InputSource(new FileInputStream(file));
        if (this.encoding != null) {
            source.setEncoding(this.encoding);
        }
        
        //下面這段代碼是為了設置systemId,當傳入URI且沒有指定字符流和字節流時,可以通過systemId去連接URL並解析
        //如果一開始傳入了字符流或字節流,這個systemId就是可選的
        String path = file.getAbsolutePath();
        if (path != null) {
            StringBuffer sb = new StringBuffer("file://");
            if (!path.startsWith(File.separator)) {
                sb.append("/");
            }
            path = path.replace('\\', '/');
            sb.append(path);
            source.setSystemId(sb.toString());
        }

        //這裏調用重載方法解析InputSource對象
        return read(source);
    }

SAXReader.read(InputSource in)

看到這個方法的代碼時,使用過JDK的SAX的朋友應該很熟悉,沒錯,dom4j也是採用事件處理的機制來解析xml。其實,只是這裏設置的SAXContentHandler已經實現好了相關的方法,這些方法共同完成一件事情:構建xml樹。明白這一點,應該就能理解dom4j是如何解決SAXDOM的缺點了。

注意:考慮篇幅和可讀性,以下代碼經過刪減,僅保留所需部分。

    public Document read(InputSource in) throws DocumentException {
        // 這裡會調用JAXP接口獲取XMLReader實現類對象
        XMLReader reader = getXMLReader();
        reader = installXMLFilter(reader);
        
        // 下面這些操作,是不是和使用JDK的SAX差不多,dom4j也是使用了事件處理機制。

        // EntityResolver:通過實現resolveEntity方法,當解析xml需要引入外部數據源時觸發,可以重定向到本地數據源或進行其他操作。
        EntityResolver thatEntityResolver = this.entityResolver;
        if (thatEntityResolver == null) {
            thatEntityResolver = createDefaultEntityResolver(in
                    .getSystemId());
            this.entityResolver = thatEntityResolver;
        }
        reader.setEntityResolver(thatEntityResolver);
        
        // 下面的SAXContentHandler繼承了DefaultHandler,即實現了EntityResolver, DTDHandler, ContentHandler, ErrorHandler等接口
        // 其中最重要的是ContentHandler接口,通過實現startDocument、endDocument、startElement、endElement等方法,當dom4j解析xml文件到指定元素類型時,可以觸發我們自定義的方法。
        // 當然,dom4j已經實現了ContentHandler的方法。具體實現的方法內容為:在解析xml時構建xml樹
        SAXContentHandler contentHandler = createContentHandler(reader);
        contentHandler.setEntityResolver(thatEntityResolver);
        contentHandler.setInputSource(in);
        boolean internal = isIncludeInternalDTDDeclarations();
        boolean external = isIncludeExternalDTDDeclarations();
        contentHandler.setIncludeInternalDTDDeclarations(internal);
        contentHandler.setIncludeExternalDTDDeclarations(external);
        contentHandler.setMergeAdjacentText(isMergeAdjacentText());
        contentHandler.setStripWhitespaceText(isStripWhitespaceText());
        contentHandler.setIgnoreComments(isIgnoreComments());
        reader.setContentHandler(contentHandler);

        configureReader(reader, contentHandler);
        
        // 使用事件處理機制解析xml,處理過程會構建xml樹
        reader.parse(in);
        // 返回構建好的xml樹
        return contentHandler.getDocument();
    }

SAXContentHandler

通過上面的分析,可知SAXContentHandlerdom4j構建xml樹的關鍵。這裏看下它的幾個重要方法和屬性。

startDocument()

    // xml樹
    private Document document;

    // 節點棧,棧頂存放當前解析節點(節點解析結束)、或當前解析節點的父節點(節點解析開始)
    private ElementStack elementStack;

    // 節點處理器,可以看成節點開始解析或結束解析的標誌
    private ElementHandler elementHandler;
    
    // 當前解析節點(節點解析結束)、或當前解析節點的父節點(節點解析開始)
    private Element currentElement;
    public void startDocument() throws SAXException {
        document = null;
        currentElement = null;
        
        // 清空節點棧
        elementStack.clear();
        // 初始化節點處理器
        if ((elementHandler != null)
                && (elementHandler instanceof DispatchHandler)) {
            elementStack.setDispatchHandler((DispatchHandler) elementHandler);
        }

        namespaceStack.clear();
        declaredNamespaceIndex = 0;

        if (mergeAdjacentText && (textBuffer == null)) {
            textBuffer = new StringBuffer();
        }

        textInTextBuffer = false;
    }

startElement(String,String,String,Attributes)

    public void startElement(String namespaceURI, String localName,
            String qualifiedName, Attributes attributes) throws SAXException {
        if (mergeAdjacentText && textInTextBuffer) {
            completeCurrentTextNode();
        }

        QName qName = namespaceStack.getQName(namespaceURI, localName,
                qualifiedName);
        // 獲取當前解析節點的父節點
        Branch branch = currentElement;

        if (branch == null) {
            branch = getDocument();
        }
        // 創建當前解析節點
        Element element = branch.addElement(qName);
        addDeclaredNamespaces(element);

        // 添加節點屬性
        addAttributes(element, attributes);
        
        //將當前節點壓入節點棧
        elementStack.pushElement(element);
        currentElement = element;
        entity = null; // fixes bug527062

        //標記節點解析開始
        if (elementHandler != null) {
            elementHandler.onStart(elementStack);
        }
    }

endElement(String, String, String)

    public void endElement(String namespaceURI, String localName, String qName)
            throws SAXException {
        if (mergeAdjacentText && textInTextBuffer) {
            completeCurrentTextNode();
        }
        // 標記節點解析結束
        if ((elementHandler != null) && (currentElement != null)) {
            elementHandler.onEnd(elementStack);
        }
        // 當前解析節點從節點棧中彈出
        elementStack.popElement();
        // 指定為棧頂節點
        currentElement = elementStack.peekElement();
    }

endDocument()

    public void endDocument() throws SAXException {
        namespaceStack.clear();
        // 清空節點棧
        elementStack.clear();
        currentElement = null;
        textBuffer = null;
    }

以上,dom4j的源碼分析基本已經分析完,其他具體細節後續再做補充。

參考以下資料:

本文為原創文章,轉載請附上原文出處鏈接:https://github.com/ZhangZiSheng001/dom4j-demo

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

SpringMvc demo示例及源碼詳細分析

三層架構介紹

  我們的開發架構一般都是基於兩種形式,一種C/S架構,也就是客戶端/服務器,另一種是B/S架構,也就是瀏覽器/服務器。在JavaEE開發中,幾乎全部都是基於B/S架構的開發。那麼在B/S架構中,系統標準的三層架構包括:表現層、業務層、持久層。三層架構在我們的實際開發中使用的非常多。

三層職責

表現層

  也就是我們長說的web層。它負責接收客戶端請求,向客戶端響應結果,通常客戶端使用http協議請求web層,web需要接收http請求,完成http響應。

  表現層包括展示層和控制層:控制層負責接收請求,展示層負責結果的展示。

  表現層依賴業務層,接收到客戶端請求一般會調用業務層進行業務處理,並將處理結果響應給客戶端。

  表現層的設計一般都是使用mvc模型。(mvc是表現層的設計模型,和其他層沒有關係)

業務層

  也就是我們常說的 service層。它負責業務邏輯處理,和我們開發項目的需求息息相關。web層依賴業務層,但是業務層不依賴web層。

  業務層在業務處理時可能會依賴持久層,如果要對數據持久化需要保證事務一致性。(也就是我們說的,事務應該放到業務層來控制)

持久層

  也就是我們常說的dao層。負責數據持久化,包括數據層即數據庫和數據訪問層,數據庫是對數據進行持久化的載體,數據訪問層是業務層和持久層交互的接口,業務層需要通過數據訪問層將數據持久化到數據庫中。

  通俗的講,持久層就是和數據交互,對數據庫表進行增刪改查的。

mvc設計模式介紹

  mvc全名是Model View Controller,模型(Model)-視圖(View)-控制器(Controller)的縮寫,是一種用於設計創建web應用程序表現層的模式。mvc中每個部分各司其職:

Model(模型)

  模型包含業務模型和數據模型,數據模型用於封裝數據,業務模型用於處理業務。

View(視圖)

  通常指的就是我們的jsp或者html。作用一般就是展示數據的。

  通過視圖是依據模型數據創建的。

Controller(控制器)

  是應用程序中處理用戶交互的部分。作用一般就是處理程序邏輯的。

SpringMVC介紹

Spring MVC是什麼?

  SpringMVC是一種基於Java的實現MVC設計模型的請求驅動類型的輕量級Web框架,屬於SpringFrameWork的後續產品,已經融合在Spring Web Flow裏面。Spring框架提供了構建Web應用程序的全功能MVC模塊。使用Spring可插入的MVC架構,從而在使用Spring進行Web開發時,可以選擇使用Spring的Spring MVC框架或集成其他MVC開發框架,如Struts1(現在一般不用),Struts2等。

  SpringMVC已經成為目前最主流的MVC框架之一,並隨着Spring3.0的發布,全面超越Struts2,成為最優秀的MVC框架。

  它通過一套註解,讓一個簡單的Java類稱為處理請求的控制器,而無需實現任何接口。同時它還支持RESTful編程風格的請求。

總結

  Spring MVC和Struts2一樣,都是為了解決表現層問題的web框架,他們都是基於MCC設計模式的。而這些表現層框架的主要職責就是處理前端HTTP請求

 Spring MVC由來?

 Spring MVC全名叫Spring Web MVC,它是Spring家族Web模塊的一個重要成員。這一點,我們可以從Spring的整體結構中看的出來:

 

 

 為什麼學習SpringMVC?

   也許你會問,為什麼要學習Spring MVC呢?struts2不才是主流嘛?看SSH的概念有多火?

  其實很多初學者混淆了一個概念,SSH實際上指的是Struts1.x+Spring+Hibernate。這個概念已經有十幾年的歷史了。在Struts1.x時代,它是當之無愧的霸主,但是在新的MVC框架湧現的時代,形式已經不是這樣了,Struts2.x藉助了Struts1.x的好名聲,讓國內開發人員認為Struts2.x是霸主繼任者(其實兩者在技術上無任何關係),導致國內程序員大多數學習基於Struts2.x的框架,又一個貌似很多的概念出來了S2SH(Struts2+Spring+Hibernate)整合開發。

 SpringMVC如何處理請求?

   SpringMVC是基於MVC設計模型的,MVC模式指的就是Model(業務模型)、View(視圖)、Controller(控制器)。SpringMVC處理請求就是通過MVC這三個角色來實現的。

注:不要把MVC設計模式工程的三層架構混淆,三層結構指的是表現層、業務層、數據持久層。而MVC只針對表現層進行設計

  下面讓我們看看處理流程吧

 

 

 第一個MVC程序

達到效果

  1. 學會如果配置前端控制器
  2. 如何開發處理器

任務需求

  訪問/queryItem,返回商品列表頁面,商品數據暫時使用靜態數據(不從數據庫查詢並返回)。

 實現

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.cyb</groupId>
    <artifactId>springmvc-demo01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <dependencies>
        <!-- spring ioc組件需要的依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>

        <!-- 基於AspectJ的aop依賴 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>

        <!-- spring MVC依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>

        <!-- jstl -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        
        <!-- servlet -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 配置Maven的JDK編譯級別 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <port>8080</port>
                </configuration>
            </plugin>
            <!-- tomcat依賴包 -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
            </plugin>
        </plugins>
    </build>
</project>

注:

1、依賴添加完之後,項目上右鍵->maven->Update Maven Project

2、項目上右鍵->Java EE Tools->Generate Deployment Descriptor Stub

 web.xml

路徑:src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">
    <!-- 學習前置條件 -->
    <!-- 問題1:web.xml中servelet、filter、listener、context-param加載順序 -->
    <!-- 問題2:load-on-startup標籤的作用,影響了Servlet對象創建的時機 -->
    <!-- 問題3:url-pattern:標籤的配置方式有四種:/dispatcherServlet、/servlet/*、*.do、/ 以上四種配置-->
    <!-- 問題4:url-pattern標籤的配置為什麼配置/就不攔截jsp請求,而配置/*,就會攔截jsp請求 -->
    <!-- 問題4原因:標籤配置為/*報錯,因為它攔截了jsp請求,但是又不能處理jsp請求。 -->
    <!-- 問題5:配置了springmvc去讀取spring配置文件之後,就產生了spring父子容器的問題 -->
    
    <!-- 配置前端控制器 -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 設置spring配置文件路徑 -->
        <!-- 如果不設置初始化參數,那麼DispatcherServlet會讀取默認路徑下的配置文件 -->
        <!-- 默認配置文件路徑:/WEB-INF/springmvc-servlet.xml -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!-- 指定初始化時機,設置為2,表示Tomcat啟動時,它會跟隨着啟動,DispatcherServlet會跟隨着初始化 -->
        <!-- 如果沒有指定初始化時機,DispatcherServlet就會在第一次被請求的時候,才會初始化,而且只會被初始化一次(單例模式) -->
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <!-- url-pattern的設置 -->
        <!-- 不要配置為/*,否則報錯 -->
        <!-- 通俗解釋:會攔截整個項目中的資源訪問,包含JSP和靜態資源的訪問,對於JS的訪問,springmvc提供了默認Handler處理器 -->
        <!-- 但是對於JSP來講,springmvc沒有提供默認的處理器,我們也沒有手動編寫對應的處理器,此時按照springmvc的處理流程分析得知,它down了 -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

springmvc.xml

路徑:src/main/resources/springmvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 處理器類的掃描 -->
    <context:component-scan
        base-package="com.cyb.springmvc.controller"></context:component-scan>
    <!-- 註解映射器 @Controller和@RequestMapping組合這種方式的註解映射的解析 -->
    <!-- <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"></bean> -->
    <!-- 註解適配器 -->
    <!-- <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean> -->
    <!-- 配置註釋的適配器和映射器,同時還注入其他很多的bean -->
    <!-- <mvc:annotation-driven></mvc:annotation-driven> -->
    <!-- 显示配置視圖解析器 -->
    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>
</beans>

ItemController.java

路徑:/src/main/java/com/cyb/springmvc/controller/ItemController.java

package com.cyb.springmvc.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.cyb.springmvc.po.item;

/**
 * 處理器的開發方式有多種,比如實現HttpRequestHandler接口、Controller接口的方式、還有註解的方式 企業中使用的一般都是註解的方式
 * 註解的注意事項
 *  1、類上加上@Controller註解(必須是Controller,可以通過源碼找到答案)
 *  2、類上或者方法上面要加上@RequestMapping(必須)
 * 
 * @author apple
 *
 */
@Controller
public class ItemController {
    //@RequestMapping此時填寫的是url
    //ModelAndView:Model標識的是數據類型,View就是最終要展示給用戶的視圖
    @RequestMapping("queryItem")
    public ModelAndView queryItem() {
        //用靜態數據模型
        List<item> itemList=new ArrayList<item>();
        
        item item_1=new item();
        item_1.setName("蘋果手機");
        item_1.setPrice(5000);
        item_1.setDetail("iphoneX蘋果手機!");
        itemList.add(item_1);
        
        item item_2=new item();
        item_2.setName("華為手機");
        item_2.setPrice(6000);
        item_2.setDetail("華為5G網速就是快!");
        itemList.add(item_2);
        ModelAndView mvAndView=new ModelAndView();
        //設置數據模型,相當於request的setAttribute方法,實質上,底層確實也是轉成了request()
        //先將k/v數據放入map中,最終根據視圖對象不同,再進行後續處理
        mvAndView.addObject("itemList",itemList);
        //設置view視圖
        mvAndView.setViewName("/WEB-INF/jsp/item/item-list.jsp");
        return mvAndView;
    }
}

item.java

路徑:src/main/java/com/cyb/springmvc/po/item.java

package com.cyb.springmvc.po;

public class item {
    private String name;
    private double price;
    private String detail;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public String getDetail() {
        return detail;
    }

    public void setDetail(String detail) {
        this.detail = detail;
    }
}

item-list.jsp

 路徑:src/webapp/WEB-INF/jsp/item/item-list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>查詢商品列表</title>
</head>
<body>
    <form action="${pageContext.request.contextPath }/itemList.do"
        method="post">
        查詢條件:
        <table width="100%" border=1>
            <tr>
                <td><input type="submit" value="查詢" /></td>
            </tr>
        </table>
        商品列表:
        <table width="100%" border=1>
            <tr>
                <td>商品名稱</td>
                <td>商品價格</td>
                <td>商品描述</td>
                <td>操作</td>
            </tr>
            <c:forEach items="${itemList }" var="item">
                <tr>
                    <td>${item.name }</td>
                    <td>${item.price }</td>
                    <td>${item.detail }</td>
                    <td><a
                        href="${pageContext.request.contextPath }/itemEdit.do?id=${item.name}">修改</a></td>
                </tr>
            </c:forEach>

        </table>
    </form>
</body>

</html>

 項目結構圖

 運行

 完整項目

 SpringMVC 框架源碼分析

 框架結構

 程序入口

一、初始化Servlet

二、處理器映射,渲染頁面

 注:標記的方法體,跟蹤進去讀源碼就好啦!~~

默認配置文件

 

 

 

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
    org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
    org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

 

架構流程

  1. 用戶發送請求至前端控制器DispatcherServlet
  2. DispatcherServlet收到請求調用HandlerMapping處理器映射器
  3. 處理器映射器根據請求url找到具體的處理器,生成處理器對象及處理器攔截器(如果有則生成)一併返回給DispatcherServlet
  4. DispatcherServlet通過HandlerAdapter處理器適配器調用處理器
  5. HandlerAdapter執行處理器(handler,也叫後端控制器)
  6. Controller執行完成返回ModelAndView
  7. HandlerAdapter將handler執行結果ModelAndView返回給DispatcherServlet
  8. DispatcherServlet將ModelAndView傳給ViewReslover視圖解析器
  9. ViewReslover解析后返回具體View對象
  10. DispatcherServlet對View進行渲染視圖(即將模型數據填充至視圖種)
  11. DispatcherServlet響應用戶

 組件說明

 DispatcherServlet:前端控制器

   用戶請求到達前端控制器,它就相當於mvc模式中的C,DispatcherServlet是整個流程控制的中心,由它調用其他組件處理用戶的請求,DispatcherServlet的存在降低了組件之間的耦合性。

HandlerMapping:處理器映射器

   HandlerMapping負責根據用戶請求找到Handler即處理器,springmvc提供了不同的映射器實現不同的映射方式,例如:配置文件方式,實現接口方式,註解方式等。

Handler:處理器

  Handler是繼DispatcherServlet前端控制器的後端控制器,在DispatcherServlet的控制下,Handler對具體的用戶請求進行處理。

  由於Handler涉及到具體的用戶業務請求,所以一般情況需要程序員根據業務需求開發Handler。

HandlerAdapter:處理器適配器

  通過HandlerAdapter對處理器進行執行,這是適配器模式的應用,通過擴展適配器可以對更多類型的處理器進行執行。

 View Resolver:視圖解析器

  View Resolver負責將處理結果生成View視圖,View Resolver首先根據邏輯視圖名解析成物理視圖名即具體的頁面地址,再生成View視圖對象,最後對View進行渲染將處理結果通過頁面展示給用戶。

View:視圖

  springmvc框架提供了很多View視圖類型的支持,包括:jstlView、freemarkerView、pdfView等。我們最常用的視圖就是jsp。

  一般情況下需要通過頁面標籤或頁面模板技術將模型數據通過頁面展示給用戶,需要由程序員根據業務需求開發具體的頁面。

說明

  再springmvc的各個組件中,處理器映射器、處理器適配器、視圖解析器稱為springmvc的三大組件。需要用戶開發的組件有:處理器、視圖

三大組件配置(註解方式)

註解映射器和適配器

通過bean標籤配置

RequestMappingHandlerMapping:註解式處理器映射器

  對類中標記@ResquestMapping的方式進行映射,根據ResquestMapping定義的url匹配ResquestMapping標記的方法,匹配成功返回HandlerMethod對象給前端控制器,HandlerMethod對象中封裝url對應的方法Method。

配置如下:

<!--註解映射器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<!--註解適配器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>

通過mvc標籤配置(推薦)

<mvc:annotation-drivern />

  mvc:annotation-drivern標籤的作用,詳見AnnotationDrivenBeanDefinitionParser類的parse方法。分析源碼可知:mvc:annotation-drivern往spring容器中註冊以下的一些BeanDefinition

  • ContentNegotiationManagerFactoryBean
  • RequestMappingHandlerMapping
  • ConfigurableWebBindingInitializer
  • RequestMappingHandlerAdapter
  • CompositeUriComponentsContributorFactoryBean
  • ConversionServiceExposingInterceptor
  • MappedInterceptor
  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver
  • BeanNameUrlHandlerMapping
  • HttpRequestHandlerAdapter
  • SimpleControllerHandlerAdapter
  • HandlerMappingIntrospector

視圖解析器

再springmvc.xml文件配置如下:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <!-- 該視圖解析器,默認的視圖類就是JstlView,可以不寫 -->
        <property name="viewClass"
            value="org.springframework.web.servlet.view.JstlView" />
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
  • InternalResourceViewResolver:默認支持JSP視圖解析
  •  viewClass:JstlView表示JSP模板頁面需要使用JSTL標籤庫,所以classpath中必須包含jstl的相關jar 包。此屬性可以不設置,默認為JstlView
  • prefix suffix:查找視圖頁面的前綴和後綴,最終視圖的址為:前綴+邏輯視圖名+後綴,邏輯視圖名需要在controller中返回的ModelAndView指定,比如邏輯視圖名為hello,則最終返回的jsp視圖地址 “WEB-INF/jsp/hello.jsp”

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Python 深入淺出支持向量機(SVM)算法

相比於邏輯回歸,在很多情況下,SVM算法能夠對數據計算從而產生更好的精度。而傳統的SVM只能適用於二分類操作,不過卻可以通過核技巧(核函數),使得SVM可以應用於多分類的任務中。

本篇文章只是介紹SVM的原理以及核技巧究竟是怎麼一回事,最後會介紹sklearn svm各個參數作用和一個demo實戰的內容,盡量通俗易懂。至於公式推導方面,網上關於這方面的文章太多了,這裏就不多進行展開了~

1.SVM簡介

支持向量機,能在N維平面中,找到最明顯得對數據進行分類的一個超平面!看下面這幅圖:

如上圖中,在二維平面中,有紅和藍兩類點。要對這兩類點進行分類,可以有很多種分類方法,就如同圖中多條綠線,都可以把數據分成兩部分。

SVM做的,是找到最好的那條線(二維空間),或者說那個超平面(更高維度的空間),來對數據進行分類。這個最好的標準,就是最大間距

至於要怎麼找到這個最大間距,要找到這個最大間距,這裏大概簡單說一下,兩個類別的數據,到超平面的距離之和,稱之為間隔。而要做的就是找到最大的間隔。

這最終就變成了一個最大化間隔的優化問題。

2.SVM的核技巧

核技巧,主要是為了解決線性SVM無法進行多分類以及SVM在某些線性不可分的情況下無法分類的情況。

比如下面這樣的數據:

這種時候就可以使用核函數,將數據轉換一下,比如這裏,我們手動定義了一個新的點,然後對所有的數據,計算和這個新的點的歐式距離,這樣我們就得到一個新的數據。而其中,離這個新點距離近的數據,就被歸為一類,否則就是另一類。這就是核函數。

這是最粗淺,也是比較直觀的介紹了。通過上面的介紹,是不是和Sigmoid有點像呢?都是通過將數據用一個函數進行轉換,最終得到結果,其實啊,Sigmoid就是一鍾核函數來着,而上面說的那種方式,是高斯核函數。

這裏補充幾點:

  • 1.上面的圖中只有一個點,實際可以有無限多個點,這就是為什麼說SVM可以將數據映射到多維空間中。計算一個點的距離就是1維,2個點就是二維,3個點就是三維等等。。。
  • 2.上面例子中的紅點是直接手動指定,實際情況中可沒辦法這樣,通常是用隨機產生,再慢慢試出最好的點。
  • 3.上面舉例這種情況屬於高斯核函數,而實際常見的核函數還有多項式核函數,Sigmoid核函數等等。

OK,以上就是關於核技巧(核函數)的初步介紹,更高級的這裏也不展開了,網上的教程已經非常多了。

接下來我們繼續介紹sklearn中SVM的應用方面內容。

3.sklearn中SVM的參數

def SVC(C=1.0, 
             kernel='rbf', 
             degree=3, 
             gamma='auto_deprecated',
             coef0=0.0, 
             shrinking=True, 
             probability=False,
             tol=1e-3, 
             cache_size=200, 
             class_weight=None,
             verbose=False, 
             max_iter=-1, 
             decision_function_shape='ovr',
             random_state=None)
 
- C:類似於Logistic regression中的正則化係數,必須為正的浮點數,默認為 1.0,這個值越小,說明正則化效果越強。換句話說,這個值越小,越訓練的模型更泛化,但也更容易欠擬合。
- kernel:核函數選擇,比較複雜,稍後介紹
- degree:多項式階數,僅在核函數選擇多項式(即“poly”)的時候才生效,int類型,默認為3。
- gamma:核函數係數,僅在核函數為高斯核,多項式核,Sigmoid核(即“rbf“,“poly“ ,“sigmoid“)時生效。float類型,默認為“auto”(即值為 1 / n_features)。
- coef0:核函數的獨立項,僅在核函數為多項式核核Sigmoid核(即“poly“ ,“sigmoid“)時生效。float類型,默認為0.0。獨立項就是常數項。
- shrinking:不斷縮小的啟髮式方法可以加快優化速度。 就像在FAQ中說的那樣,它們有時會有所幫助,有時卻沒有幫助。 我認為這是運行時問題,而不是收斂問題。
- probability:是否使用概率評估,布爾類型,默認為False。開啟的話會評估數據到每個分類的概率,不過這個會使用到較多的計算資源,慎用!!
- tol:停止迭代求解的閾值,單精度類型,默認為1e-3。邏輯回歸也有這樣的一個參數,功能都是一樣的。
- cache_size:指定使用多少內存來運行,浮點型,默認200,單位是MB。
- class_weight:分類權重,也是和邏輯回歸的一樣,我直接就搬當時的內容了:分類權重,可以是一個dict(字典類型),也可以是一個字符串"balanced"字符串。默認是None,也就是不做任何處理,而"balanced"則會去自動計算權重,分類越多的類,權重越低,反之權重越高。也可以自己輸出一個字典,比如一個 0/1 的二元分類,可以傳入{0:0.1,1:0.9},這樣 0 這個分類的權重是0.1,1這個分類的權重是0.9。這樣的目的是因為有些分類問題,樣本極端不平衡,比如網絡攻擊,大部分正常流量,小部分攻擊流量,但攻擊流量非常重要,需要有效識別,這時候就可以設置權重這個參數。
- verbose:輸出詳細過程,int類型,默認為0(不輸出)。當大於等於1時,輸出訓練的詳細過程。僅當"solvers"參數設置為"liblinear"和"lbfgs"時有效。
- max_iter:最大迭代次數,int類型,默認-1(即無限制)。注意前面也有一個tol迭代限制,但這個max_iter的優先級是比它高的,也就如果限制了這個參數,那是不會去管tol這個參數的。
- decision_function_shape:多分類的方案選擇,有“ovo”,“ovr”兩種方案,也可以選則“None”,默認是“ovr”,詳細區別見下面。
- random_state:隨時數種子。

sklearn-SVM參數,kernel特徵選擇

kernel:核函數選擇,字符串類型,可選的有“linear”,“poly”,“rbf”,“sigmoid”,“precomputed”以及自定義的核函數,默認選擇是“rbf”。各個核函數介紹如下:
“linear”:線性核函數,最基礎的核函數,計算速度較快,但無法將數據從低維度演化到高維度
“poly”:多項式核函數,依靠提升維度使得原本線性不可分的數據變得線性可分
“rbf”:高斯核函數,這個可以映射到無限維度,缺點是計算量比較大
“sigmoid”:Sigmoid核函數,對,就是邏輯回歸裏面的那個Sigmoid函數,使用Sigmoid的話,其實就類似使用一個一層的神經網絡
“precomputed”:提供已經計算好的核函數矩陣,sklearn不會再去計算,這個應該不常用
“自定義核函數”:sklearn會使用提供的核函數來進行計算
說這麼多,那麼給個不大嚴謹的推薦吧
樣本多,特徵多,二分類,選擇線性核函數
樣本多,特徵多,多分類,多項式核函數
樣本不多,特徵多,二分類/多分類,高斯核函數
樣本不多,特徵不多,二分類/多分類,高斯核函數

當然,正常情況下,一般都是用交叉驗證來選擇特徵,上面所說只是一個較為粗淺的推薦。

sklearn-SVM參數,多分類方案

其實這個在邏輯回歸裏面已經有說過了,這裏還是多說一下。

原始的SVM是基於二分類的,但有些需求肯定是需要多分類。那麼有沒有辦法讓SVM實現多分類呢?那肯定是有的,還不止一種。

實際上二元分類問題很容易推廣到多元邏輯回歸。比如總是認為某種類型為正值,其餘為0值

舉個例子,要分類為A,B,C三類,那麼就可以把A當作正向數據,B和C當作負向數據來處理,這樣就可以用二分類的方法解決多分類的問題,這種方法就是最常用的one-vs-rest,簡稱OvR。而且這種方法也可以方便得推廣到其他二分類模型中(當然其他算法可能有更好的多分類辦法)。

另一種多分類的方案是Many-vs-Many(MvM),它會選擇一部分類別的樣本和另一部分類別的樣本來做二分類

聽起來很不可思議,但其實確實是能辦到的。比如數據有A,B,C三個分類。

我們將A,B作為正向數據,C作為負向數據,訓練出一個分模型。再將A,C作為正向數據,B作為負向數據,訓練出一個分類模型。最後B,C作為正向數據,C作為負向數據,訓練出一個模型。

通過這三個模型就能實現多分類,當然這裏只是舉個例子,實際使用中有其他更好的MVM方法。限於篇幅這裏不展開了。

MVM中最常用的是One-Vs-One(OvO)。OvO是MvM的特例。即每次選擇兩類樣本來做二元邏輯回歸。

對比下兩種多分類方法,通常情況下,Ovr比較簡單,速度也比較快,但模型精度上沒MvM那麼高。MvM則正好相反,精度高,但速度上比不過Ovr。

4.sklearn SVM實戰

我們還是使用鳶尾花數據集,不過這次只使用其中的兩種花來進行分類。首先準備數據:

import matplotlib.pyplot as plt
import numpy as np
from sklearn import svm,datasets
import pandas as pd
tem_X = iris.data[:, :2]
tem_Y = iris.target
new_data = pd.DataFrame(np.column_stack([tem_X,tem_Y]))
#過濾掉其中一種類型的花
new_data = new_data[new_data[2] != 1.0]
#生成X和Y
X = new_data[[0,1]].values
Y = new_data[[2]].values

然後用數據訓練,並生成最終圖形


# 擬合一個SVM模型
clf = svm.SVC(kernel='linear')
clf.fit(X, Y)

# 獲取分割超平面
w = clf.coef_[0]
# 斜率
a = -w[0] / w[1]
# 從-5到5,順序間隔採樣50個樣本,默認是num=50
# xx = np.linspace(-5, 5)  # , num=50)
xx = np.linspace(-2, 10)  # , num=50)
# 二維的直線方程
yy = a * xx - (clf.intercept_[0]) / w[1]
print("yy=", yy)

# plot the parallels to the separating hyperplane that pass through the support vectors
# 通過支持向量繪製分割超平面
print("support_vectors_=", clf.support_vectors_)
b = clf.support_vectors_[0]
yy_down = a * xx + (b[1] - a * b[0])
b = clf.support_vectors_[-1]
yy_up = a * xx + (b[1] - a * b[0])

# plot the line, the points, and the nearest vectors to the plane
plt.plot(xx, yy, 'k-')
plt.plot(xx, yy_down, 'k--')
plt.plot(xx, yy_up, 'k--')

plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1], s=80, facecolors='none')


plt.scatter(X[:, 0].flat, X[:, 1].flat, c='#86c6ec', cmap=plt.cm.Paired)
# import operator
# from functools import reduce
# plt.scatter(X[:, 0].flat, X[:, 1].flat, c=reduce(operator.add, Y), cmap=plt.cm.Paired)

plt.axis('tight')
plt.show()

最終的SVM的分類結果如下:

以上~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

PHP安全之道學習筆記2:編碼安全指南

編碼安全指南

編程本身就應該是一門藝術,而安全編程更是一種在刀尖上舞蹈的藝術,不僅要小心腳下的鋒利寒刃,更要小心來自網絡黑客或攻擊者的狂轟亂炸。
– by code artist

  • 1.hash比較的缺陷
    經過試驗發現,當Hash值以”0e”開頭且後面都為数字,當和数字進行比較的時候總會被判斷和0相等

例如:

var_dump(‘0e1327544’ == 0); // bool(true)

當密碼被md5計算后,可能會以”0e”開頭,下面這個例子可以繞過密碼驗證。
經過我的驗證PHP 7.1.x后沒有這種問題。

<?php
    $password_from_db = "0e23434";
    $password = "2323"; // 隨意的一個密碼。來自$_POST,即表單提交
    if ($password_from_db == md5($password)) {
        echo "login success!";
    } else {
        echo "login fails";
    }

更安全的hash比較:
可以使用內置函數hash_equals()來比較hash值。(PHP版本必須是5.6及其以上)

 if (hash_equals($password_from_db, md5($password)) {
     .....// other logic
 }
  • 2.bool比較的缺陷

json_decode和unserialize函數可能將部分結構解析成bool值,造成一些比較上的缺陷。

先舉例json_decode的案例:

<?php
$str = '{"user":true, "pass": true}';

$data = json_decode($str, true);

if ($data['user'] == 'root' && $data['pass'] == 'pass') {
    echo "login success\r\n";
} else {
    echo "login fails\r\n";
}

執行結果為:login success
這樣利用bool比較的漏洞就繞過了登錄或者授權驗證。

unserialize過程相逆,結果類似,也會出現安全問題。

正確的做法還是使用”===”來進行比較,這不光是php,包括一些其他腳本語言或者靜態語言,都請嚴謹地使用全等於符號進行比較。

  • 3.數值比較

PHP雖然是弱類型語言,但是數據類型也有數值範圍。對於整型而言,最大值為PHP_INT_MAX(即9223372036854775807)
攻擊者可以利用最大值越界,繞過一些驗證,如登錄、賬號充值等等。

舉例:

$a = 9223372036854775807;
$b = 9223372036854775827;
var_dump($a === $b); // bool(true)
var_dump($a % 100); // int(0)

由此,可見全等號(===)也不是萬能的,具體場景下要更小心。經驗證,PHP7.1.x后不會出現該問題,5.x的可能出現。

在實際業務邏輯裏面一定要注意判斷最大值問題,避免越界帶來的問題。

當使用超長浮點數變量的時候,PHP也會出錯。

<?php
$uid = 0.999999999999999999;
if ($uid == "1") {
    echo "search uid is 1 for data\r\n"; // 這裏PHP將$uid約等於1了,進入該判斷條件里的邏輯
}

同理,2.999999999999也會被當成3,這就是超越浮點數精度造成的偏差。

解決辦法有很多,最簡單的就是用is_int()函數進行判斷,如果不是整型,則報錯或做錯誤處理。

  • 4.switch缺陷

當用case判斷数字的時候,switch會把參數轉換成int類型進行計算,代碼如下:

<?php
$num = "1FreePHP";
switch ($num) {
    case 0: echo "nothing";
        break;
    case 1: echo "1 hacker here!";
        break;
    case 2: echo "2 hackers here";
        break;
    default:
        echo "confused";
}

最後輸出:1 hacker here!

所以,請使用is_numeric()函數進行判斷,保證數據類型如預期的一致。

  • 5.數組缺陷。
    in_array()和array_search()函數在沒有使用嚴格模式的情況下會用鬆散比較,可能造成一些錯誤。
    例如:
<?php
$arr = [0, 2, 3, "4"];
var_dump(in_array('freephp', $arr)); // true
var_dump(array_search('freephp', $arr)); // 0: 下標
var_dump(in_array('2freephp', $arr)); // true
var_dump(array_search('3freephp', $arr)); // 2: 下標

總的來說,PHP工程師對於這種弱類型語言的使用上要更加小心,雖然平時寫起業務來“短平快”,但安全編程也不要忘記,能用上hint的高版本PHP就進行標註清楚入參、出參,讓PHP代碼更加健壯。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

HTTP基礎及telnet簡單命令

一、HTTP概況

 

  20世紀90年代初期,一個主要的新興應用即萬維網(World Wide Web)登上了舞台。Web是一個引起公眾注意的因特網應用。Web的應用層協議是超文本傳輸協議(HTTP),它是Web的核心。HTTP由兩個程序實現:一個客戶程序和一個服務器程序。客戶程序和服務器程序運行在不同的端系統中,通過交換HTTP報文進行會話。HTTP會話定義了這些報文的結構以及客戶和服務器進行報文交換的方式。

  Web頁面(也叫文檔)是由對象組成的。一個對象只是一個文件,諸如一個HTML文件、一個JPEG圖形、一個Java小程序或一個視頻片段這樣的文件,且他們可通過一個URL地址尋址。多數Web頁面含有一個HTML基本文件以及幾個引用對象。例如,如果一個Web頁面包含HTML基本文件和5個JPEG圖形,那麼這個Web頁面6個對象:一個HTML基本文件加5個圖形。HTML基本文件通過對象的URL地址引用頁面中的其他對象。每個URL地址由兩部分組成:存放對象的服務器主機名和對象的路徑名。Web瀏覽器實現了HTTP的客戶端,Web服務器實現了HTTP的服務器端,它用於存儲Web對象,每個對象由URL尋址。

  HTTP定義了Web客戶向Web服務器請求Web頁面的方式,以及服務器向客戶傳送Web頁面的方式,其基本思想就是當用戶請求一個Web頁面(如點擊一個超鏈接)時,瀏覽器向服務器發出對該頁面中所包含對象的HTTP請求報文,服務器接收到請求並用包含這些對象的HTTP響應報文進行響應。

  HTTP使用TCP作為它的支撐運輸協議(而不是在UDP上運行)。HTTP客戶首先發起一個與服務器的TCP連接。一旦連接建立,該瀏覽器和服務器進程就可以通過套接字接口訪問TCP。客戶向它的套接字接口發送HTTP請求報文並從它的套接字接口接收HTTP響應報文。類似的,服務器從它的套接字接口接收HTTP請求報文和向它的套接字接口發送HTTP響應報文。一旦客戶向他的套接字接口發送了一個請求報文,該報文就脫離了客戶控制並進入TCP的控制。TCP為HTTP提供可靠數據傳輸服務。這意味着,一個客戶進程發出的每個HTTP請求報文最終能完整地到達服務器;類似的,服務器進程發出的每個HTTP響應報文最終能完整地到達客戶。

  注意到下列現象很重要:服務器向客戶發送被請求的文件,而不存儲任何關於該客戶的狀態信息。假如某個特定的客戶在短短的幾秒鐘內兩次請求同一個對象,服務器並不會因為剛剛為該客戶提供了該對象就不再做出反應,而是重新發送該對象,就像服務器已經完全忘記不久之前所做過的事一樣。因為HTTP服務器並不保存關於客戶的任何信息,所以我們說HTTP是一個無狀態協議

 

二、非持續連接和持續連接

 

  在許多因特網應用程序中,客戶和服務器在一個相當長的時間範圍內通信,其中客戶發出一系列請求並且服務器對每個請求進行響應。依據應用程序以及該應用程序的使用方式,這一系列請求可以以規則的間隔周期性的或者間斷性的一個接一個發出。當這種客戶-服務器的交互是經TCP進行的,應用程序的研製者就要做一個重要決定,即每個請求/響應對是經一個單獨的TCP連接發送,還是所有的請求及其相應經相同的TCP連接發送呢?採用前一種方法,該應用程序被稱為使用非持續連接;採用后一種方法,該應用程序被稱為使用持續連接。如HTTP既能夠使用非持續連接,也能夠使用持續連接。儘管HTTP在默認方式下使用持續連接,HTTP客戶和服務器也能配置成非持續連接。

1.採用非持續連接的HTTP

  我們看看在非持續連接情況下,從服務器向客戶傳送一個Web頁面的步驟。假設該頁面含有一個HTML基本文件和10個JPEG圖形,並且這11個對象位於同一台服務器上。該HTML文件的URL為:我們看看發生了什麼情況:

  • HTTP客戶進程在端口號80發起一個到服務器的TCP連接,該端口號是HTTP的默認端口。在客戶和服務器上分別有一個套接字與該連接相關聯。

  • HTTP客戶經它的套接字向該服務器發送一個HTTP請求報文。請求報文中包含了路徑名/someDepartment/home.index。

  • HTTP服務器進程經它的套接字接收該請求報文,從其存儲器(RAM或磁盤)中檢索出對象,在一個HTTP響應報文中封裝對象,並通過其套接字向客戶發送響應報文。

  • HTTP服務器進程通知TCP斷開該TCP連接。(但是直到TCP確認客戶已經完整的收到響應報文為止,它才會實際中斷連接。

  • HTTP客戶接收響應報文,TCP連接關閉。該報文指出封裝的對象是一個HTML文件,客戶從響應報文中提取出該文件,檢查該HTML文件,得到對10個JPEG圖形的引用。

  • 對每個引用的JPEG圖形對象重複前4個步驟。

  上面的步驟舉例說明了非持續連接的使用,其中每個TCP連接在服務器發送一個對象后關閉,即該連接並不為其他的對象而持續下來。值得注意的是每個TCP來接只傳輸一個請求報文和響應報文。

     在上面描述的步驟中,我們有意沒有明確客戶獲得這10個JPEG圖形對象是使用10個串行的TCP連接,還是某些JPEG對象使用了一些并行的TCP連接。事實上,用戶能配置現代瀏覽器以控制并行度。在默認方式下,大部分瀏覽器打開5~10個并行的TCP連接,而每條連接處理一個請求響應事務。如果用戶願意,最大并行連接數可以設置為1,這樣10條連接就會串行建立。

  我們來簡單估算一下從客戶請求HTML基本文件起到該客戶收到整個文件止所花費的時間。為此,我們給出往返時間(Round-Trip Time,RTT)的定義,該時間是指一個短分組從客戶到服務器然後再返回客戶所花費的時間。RTT包括分組傳播時延、分組在中間路由器和交換機上的排隊時延以及分組處理時延。現在考慮當用戶點擊超鏈接時會發生什麼現象。如圖2-7所示,這引起瀏覽器在它和Web服務器之間發起一個TCP連接;這涉及一次“三次握手”過程。即客戶向服務器發送一個小TCP報文段,服務器用一個小TCP報文段做出確認和響應,最後,客戶向服務器返回確認。三次握手中前兩個部分所耗費的時間佔用了一個RTT。完成了三次握手的前兩個部分后,客戶結合三次握手的第三部分(確認)向該TCP連接發送一個HTTP請求報文。一旦該請求報文到達服務器,服務器就在該TCP連接上發送HTML文件。該HTTP請求/響應用去了另一個RTT。因此,粗略地將,總的響應時間就是兩個RTT加上服務器傳輸HTML文件的時間。

2.採用持續連接的HTTP

  非持續連接有一些缺點。首先,必須為每一個請求的對象建立和維護一個全新的連接。對於每個這樣的連接,在客戶和服務器中都要分配TCP的緩衝區和保持TCP變量,這給Web服務器帶來了嚴重的負擔,因為一台Web服務器可能同時服務於數以百計不同的客戶的請求。第二,就像我們剛描述的那樣,每一個對象經受兩倍RTT的交付時延,即一個RTT用於創建TCP,另一個RTT用於請求和接收一個對象。

  在採用持續連接的情況下,服務器在發送響應后保持該TCP連接打開。在相同的客戶與服務器之間的後續請求和響應報文能夠通過相同的連接進行傳送。特別是,一個完整的Web頁面(上例中的HTML基本文件加上10個圖形)可以用單個持續TCP連接進行傳送。更有甚者,位於同一台服務器的多個Web頁面在從該服務器發送給同一個客戶時,可以在單個持續TCP連接上進行。可以一個接一個地發出對對象的這些請求,而不必等待對未決請求(流水線)的回答。一般來說,如果一條連接經過一定的時間間隔(一個可配置的超時間隔)仍未被使用,HTTP服務器就關閉該連接。HTTP的默認模式是使用帶流水線的持續連接。

三、HTTP報文格式

  HTTP報文有兩種:請求報文和響應報文。

1.HTTP請求報文

  下面提供了一個典型的HTTP請求報文:

GET /somedir/page.html HTTP/1.1

Host:

Connection: close

User-agent: Mozilla/5.0

Accept-language: fr

  通過仔細觀察這個簡單的請求報文,我們就能知道很多東西。首先,我們看到該報文是用普通的ASCII文本書寫的,我們看到該報文由5行組成,每行由一個回車和換行符結束。最後一行后再附加一個回車換行符。一個請求報文能夠具有更多的行或者至少為一行。請求行的方法字段可以取幾種不同的值,包括GET、POST、HEAD、PUT和DELETE。當瀏覽器請求一個對象時,使用GET方法,在URL字段帶有請求對象的標識,在本例中,該瀏覽器正在請求對象/somedir/page.html。其版本字段是自解釋的;在本例中,瀏覽器實現的是HTTP/1.1版本。現在我們看看本例的首部行。首部行Host: 指明了對象所在的主機。你也許認為該首部行是不必要的,因為在該主機中已經有一條TCP連接存在了,但是,該首部行提供的信息是Web代理高速緩存所要求的。通過包含Connection: close首部行,該瀏覽器告訴服務器不希望麻煩地使用持續連接,它要求服務器在發送完被請求的對象后就關閉這條連接。User-agent: 首部行用來指明用戶代理,即向服務器發送請求的瀏覽器類型。這裏瀏覽器類型是Mozilla/5.0,即Firefox瀏覽器。這個首部行是有用的,因為服務器可以有效地為不同類型的用戶代理實際發送相同對象的不同版本。(每個版本都由相同的URL尋址。)最後,Accept-language: 首部行表示用戶想得到該對象的法語版本。如果服務器中沒有這樣的對象的話,服務器應當發送它的默認版本。

  接下來看看如圖2-8所示的一個請求報文的通用格式。你可能注意到了在首部行(和附加的回車和換行)後有一個“實體主體”。使用GET方法是實體主體為空,而使用POST方法時才使用該實體主體。當用戶提交表單時,HTTP客戶常常使用POST方法,例如當用戶向搜索引擎提供搜索關鍵詞時。使用POST報文時,用戶仍可以向服務器請求一個Web頁面,但Web頁面的特定內容依賴於用戶在表單字段中輸入的內容。如果方法字段的值為POST時,則實體主體中包含的就是用戶在表單字段中的輸入值。

  當然,如果不提“用表單生成的請求報文不是必須使用POST方法”這一點,那將是失職。HTML表單經常使用GET方法,並在(表單字段中)所請求的URL中包括輸入的數據。例如,一個表單使用GET方法,它有兩個字段,分別填寫的是“monkeys”和“bananas”,這樣,該URL結構為? monkeys&bananas。

  HEAD方法類似GET方法。當服務器收到使用HEAD方法的請求時,將會用一個HTTP報文進行響應,但是並不返回請求對象。應用程序開發者常用HEAD方法進行調試跟蹤。PUT方法常與Web發行工具聯合使用,它允許用戶上傳對象到指定的Web服務器上指定的路徑(目錄)。PUT也被那些需要向Web服務器上傳對象的應用程序使用。DELETE方法允許用戶或者應用程序刪除Web服務器上的對象。

2.HTTP響應報文

  下面我們提供了一條典型的HTTP響應報文。該響應報文可以是對剛剛討論的例子中請求報文的響應。

HTTP/1.1 200 OK

Connection: close

Date: Tue, 09 Aug 2011 15:44:04 GMT

Server: Apache/2.2.3 (CentOS)

Last-Modified: Tue, 09 Aug 2011 15:11:03 GMT

Content-Length: 6821

Content-Type: text/html

(data data data data data …)

  我們仔細看這個響應報文。實體主體部分是報文的主要部分,即它包含了所請求的對象本身(表示為data data data data data …)。我們現在來看看首部行。服務器用Connection:close首部行告訴客戶,發送完報文後將關閉該TCP連接。Date:首部行指示服務器產生併發送該響應報文的日期和時間。值得一提的是,這個時間不是指對象創建或者最後修改的時間;而是服務器從它的文件系統中檢索到該對象,插入到響應報文,併發送響應報文的時間。Server:首部行指示該報文是由一台Apache Web服務器產生的,它類似於HTTP請求報文中的User-agent:首部行,Last-Modified:首部行指示了對象創建或者最後修改的日期和時間。Last-Modified:首部行對極可能在本地客戶也可能在網絡緩存服務器(代理服務器)上的對象緩存來說非常重要。Content-Length:首部行知識了被發送對象中的字節數。Content-Type:首部行指示了實體主體中的對象是HTML文本。(該對象類型應該正式地由Content-Type:首部行而不是用文件擴展名來指示。)

  看過一個例子后,我們再來查看響應報文的通用格式(如圖2-9所示)。我們補充說明一下狀態碼和它們對應的短語。狀態碼及其相應的短語指示了請求的結果。一些常見的狀態碼和相關的短語包括:

  • 200 OK:請求成功,信息在返回的響應報文中。

  • 301 Moved Permanently:請求的對象已經被永久轉移了,新的URL定義在響應報文的Location:首部行中。**客戶軟件將自動獲取新的URL。

  • 400 Bad Request:一個通用差錯代碼,指示該請求不能被服務器理解。

  • 404 Not Found:被請求的文檔不在服務器上。

  • 505 HTTP Version Not Supported:服務器不支持請求報文使用的HTTP協議版本。

  你想看一下真正的HTTP響應報文嗎?很容易做到。首先用Telnet登錄到你喜歡的Web服務器上,接下來輸入一個只有一行的請求報文去請求放在該服務器上的某些對象。

  在linux終端輸入完telnet 80后,會是下面這種情況:

  然後按下ctrl + ]呼出telnet命令行出現下面這種情況:

  先按下回車鍵,再輸入HTTP請求,最終得到HTTP響應如下:

  在telnet命令行上輸入quit退出telnet,如下圖:

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

軟件開發要質量還是要效率?

質量和效率似乎永遠都是一對冤家,儘管我們都希望既有質量,又有效率。

把“質量”當做宗旨的企業,通常都有一系列的規章制度,甚至是繁重且冗餘的流程用來約束軟件開發過程中種種“有意”或“無意”的威脅軟件質量的行為。

把“效率”當做宗旨的企業,通常其內部並無嚴格的規章制度,甚至寬鬆到一個人都可以輕鬆地完成從刪庫到跑路。

從事IT行業的相關人員大多知道,軟件開發不同於一般性的勞動,它並不能單純地增加人手就能縮減開發周期,也就是說一個軟件1個人開發需要10天,這並不意味着10個人就可以1天開發完成。並且在軟件開發的過程中,由於需要“適應市場的快速發展”,常常伴隨需求變更等不可預知的問題。也就是在前期所做的工作可能因為某個需求而全部推倒重來。

下面從要質量還是要效率兩個方面來闡述,不同的側重點所帶來的的問題。

我們首先假設,公理P1:作為IT行業的從業者(開發、測試、產品等)都知道,軟件開發具有一定的不可預知性

那麼在這個前提下,傾向於“質量”的企業通常情況下有以下做法:

  • 通過規章制度讓軟件開發具有一定的可預知性

讓軟件開發具有一定的可預知性,這種方式有很多種實現,比較常見的手段是讓需求變更的成本上升。一旦進入開發階段(含設計階段),需求不得隨意變更,這種方式對開發人員相對比較友好,開發人員不再被隨意變更的需求所打擾,但同時也對產品經理提出了更多的要求。這要求產品經理需要有高超的業務能力,以及一定的前瞻性。除了讓需求變更的成本上升以外,通常也會在前期做大量的工作,包括需求評審、文檔設計、設計評審等會議,在軟件開發的中後期不斷地進行代碼評審等工作。這一系列的規章制度流程,能使得軟件開發不再隨心所欲,而是有章可循。顯而易見,這樣“傳統”的開發形式,勢必帶來效率的下降。例如我曾經見過有的公司,一年最多發布2個版本。這在如今快速的互聯網發展中是不可接受的。

而傾向於“效率”的企業,也就是通常所說的互聯網公司對於效率的提升通常採取以下手段:

  • 通過縮短開發周期使軟件開發具有一定的可預知性

目前在部分互聯網公司所倡導的“敏捷開發”實際上就是通過縮短開發周期來使軟件具有一定的可預知性。我們在開頭假設了了公理P1,軟件開發具有一定的不可預知性。並且開發周期越長,不可預知性越大。注重質量的公司,可能更傾向於提高需求變更的成本,而注重效率的公司則縮短開發周期。兩者都是為了使得軟件開發變得可控。但兩個不同的方式則導致了兩個不同的傾向。

縮短開發周期的確會讓效率變得更高,起碼能更快的適應市場的需求。那為什麼會說縮短開發周期會使得質量降低呢?

其實這是一個顯而易見的道理,縮短開發周期,理論上來講似乎就能縮短開發時間。10個需求需要做10天,平均1個需求不就只需要1天嗎?那麼我為了提高我的效率,快速響應市場變化,我就採取敏捷開發的方式,這樣不就既滿足了效率,同時也滿足了開發時間,這樣的做法似乎並不會降低軟件開發的質量。這麼想的通常是沒有從事過技術研發的同學。仍然回到公理P1,軟件開發具有一定的不可預知性。我在做當前開發的時候,所採取的的設計基本上只適用於當前的業務模型,對於未來幾乎一無所知。隨着系統不斷地快速迭代,一次又一次的在原有的系統上疊加新的功能修改刪除舊的功能。這對於軟件開發者可以說是災難性的,沒有哪一個系統架構師能遇見未來的所有可能。“天下武功唯快不破”,快是快了,代碼後院也快起火了。

天底下沒有公司敢說我不注重質量,我只注重效率。無論是什麼公司都會採取以下手段去保證軟件質量。

  • 通過一定的經濟利益懲罰手段

一定的懲罰手段,簡單粗暴地將開發人員的bug數與績效掛鈎。不過直接將bug數與績效掛鈎的情況比較少,大多情況是bug的reopen次數,以及是否有新引入的bug。其中reopen是較為常見的一種懲罰手段,同樣也能較好地推動軟件質量提升。

事實上,並沒有哪一種絕對完美的兼顧了質量和效率,對於目前的互聯網公司大多所採用的是快速迭代的開發方式。但這並不代表採用這種方式的公司質量就一定低下。

“快速適應市場的變化”這本身也是一種需求,採取快速迭代的方式實際上也是為了滿足這一“需求”。阿里巴巴集團CTO行癲曾談到過,“最早,業務比技術跑的快,技術一直追業務,因為業務增長實在太快了。前兩年我覺得是技術推動業務,特別是人工智能興起的之後,包括我們程序化交易、廣告平台、千人千面、推薦、搜索大量用算法和AI,包括客服等等大量用數據智能在驅動業務”。

“業務比技術跑得快”,這意味着一定一個快速迭代的過程。而後來“技術推動業務”,意味着技術走在了業務的前面,反倒是技術追着業務打。這其中儘管並未提及質量,但我認為技術能推動業務不斷向前跑,一定是因為有堅實的技術後盾做支撐,而堅實的技術後盾也就意味着有超高的軟件質量

所以,在質量與效率的權衡利弊平衡中,不妨回過頭來重新審視技術的重要性。在滿足“市場快速變化”這一需求的同時,不要忘記技術也會負債,欠得越多越不牢靠。

這是一個能給程序員加buff的公眾號 (CoderBuff)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!