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收購-全台皆可收購

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

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

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

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

Rust 入門 (二)

我認為學習計算機語言,應該先用後學,這一節,我們來實現一個猜数字的小遊戲。

先簡單介紹一個這個遊戲的內容:遊戲先生成一個1到100之間的任意一個数字,然後我們輸入自己猜測的数字,遊戲會告訴我們輸入的数字太大還是太小,然後我們重新輸入新的数字,直到猜到遊戲生成的数字,然後遊戲結束。

創建項目

製作遊戲的第一步先創建項目,創建方法和上一節一樣,使用 cargo 來創建一個名為 guessing_game 的項目。

cargo new guessing_game && cd guessing_game

項目創建完成,可以運行一下,如果程序打印出 Hello, World! 則證明程序創建完成,運行命令如下:

cargo run 

讀取猜測的数字

正式寫遊戲的第一步,讓遊戲先讀取我們猜測的数字。我們可以先把打印語句換成提示我們輸入数字的提示語句。

use std::io;

fn main() {
    println!("猜測数字遊戲,請輸入您猜測的数字。");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess).expect("讀取数字失敗!");

    println!("您猜測的数字是:{}", guess);
}

這段代碼包含了大量的信息,我們一行一行地過一遍。
1.因為我們需要讀取用戶的輸入,然後把它作為結果打印出來,所以需要把 標準庫(被稱作 std )中的 io 依賴引入當前作用域。
2.在主函數中寫方法體,首先是打印提示語,不說了。
3.然後創建一個用於保存即將輸入的字符串的 String 類型的變量 guess。
4.把控制台輸入的数字讀取到變量 guess 中,如果讀取失敗,則打印 “讀取数字失敗!” 的字符串。
5.把讀取的数字再打印到控制台。

注:這段程序的細節暫時先不深究了,後續文章會一一解釋清楚。

測試一下這段程序:

cargo run                                    
   Compiling guessing_game v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.01s
     Running `target/debug/guessing_game`
猜測数字遊戲,請輸入您猜測的数字。
2
您猜測的数字是:2

生成隨機數

我們的遊戲需要創建一個隨機數,供我們去猜測,這個数字要求每次啟動遊戲時都是不相同的,這樣遊戲才更加有意思。接下來我們在遊戲中生成一個1到100的隨機數。但是 rust 沒有在它的標準庫中提供生成隨機數的方法,不過沒關係,它提供了生成隨機數的名為 rand 的 crate。我們來引入一下生成隨機數的 crate,修改 Cargo.toml 文件:

[dependencies]

rand = "^0.3.14"

只需要在 [dependencies] 下面添加需要的 crate 即可。這次添加的 crate 名字是 rand,版本號 0.3.14, 而 ^ 的意思是兼容 0.3.14 版本的任何版本都可以。然後我們編譯一下程序,就會自動下載引入的依賴:

cargo build                                      
    Updating crates.io index
   Compiling libc v0.2.65
   Compiling rand v0.4.6
   Compiling rand v0.3.23
   Compiling guessing_game v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1m 13s

引入了生成隨機數和 crate 后,我們來生成一下需要的 crate,代碼如下:

use std::io;
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("生成的隨機数字是:{}", secret_number);

    println!("猜測数字遊戲,請輸入您猜測的数字。");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess).expect("讀取数字失敗!");

    println!("您猜測的数字是:{}", guess);
}

可以看到我們在前面代碼的基礎上添加了三行代碼:
1.第一行是引入生成隨機數的依賴。
2.第二行是生成一個隨機數,隨機數的範圍是 [1, 101),區間是左閉右開,說人話就是1到100。
3.第三行是打印生成的隨機數。
然後我們測試一下添加的隨機數是否生效:

cargo run                                    
   Compiling guessing_game v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/guessing_game`
生成的隨機数字是:79
猜測数字遊戲,請輸入您猜測的数字。
6
您猜測的数字是:6

比較隨機數和猜測數

現在我們可以輸入自己猜測的数字,也可以生成隨機数字了,那麼接下來就是比較二者的大小了。但是在比較之前還有個問題,控制台輸入的数字是 string 類型的,而隨機生成的数字是無符號32位整型(u32),二者不類型不一致,不能作比較,因此,在比較之前,我們應該先把控制台輸入的 string 類型的数字轉成u32類型的,代碼如下:

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("生成的隨機数字是:{}", secret_number);

    println!("猜測数字遊戲,請輸入您猜測的数字。");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess).expect("讀取数字失敗!");

    let guess: u32 = guess.trim().parse().expect("請輸入一個数字!");

    println!("您猜測的数字是:{}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("您猜測的数字太小了!"),
        Ordering::Greater => println!("您猜測的数字太大了!"),
        Ordering::Equal => println!("恭喜您,猜對了!"),
    }
}

可見,我們在三個位置添加了代碼:
1.從標準庫中添加了比較的依賴。
2.把輸入的数字類型成u32類型,如果輸入的不是数字,則轉換失敗,打印出錯誤信息。
3.最後一部分就是比較一下二者的大小,並打印出比較的結果。
好了,我們先測試一下吧,這裏我們只測正確的輸入:

cargo run                                     101 ↵
   Compiling guessing_game v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/guessing_game`
生成的隨機数字是:53
猜測数字遊戲,請輸入您猜測的数字。
4
您猜測的数字是:4
您猜測的数字太小了!

添加循環

我們發現,我們只輸入了一次,遊戲就結束了,這顯然不符合我們的預期。我們的預期是,我們可以一直猜一直猜,直到猜中才讓遊戲結束,那應該怎麼修改一下呢?添加一個循環,代碼如下:

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("生成的隨機数字是:{}", secret_number);

    loop {

        println!("猜測数字遊戲,請輸入您猜測的数字。");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess).expect("讀取数字失敗!");

        let guess: u32 = guess.trim().parse().expect("請輸入一個数字!");

        println!("您猜測的数字是:{}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("您猜測的数字太小了!"),
            Ordering::Greater => println!("您猜測的数字太大了!"),
            Ordering::Equal => println!("恭喜您,猜對了!"),
        }
    }
}

這裏修改得比較簡單,只需要添加一個名叫 loop 的關鍵字,然後把需要循環的內容放在 {} 中即可,然後我們測試一下:

cargo run                                    
   Compiling guessing_game v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/guessing_game`
生成的隨機数字是:71
猜測数字遊戲,請輸入您猜測的数字。
50
您猜測的数字是:50
您猜測的数字太小了!
猜測数字遊戲,請輸入您猜測的数字。
71
您猜測的数字是:71
恭喜您,猜對了!
猜測数字遊戲,請輸入您猜測的数字。
45
您猜測的数字是:45
您猜測的数字太小了!
猜測数字遊戲,請輸入
t
thread 'main' panicked at '請輸入一個数字!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:1165:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

我們的遊戲可以多次輸入了,但是有沒有發現一些問題呢?
1.遊戲直接告訴我們生成的数字了,那就不用猜了,直接輸入就好了。
2.當我們猜對后,遊戲沒有結束。
3.當我們輸入的內容不是数字的時候,才會結束遊戲,而且不僅打印了我們預期的錯誤信息,還打印了其它信息。
接下來,我們把這些問題依次修改,代碼如下:

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);

    // println!("生成的隨機数字是:{}", secret_number);

    loop {

        println!("猜測数字遊戲,請輸入您猜測的数字。");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess).expect("讀取数字失敗!");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("您猜測的数字是:{}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("您猜測的数字太小了!"),
            Ordering::Greater => println!("您猜測的数字太大了!"),
            Ordering::Equal => {
                println!("恭喜您,猜對了!");
                break;
            }
        }
    }
}

這三處錯誤的修改方式依次是:
1.把打印隨機數的代碼註釋掉。
2.在做類型轉換時,使用 match 關鍵字作判斷,如果轉化成功,則返迴轉化后的結果,如果轉化失敗,不管因為什麼原因失敗,都直接跳出本次循環。
3.在做二個数字大小判斷時,如果判斷相等,則結束循環。
我們來測試一下修改的結果:

cargo run                                    
   Compiling guessing_game v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/guessing_game`
猜測数字遊戲,請輸入您猜測的数字。
50
您猜測的数字是:50
您猜測的数字太小了!
猜測数字遊戲,請輸入您猜測的数字。
r
猜測数字遊戲,請輸入您猜測的数字。
75
您猜測的数字是:75
您猜測的数字太小了!
猜測数字遊戲,請輸入您猜測的数字。
87
您猜測的数字是:87
您猜測的数字太大了!
猜測数字遊戲,請輸入您猜測的数字。
81
您猜測的数字是:81
恭喜您,猜對了!

可以看到我們的遊戲製作完成了~~

歡迎閱讀單鵬飛的學習筆記

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

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

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

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

※公開收購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收購-全台皆可收購

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

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

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

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

使用Docker搭建maven私服 及常規使用方法

安裝-登錄-配置

下載鏡像
docker pull sonatype/nexus3
運行
docker run -d -p 9998:8081 --name nexus --restart=always sonatype/nexus3

進入容器中查看密碼是多少

docker exec -it 容器名/容器id /bin/bash

根據上圖的提示進入到指定的目錄,查看密碼是啥

繼續訪問, 修改密碼

修改私服的中央倉庫位置,如果嫌國外的站點太慢了, 我們就將其修改成阿里雲,修改方式就是替換一下鏈接就ok

創建hosted類型的倉庫

選擇創建的倉庫類型是hosted類型,為什麼非得選擇這種類型呢? 如下錶中解密

項目 具體說明
hosted 本地存儲。像官方倉庫一樣提供本地私庫功能
proxy 提供代理其它倉庫的類型
group 組類型,能夠組合多個倉庫為一個地址提供服務

繼續創建

創建一個私服的帳號,然後在我的windows本中本地maven添加進去私服的新創建的這個用戶的信息, 進而可以使用這個用戶往私服中發布jar包

填寫用戶的信息

找到本機的settings.xml配置文件, 將我們剛剛創建的私服添加進去

ok, 下面去idea中發布jar包

發布

首先是將連接私服的用戶信息配置進配置文件

  1. id 就是上圖中的id
  2. url: 在nexus可視化界面中找到我們在上面創建的倉庫可以找到url

準備腳本

 <!--添加build依賴,表示可以發布jar-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <version>2.8</version>
            </plugin>
            <!--發布源碼的插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>2.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

發布命令:

mvn deploy

踩坑

  • 再發布之前檢查一下idea中關於maven的配置,使用我們剛才修改的settings.xml配置文件 , 不然這就是個坑,會一直deploy失敗
  • 上面的版本一定得和我們創建的倉庫的類型對應起來, 否則會報錯失敗

發布成果后我們繼續查看結果, 可

詳細結果

拉取使用

添加如下的在pom文件中依賴就ok

<dependency>
  <groupId>com.changwu</groupId>
  <artifactId>lawyer-eureka</artifactId>
  <version>1.0-RELEASE</version>
</dependency>
 <repository>
     <id>changwu</id>
     <name>lawyer-lover-release</name>
     <url>http://139.x.xx.235:9998/repository/lawyer-lover-release/</url>
</repository>

歡迎關注我的博客, 我將會把整理的docker(從入門到部署微服務)分享全套筆記

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

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

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

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

※公開收購3c價格,不怕被賤賣!

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

PowerMock學習(六)之Mock Final的使用

Mock Final

mockfinal相對來說就比較簡單了,使用powermock來測試使用final修飾的method或class,比較簡單,接口調用部分,還是service調用dao。

對於接口及場景這裏就不細說了,特別簡單。

service層

具體代碼示例如下:

package com.rongrong.powermock.mockfinal;

/**
 * @author rongrong
 * @version 1.0
 * @date 2019/11/27 21:29
 */
public class StudentFinalService {

    private StudentFinalDao studentFinalDao;

    public StudentFinalService(StudentFinalDao studentFinalDao) {
        this.studentFinalDao = studentFinalDao;
    }

    public void createStudent(Student student) {
        studentFinalDao.isInsert(student);
    }
}

dao層

為了模擬測試,我在dao層的類加了一個final關鍵字進行修飾,也就是這個類不允許被繼承了。

具體代碼如下:

package com.rongrong.powermock.mockfinal;


/**
 * @author rongrong
 * @version 1.0
 * @date 2019/11/27 21:20
 */
final public class StudentFinalDao {

    public Boolean isInsert(Student student){
        throw new UnsupportedOperationException();
    }
}

進行單元測試

為了區分powermock與Easymock的區別,我們先採用EasyMock測試,這裏先忽略EasyMock的用法,有興趣的同學可自行去嘗試學習。

使用EasyMock進行測試

具體代碼示例如下:

    @Test
    public void testStudentFinalServiceWithEasyMock(){
        //mock對象
        StudentFinalDao studentFinalDao = EasyMock.createMock(StudentFinalDao.class);
        Student student = new Student();
        //mock調用,默認返回成功
        EasyMock.expect(studentFinalDao.isInsert(student)).andReturn(true);
        EasyMock.replay(studentFinalDao);
        StudentFinalService studentFinalService = new StudentFinalService(studentFinalDao);
        studentFinalService.createStudent(student);
        EasyMock.verify(studentFinalDao);
    }

我們先來運行下這個單元測試,會發現運行報錯,具體如下圖显示:

 

 很明顯由於有final關鍵字修飾后,導致不能讓測試成功,我們可以刪除final關鍵再來測試一下,結果發現,測試通過。

使用PowerMock進行測試

具體代碼示例如下:

package com.rongrong.powermock.mockfinal;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

/**
 * @author rongrong
 * @version 1.0
 * @date 2019/11/27 22:10
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest(StudentFinalDao.class)
public class TestStudentFinalService {

    @Test
    public void testStudentFinalServiceWithPowerMock(){
        StudentFinalDao studentFinalDao = PowerMockito.mock(StudentFinalDao.class);
        Student student = new Student();
        PowerMockito.when(studentFinalDao.isInsert(student)).thenReturn(true);
        StudentFinalService studentFinalService = new StudentFinalService(studentFinalDao);
        studentFinalService.createStudent(student);
        Mockito.verify(studentFinalDao).isInsert(student);
    }
}

運行上面的單元測試時,會發現運行通過!!

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

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

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

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

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

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

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

Spring Security之多次登錄失敗后賬戶鎖定功能的實現

在上一次寫的文章中,為大家說到了如何動態的從數據庫加載用戶、角色、權限信息,從而實現登錄驗證及授權。在實際的開發過程中,我們通常會有這樣的一個需求:當用戶多次登錄失敗的時候,我們應該將賬戶鎖定,等待一定的時間之後才能再次進行登錄操作。

一、基礎知識回顧

要實現多次登錄失敗賬戶鎖定的功能,我們需要先回顧一下基礎知識:

  • Spring Security 不需要我們自己實現登錄驗證邏輯,而是將用戶、角色、權限信息以實現UserDetails和UserDetailsService接口的方式告知Spring Security。具體的登錄驗證邏輯Spring Security 會幫助我們實現。
  • UserDetails接口中有一個方法叫做isAccountNonLocked()用於判斷賬號是否被鎖定,也就是說我們應該通過該方法對應的set方法setAccountNonLocked(false)告知Spring Security該登錄賬戶被鎖定。
  • 那麼應該在哪裡判斷賬號登錄失敗的次數並執行鎖定機制呢?當然是我們之前文章給大家介紹的《自定義登錄成功及失敗結果處理》的AuthenticationFailureHandler。

建議您先閱讀本文,如果您對本文的實現過程感到迷惑,建議您再翻看本號之前的相關內容。

二、實現多次登錄失敗鎖定的原理

一般來說實現這個需求,我們需要針對每一個用戶記錄登錄失敗的次數nLock和鎖定賬戶的到期時間releaseTime。具體你是把這2個信息存儲在mysql、還是文件中、還是redis中等等,完全取決於你對你所處的應用架構適用性的判斷。具體的實現邏輯無非就是:

  • 登陸失敗之後,從存儲中將nLock取出來加1。
  • 如果nLock大於登陸失敗閾值(比如3次),則將nLock=0,然後設置releaseTime為當前時間加上鎖定周期。通過setAccountNonLocked(false)告知Spring Security該登錄賬戶被鎖定。
  • 如果nLock小於等於1,則將nLock再次存起來。
  • 在一個合適的時機,將鎖定狀態重置為setAccountNonLocked(true)。

這是一種非常典型的實現方式,筆者向大家介紹一款非常有用的開源軟件叫做:ratelimitj。這個軟件的功能主要是為API訪問進行限流,也就是說可以通過制定規則限制API接口的訪問頻率。那恰好登錄驗證接口也是API的一種啊,我們正好也需要限制它在一定的時間內的訪問次數。

三、具體實現

首先需要將ratelimitj通過maven坐標引入到我們的應用裏面來。我們使用的是內存存儲的版本,還有redis存儲的版本,大家可以根據自己的應用情況選用。

        <dependency>
            <groupId>es.moki.ratelimitj</groupId>
            <artifactId>ratelimitj-inmemory</artifactId>
            <version>0.4.1</version>
        </dependency>

之後通過繼承SimpleUrlAuthenticationFailureHandler ,實現onAuthenticationFailure方法。該實現是針對登錄失敗的結果的處理,在我們之前的文章中已經講過。

@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    UserDetailsManager userDetailsManager;

    //規則定義:1小時之內5次機會,就觸發限流行為
    Set<RequestLimitRule> rules = 
            Collections.singleton(RequestLimitRule.of(1 * 60, TimeUnit.MINUTES,5)); 
    RequestRateLimiter limiter = new InMemorySlidingWindowRequestRateLimiter(rules);


    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response, 
                                        AuthenticationException exception) 
                                        throws IOException, ServletException {

         String userId = //從request或request.getSession中獲取登錄用戶名
         //計數器加1,並判斷該用戶是否已經到了觸發了鎖定規則
         boolean reachLimit = limiter.overLimitWhenIncremented(userId);

        if(reachLimit){ //如果觸發了鎖定規則,通過UserDetails告知Spring Security鎖定賬戶
               user.setAccountNonLocked(false);
               userDetailsManager.updateUser(user);
               SysUser user = (SysUser) userDetailsManager.loadUserByUsername(userId);
        }
        

        
        //此處省略通過response做json或html響應
    }
}
  • 核心實現注意看代碼中的註釋
  • 代碼中的SysUser為UserDetails的實現類,如果不知道如何實現請參考本號之前的文章
  • userDetailsManager被用於管理UserDetails信息,通過改變UserDetails改變Spring Security驗證行為。

四、重置鎖定狀態的時機

user.setAccountNonLocked(true);

重置鎖定狀態很簡單,就是上面的代碼。但是更重要的是如何選擇重置鎖定狀態的時機。筆者能想到幾種方案如下

  • 下一次登陸的時候,自定義過濾器,加在Spring Boot過濾器鏈最前端做鎖定狀態重置的判斷。
  • 當登錄賬戶被鎖定之後,之後用戶的每一次登錄都會拋出LockedException。我們完全可以通過Spring Boot的全局異常捕獲機制,在其中捕獲LockedException,並做鎖定狀態的判斷及重置行為。
  • 寫一個Spring 的定時器輪詢,當然這是最差的方案。

    期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

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

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

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

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

Head First設計模式——適配器和外觀模式,Head First設計模式——裝飾者模式

前言:為什麼要一次講解這兩個模式,說點騷話:因為比較簡單(*^_^*),其實是他們兩個有相似和有時候我們容易搞混概念。

講到這兩個設計模式與另外一個“裝飾者模式”也有相似,他們三個按照結構模式分類都屬於“結構性模式”,所有我們接下來就來看什麼是適配器模式和外觀模式。

另外裝飾模式可以看我的另一篇博文→

一、適配器模式

適配器對應到我們現實生活中的例子,最典型的就是插頭接口適配器,比如我們買的有些港版手機充電頭是圓形三角插頭,而大陸的三角電源插板插不進去港版的插頭。

這時候我們就會在某寶上買個轉接頭轉換一下,而這個轉接頭就是適配器,用它來適配港版手機充電頭讓他能夠插入到我們的電源插板裏面。

在設計模式中這個適配器是什麼,用程序如何表現,先讓我舉個栗子:我們有一隻鴨子,一隻雞,我們如何通過適配器轉換鴨和雞。

鴨子有很多種,我們定義一個鴨子的接口,然後以綠頭鴨為例。關於這個綠頭鴨在策略模式也有用到,可以看看我另一篇綠頭鴨如何攪動策略模式→

    public  interface Duck
    {
        //叫
        public void Quack();
        //飛
        public void Fly();
    }

    public class GreenDuck : Duck
    {
        public void Fly()
        {
            Console.WriteLine("綠頭鴨,飛");
        }

        public void Quack()
        {
            Console.WriteLine("綠頭鴨,呱呱叫");
        }
    }

  同樣我們定義一個雞的接口,和一隻母雞的類

    public  interface Chicken
    {
        //叫
        public void Gobble();
        //飛
        public void Fly();
    }

    public class Hen : Chicken
    {
       
        public void Gobble()
        {
            Console.WriteLine("母雞,咯咯叫");
        }

        public void Fly()
        {
            Console.WriteLine("母雞,飛");
        }

    }

  鴨子和母雞的叫聲不一樣,現在我們讓母雞來冒充鴨子,利用適配器模式如何做。 直接看代碼吧

    /// <summary>
    /// 母雞適配器
    /// 適配母雞讓它變成鴨子
    /// </summary>
    public class HenAdapter : Duck
    {
        Chicken chicken;
        public HenAdapter(Chicken chicken)
        {
            this.chicken = chicken;
        }
        public void Quack()
        {
            //調用母雞咯咯叫
            chicken.Gobble();
        }

        public void Fly()
        {
            //調用母雞飛
            chicken.Fly();
        }

    }

  測試母雞適配器

如上我們使用母雞適配器將母雞適配成了鴨子,鴨子也可以用適配器將鴨子適配成母雞,適配器模式定義:

適配器模式:將一個類的接口,裝換成客戶期望的另一個接口。適配器讓原本接口不兼容的類可以合作無間。

與適配器看起來相似的裝飾者模式是包裝對象的行為或責任,裝飾者被包裝后可能會繼續被包裝,他們不裝換接口,而適配器則一定會進行接口的轉換。

適配的工作是將一個接口轉換成另外一個接口,雖然大多數適配器採取的例子都是讓一個適配器包裝一個被適配者,但是有時候我們需要讓一個適配器包裝多個被適配者。

而這實際又涉及到另外一個模式,就是外觀模式,我們常常將適配器模式和外觀模式混為一談,那接着就來講解外觀模式。

二、外觀模式

外觀模式以家庭影院為例,家庭影院有許多組件構成,比如:显示屏、DVD、音響、燈光等等。

當我們要看電影的時候要打開显示屏,打開DVD,打開音響,關閉燈光等一系列動作,將這些動作寫成類方法的調用

            Screen screen = new Screen();
            DVD dvd = new DVD();
            SoundEngineer sound = new SoundEngineer();
            Light light = new Light();

            screen.Down();
            dvd.PlayDVD();
            sound.TurnOn();
            light.TurnOff();

可以看到每次我們要使用就要調用一篇這些方法,如果要關閉呢,我們也需要調用一篇。而我們正需要的就是一個外觀:通過實現一個提供更合理的接口的外觀類。

還是看代碼吧

 public class HomeThreaterFacade
    {
        Screen screen;
        DVD dvd;
        SoundEngineer sound;
        Light light;

        public HomeThreaterFacade(Screen screen, DVD dvd, SoundEngineer sound, Light light)
        {
            this.screen = screen;
            this.dvd = dvd;
            this.sound = sound;
            this.light = light;
        }

        public void WatchMovie()
        {
            Console.WriteLine("開始播放電影......");
            screen.Down();
            dvd.PlayDVD();
            sound.TurnOn();
            light.TurnOff();
        }
    }

由於其他類比較簡單就是一個打印輸出,我就不列出來了,還有關閉方法同理也很簡單就實現了。

還是測試一下效果:

外觀模式定義

外觀模式:提供了一個統一的接口,用來訪問子系統中的一群接口。外觀定義了一個高層接口,讓子系統更容易使用。

外觀模式遵循了一個設計原則

最少知識原則:之和你的密友談話。

這個原則希望我們在設計中,不要讓太多的類耦合在一起,免得修改系統中一部分,會影響其他部分。而外觀模式讓用戶不用關心全部子系統組件,讓客戶變得簡單有彈性。我們可以在不影響客戶的情況下升級外觀模式里的組件,而客戶只有一個朋友,也就是外觀模式。

三、適配器模式與外觀模式區別

從上面例子我們也許會覺得適配器和外觀模式之間的差異在於:適配器包裝一個類,而外觀可以代表許多類

但是實際它們的本質和作用並不是在於包裝多少類,適配器模式將一個或多個接口變成客戶期望的一個接口,我們一般適配一個類,但是特殊需求也可以適配多個類來提供一個接口。類是地,一個外觀也可以只爭對一個複雜接口的類提供簡化接口。兩中模式的差異在於他們的意圖。適配器模式意圖是將接口裝換成不同接口,外觀的意圖是簡化接口。

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

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

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

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

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

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

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

中國動力電池技術突破,2025 年電動車成本效益比料勝燃油車

 

21 世紀經濟報導,中國電動汽車百人會理事長陳清泰表示,過去一年,中國電動汽車產業正在向高品質發輾轉型,發展形勢良好;而電動汽車再往前發展要跨越一個臨界點,就是電動車的成本效益比達到和超過燃油車,他預期這個臨界點會可能會在 2025 年前後出現。   中國 2017 年新能源汽車銷量目標 70 萬輛,去年 1 到 11 月累計銷售 60.9 萬輛、年增 51.4%。專家預測,中國去年新能源汽車總銷量可能超過 80 萬輛。中國汽車工業協會秘書長助理許海東日前表示,中國去年新能源汽車 70 萬輛銷量目標應可達成,2018 年新能源車銷量增速仍可保持 40% 至 50%,預期銷量將超過 100 萬輛。   陳清泰指出,電動汽車再往前發展要跨越一個臨界點,就是電動車的成本效益比達到和超過燃油車,如果跨過了這個門檻,電動車就可依託市場力量自主發展了,目前仍需靠政策、靠政府補貼。他亦預期,當財政補貼完全取消後,雙積分政策作為替代政策,新能源汽車積分比例將在 2020 年 12% 的基礎上繼續上調。   對於上述臨界點會出現在什麼時候?陳清泰的判斷是,大約在 2025 年前後。對此,他建議中國車企要在以下幾個方面做好準備,首先是在財政補貼退坡之後,做好可持續發展的保障工作,另外電動車自身要透過輕量化、節能化提高成本效益比;其次,產品技術要雙線作戰,其中一條戰線是完善汽車的行駛功能,另一條戰線則是將車聯網和共享化應用到新能源汽車上;第三,自動駕駛是爭奪未來的一個重點;第四,在有限的時間要加緊做品牌建設。   中國電動汽車百人會執行副理事長歐陽明高表示,2017 年中國動力電池技術已取得實質性進展,動力電池系統能量密度已達到 150 瓦時/公斤甚至以上,鋰離子動力電池單體比能量有望於 2020 年前實現 300 瓦時/公斤目標。   他進一步指出,2025 年,具備一般成本效益比的純電動轎車合理的里程是 300 到 400 公里;但 2030 年,最大的技術突破將體現在電解質上,固態電池會規模產業化,電池單體比能量可望觸及 500 瓦時/公斤;2030 年常規的成本效益比車型,續航里程應該可達到 500 公里以上。   (本文內容由授權使用。首圖來源:)

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

【其他文章推薦】

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

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

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

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

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

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