作為一個Java開發你用過Jib嗎

1. 前言

JibGoogle開發的可以直接構建 Java應用的DockerOCI鏡像的類庫,以MavenGradle插件形式提供。它最騷操作的是可以在沒有Docker守護程序的情況下構建,也就是說,您不必在計算機上安裝docker守護程序!儘管Spring Boot 2.3.0.RELEASE已經推出了構建鏡像的功能,胖哥還是忍不住要試試Jib

其實最騷的還是名字。

2. Docker構建流程和Jib的構建流程

沒有對比就沒有傷害。我們還是要對比一下這兩者的構建流程。

Docker構建流程需要我們先把項目打成Jar然後編寫Dockerfile,然後使用Docker構建功能進行構建鏡像、運行容器。流程如下:

而Jib是這樣構建的:

作為一個Java開發者,不用再關心各種無關的命令和操作,只需要專註於Java,而且高效穩定以及可復用的增量構建。為什麼Jib能這麼快而高效?

傳統上,將Java應用程序與應用程序Jar一起構建為單個圖像層,而Jib的構建策略將Java應用程序分為多層,以進行更細化的增量構建。更改代碼時,僅重建更改,而不重建整個應用程序。

3. Jib構建Spring Boot應用

接下來我將演示如何將Spring Boot 應用打成鏡像並上傳到Dockerhub倉庫。

Maven工程為例,我們只需要在pom.xml中引入Jib Maven 插件。默認情況下Jib會把我們打好的鏡像上傳到Googlegcr.io倉庫,實際中我們會把打好的鏡像上傳到私有倉庫,所以我們要加一些個性化配置。這裏我以dockerhub倉庫為例添加一些個性化配置:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>2.4.0</version>
    <configuration>
        <!-- 相當於 Dockerfile 中的 FROM -->
        <from>
            <image>amazoncorretto:8</image>
        </from>
        <to>
            <!--構建鏡像名稱,這裏我使用maven中定義的項目名稱-->
            <image>daxus/${project.name}</image>
            <!--私有倉庫的賬號密碼-->
            <auth>
                <username>felordcn</username>
                <password>yourpassword</password>
            </auth>
            <!--Docker 鏡像的 tag 這裏使用maven定義的版本號-->
            <tags>
                <tag>
                    ${project.version}
                </tag>
            </tags>
        </to>
    </configuration>
</plugin>

然後在項目根目錄執行mvn clean compile jib:build就可以了。

其實也可以簡單引入Jib插件:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>2.4.0</version>
</plugin>

只不過我們的命令會更複雜一些,需要指定一些必要的參數,例如:

mvn clean compile jib:build \
    -Djib.to.image=myregistry/myimage:latest \
    -Djib.to.auth.username=$USERNAME \
    -Djib.to.auth.password=$PASSWORD

更多的定製命令可參考官方文檔:

https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#extended-usage

4. 總結

Jib使用起來非常簡單,讓開發人員以Java的風格來完成Docker鏡像的構建,能夠大大改善編程的體驗。多多關注:碼農小胖哥 獲取更多有用的編程乾貨教程。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

重學 Java 設計模式:實戰中介者模式「按照Mybatis原理手寫ORM框架,給JDBC方式操作數據庫增加中介者場景」

作者:小傅哥
博客:https://bugstack.cn – 原創系列專題文章

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

一、前言

同齡人的差距是從什麼時候拉開的

同樣的幼兒園、同樣的小學、一樣的書本、一樣的課堂,有人學習好、有人學習差。不只是上學,幾乎人生處處都是賽道,發令槍響起的時刻,也就把人生的差距拉開。編程開發這條路也是很長很寬,有人跑得快有人跑得慢。那麼你是否想起過,這一點點的差距到遙不可及的距離,是從哪一天開始的。摸摸肚子的肉,看看遠處的路,別人講的是故事,你想起的都是事故

思想沒有產品高才寫出一片的ifelse

當你承接一個需求的時候,比如;交易、訂單、營銷、保險等各類場景。如果你不熟悉這個場景下的業務模式,以及將來的拓展方向,那麼很難設計出良好可擴展的系統。再加上產品功能初建,說老闆要的急,儘快上線。作為程序員的你更沒有時間思考,整體一看現在的需求也不難,直接上手開干(一個方法兩個if語句),這樣確實滿足了當前需求。但老闆的想法多呀,產品也跟着變化快,到你這就是改改改,加加加。當然你也不客氣,回首掏就是1024個if語句!

日積月累的技術沉澱是為了厚積薄發

粗略的估算過,如果從上大學開始每天寫200行,一個月是6000行,一年算10個月話,就是6萬行,第三年出去實習的是時候就有20萬行的代碼量。如果你能做到這一點,找工作難?有時候很多事情就是靠時間積累出來的,想走捷徑有時候真的沒有。你的技術水平、你的業務能力、你身上的肉,都是一點點積累下來的,不要浪費看似很短的時間,一年年堅持下來,留下印刻青春的痕迹,多給自己武裝上一些能力。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. mysql 5.1.20
  4. 涉及工程一個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-16-01 使用JDBC方式連接數據庫
itstack-demo-design-16-02 手寫ORM框架操作數據庫

三、中介者模式介紹

中介者模式要解決的就是複雜功能應用之間的重複調用,在這中間添加一層中介者包裝服務,對外提供簡單、通用、易擴展的服務能力。

這樣的設計模式幾乎在我們日常生活和實際業務開發中都會見到,例如;飛機降落有小姐姐在塔台喊話、無論哪個方向來的候車都從站台上下、公司的系統中有一个中台專門為你包裝所有接口和提供統一的服務等等,這些都運用了中介者模式。除此之外,你用到的一些中間件,他們包裝了底層多種數據庫的差異化,提供非常簡單的方式進行使用。

四、案例場景模擬

在本案例中我們通過模仿Mybatis手寫ORM框架,通過這樣操作數據庫學習中介者運用場景

除了這樣的中間件層使用場景外,對於一些外部接口,例如N種獎品服務,可以由中台系統進行統一包裝對外提供服務能力。也是中介者模式的一種思想體現。

在本案例中我們會把jdbc層進行包裝,讓用戶在使用數據庫服務的時候,可以和使用mybatis一樣簡單方便,通過這樣的源碼方式學習中介者模式,也方便對源碼知識的拓展學習,增強知識棧。

五、用一坨坨代碼實現

這是一種關於數據庫操作最初的方式

基本上每一個學習開發的人都學習過直接使用jdbc方式連接數據庫,進行CRUD操作。以下的例子可以當做回憶。

1. 工程結構

itstack-demo-design-16-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── JDBCUtil.java
  • 這裏的類比較簡單隻包括了一個數據庫操作類。

2. 代碼實現

public class JDBCUtil {

    private static Logger logger = LoggerFactory.getLogger(JDBCUtil.class);

    public static final String URL = "jdbc:mysql://127.0.0.1:3306/itstack-demo-design";
    public static final String USER = "root";
    public static final String PASSWORD = "123456";

    public static void main(String[] args) throws Exception {
        //1. 加載驅動程序
        Class.forName("com.mysql.jdbc.Driver");
        //2. 獲得數據庫連接
        Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
        //3. 操作數據庫
        Statement stmt = conn.createStatement();
        ResultSet resultSet = stmt.executeQuery("SELECT id, name, age, createTime, updateTime FROM user");
        //4. 如果有數據 resultSet.next() 返回true
        while (resultSet.next()) {
            logger.info("測試結果 姓名:{} 年齡:{}", resultSet.getString("name"),resultSet.getInt("age"));
        }
    }

}
  • 以上是使用JDBC的方式進行直接操作數據庫,幾乎大家都使用過這樣的方式。

3. 測試結果

15:38:10.919 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:水水 年齡:18
15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:豆豆 年齡:18
15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:花花 年齡:19

Process finished with exit code 0
  • 從測試結果可以看到這裏已經查詢到了數據庫中的數據。只不過如果在全部的業務開發中都這樣實現,會非常的麻煩。

六、中介模式開發ORM框架

`接下來就使用中介模式的思想完成模仿Mybatis的ORM框架開發~

1. 工程結構

itstack-demo-design-16-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── dao
    │   │       │	├── ISchool.java
    │   │       │	└── IUserDao.java
    │   │       ├── mediator
    │   │       │	├── Configuration.java
    │   │       │	├── DefaultSqlSession.java
    │   │       │	├── DefaultSqlSessionFactory.java
    │   │       │	├── Resources.java
    │   │       │	├── SqlSession.java
    │   │       │	├── SqlSessionFactory.java
    │   │       │	├── SqlSessionFactoryBuilder.java
    │   │       │	└── SqlSessionFactoryBuilder.java
    │   │       └── po
    │   │         	├── School.java
    │   │         	└── User.java
    │   └── resources
    │       ├── mapper
    │       │   ├── School_Mapper.xml
    │       │   └── User_Mapper.xml
    │       └── mybatis-config-datasource.xml
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

中介者模式模型結構

  • 以上是對ORM框架實現的核心類,包括了;加載配置文件、對xml解析、獲取數據庫session、操作數據庫以及結果返回。
  • 左上是對數據庫的定義和處理,基本包括我們常用的方法;<T> T selectOne<T> List<T> selectList等。
  • 右側藍色部分是對數據庫配置的開啟session的工廠處理類,這裏的工廠會操作DefaultSqlSession
  • 之後是紅色地方的SqlSessionFactoryBuilder,這個類是對數據庫操作的核心類;處理工廠、解析文件、拿到session等。

接下來我們就分別介紹各個類的功能實現過程。

2. 代碼實現

2.1 定義SqlSession接口

public interface SqlSession {

    <T> T selectOne(String statement);

    <T> T selectOne(String statement, Object parameter);

    <T> List<T> selectList(String statement);

    <T> List<T> selectList(String statement, Object parameter);

    void close();
}
  • 這裏定義了對數據庫操作的查詢接口,分為查詢一個結果和查詢多個結果,同時包括有參數和沒有參數的方法。

2.2 SqlSession具體實現類

public class DefaultSqlSession implements SqlSession {

    private Connection connection;
    private Map<String, XNode> mapperElement;

    public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
        this.connection = connection;
        this.mapperElement = mapperElement;
    }

    @Override
    public <T> T selectOne(String statement) {
        try {
            XNode xNode = mapperElement.get(statement);
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            return objects.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public <T> List<T> selectList(String statement) {
        XNode xNode = mapperElement.get(statement);
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // ...

    private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
        List<T> list = new ArrayList<>();
        try {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            // 每次遍歷行值
            while (resultSet.next()) {
                T obj = (T) clazz.newInstance();
                for (int i = 1; i <= columnCount; i++) {
                    Object value = resultSet.getObject(i);
                    String columnName = metaData.getColumnName(i);
                    String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                    Method method;
                    if (value instanceof Timestamp) {
                        method = clazz.getMethod(setMethod, Date.class);
                    } else {
                        method = clazz.getMethod(setMethod, value.getClass());
                    }
                    method.invoke(obj, value);
                }
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    @Override
    public void close() {
        if (null == connection) return;
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • 這裏包括了接口定義的方法實現,也就是包裝了jdbc層。
  • 通過這樣的包裝可以讓對數據庫的jdbc操作隱藏起來,外部調用的時候對入參、出參都有內部進行處理。

2.3 定義SqlSessionFactory接口

public interface SqlSessionFactory {

    SqlSession openSession();

}
  • 開啟一個SqlSession, 這幾乎是大家在平時的使用中都需要進行操作的內容。雖然你看不見,但是當你有數據庫操作的時候都會獲取每一次執行的SqlSession

2.4 SqlSessionFactory具體實現類

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
    }

}
  • DefaultSqlSessionFactory,是使用mybatis最常用的類,這裏我們簡單的實現了一個版本。
  • 雖然是簡單的版本,但是包括了最基本的核心思路。當開啟SqlSession時會進行返回一個DefaultSqlSession
  • 這個構造函數中向下傳遞了Configuration配置文件,在這個配置文件中包括;Connection connectionMap<String, String> dataSourceMap<String, XNode> mapperElement。如果有你閱讀過Mybatis源碼,對這個就不會陌生。

2.5 SqlSessionFactoryBuilder實現

public class SqlSessionFactoryBuilder {

    public DefaultSqlSessionFactory build(Reader reader) {
        SAXReader saxReader = new SAXReader();
        try {
            saxReader.setEntityResolver(new XMLMapperEntityResolver());
            Document document = saxReader.read(new InputSource(reader));
            Configuration configuration = parseConfiguration(document.getRootElement());
            return new DefaultSqlSessionFactory(configuration);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Configuration parseConfiguration(Element root) {
        Configuration configuration = new Configuration();
        configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
        configuration.setConnection(connection(configuration.dataSource));
        configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
        return configuration;
    }

    // 獲取數據源配置信息
    private Map<String, String> dataSource(List<Element> list) {
        Map<String, String> dataSource = new HashMap<>(4);
        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            dataSource.put(name, value);
        }
        return dataSource;
    }

    private Connection connection(Map<String, String> dataSource) {
        try {
            Class.forName(dataSource.get("driver"));
            return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 獲取SQL語句信息
    private Map<String, XNode> mapperElement(List<Element> list) {
        Map<String, XNode> map = new HashMap<>();

        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String resource = e.attributeValue("resource");

            try {
                Reader reader = Resources.getResourceAsReader(resource);
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(new InputSource(reader));
                Element root = document.getRootElement();
                //命名空間
                String namespace = root.attributeValue("namespace");

                // SELECT
                List<Element> selectNodes = root.selectNodes("select");
                for (Element node : selectNodes) {
                    String id = node.attributeValue("id");
                    String parameterType = node.attributeValue("parameterType");
                    String resultType = node.attributeValue("resultType");
                    String sql = node.getText();

                    // ? 匹配
                    Map<Integer, String> parameter = new HashMap<>();
                    Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                    Matcher matcher = pattern.matcher(sql);
                    for (int i = 1; matcher.find(); i++) {
                        String g1 = matcher.group(1);
                        String g2 = matcher.group(2);
                        parameter.put(i, g2);
                        sql = sql.replace(g1, "?");
                    }

                    XNode xNode = new XNode();
                    xNode.setNamespace(namespace);
                    xNode.setId(id);
                    xNode.setParameterType(parameterType);
                    xNode.setResultType(resultType);
                    xNode.setSql(sql);
                    xNode.setParameter(parameter);

                    map.put(namespace + "." + id, xNode);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }

        }
        return map;
    }

}
  • 在這個類中包括的核心方法有;build(構建實例化元素)parseConfiguration(解析配置)dataSource(獲取數據庫配置)connection(Map<String, String> dataSource) (鏈接數據庫)mapperElement (解析sql語句)
  • 接下來我們分別介紹這樣的幾個核心方法。

build(構建實例化元素)

這個類主要用於創建解析xml文件的類,以及初始化SqlSession工廠類DefaultSqlSessionFactory。另外需要注意這段代碼saxReader.setEntityResolver(new XMLMapperEntityResolver());,是為了保證在不聯網的時候一樣可以解析xml,否則會需要從互聯網獲取dtd文件。

parseConfiguration(解析配置)

是對xml中的元素進行獲取,這裏主要獲取了;dataSourcemappers,而這兩個配置一個是我們數據庫的鏈接信息,另外一個是對數據庫操作語句的解析。

connection(Map<String, String> dataSource) (鏈接數據庫)

鏈接數據庫的地方和我們常見的方式是一樣的;Class.forName(dataSource.get("driver"));,但是這樣包裝以後外部是不需要知道具體的操作。同時當我們需要鏈接多套數據庫的時候,也是可以在這裏擴展。

mapperElement (解析sql語句)

這部分代碼塊內容相對來說比較長,但是核心的點就是為了解析xml中的sql語句配置。在我們平常的使用中基本都會配置一些sql語句,也有一些入參的佔位符。在這裏我們使用正則表達式的方式進行解析操作。

解析完成的sql語句就有了一個名稱和sql的映射關係,當我們進行數據庫操作的時候,這個組件就可以通過映射關係獲取到對應sql語句進行操作。

3. 測試驗證

在測試之前需要導入sql語句到數據庫中;

  • 庫名:itstack-demo-design
  • 表名:userschool
CREATE TABLE school ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64), address varchar(256), createTime datetime, updateTime datetime, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into school (id, name, address, createTime, updateTime) values (1, '北京大學', '北京市海淀區頤和園路5號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (2, '南開大學', '中國天津市南開區衛津路94號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (3, '同濟大學', '上海市彰武路1號同濟大廈A樓7樓7區', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
CREATE TABLE user ( id bigint(11) NOT NULL AUTO_INCREMENT, name varchar(32), age int(4), address varchar(128), entryTime datetime, remark varchar(64), createTime datetime, updateTime datetime, status int(4) DEFAULT '0', dateTime varchar(64), PRIMARY KEY (id), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (1, '水水', 18, '吉林省榆樹市黑林鎮尹家村5組', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309');
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (2, '豆豆', 18, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null);
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (3, '花花', 19, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');

3.1 創建數據庫對象類

用戶類

public class User {

    private Long id;
    private String name;
    private Integer age;
    private Date createTime;
    private Date updateTime;
    
    // ... get/set
}

學校類

public class School {

    private Long id;
    private String name;
    private String address;
    private Date createTime;
    private Date updateTime;  
  
    // ... get/set
}
  • 這兩個類都非常簡單,就是基本的數據庫信息。

3.2 創建DAO包

用戶Dao

public interface IUserDao {

     User queryUserInfoById(Long id);

}

學校Dao

public interface ISchoolDao {

    School querySchoolInfoById(Long treeId);

}

3.3 ORM配置文件

鏈接配置

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_design?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>

</configuration>
  • 這個配置與我們平常使用的mybatis基本是一樣的,包括了數據庫的連接池信息以及需要引入的mapper映射文件。

操作配置(用戶)

<mapper namespace="org.itstack.demo.design.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where id = #{id}
    </select>

    <select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where age = #{age}
    </select>

</mapper>

操作配置(學校)

<mapper namespace="org.itstack.demo.design.dao.ISchoolDao">

    <select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
        SELECT id, name, address, createTime, updateTime
        FROM school
        where id = #{id}
    </select>

</mapper>

3.4 單個結果查詢測試

@Test
public void test_queryUserInfoById() {
    String resource = "mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        try {
            User user = session.selectOne("org.itstack.demo.design.dao.IUserDao.queryUserInfoById", 1L);
            logger.info("測試結果:{}", JSON.toJSONString(user));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 這裏的使用方式和Mybatis是一樣的,都包括了;資源加載和解析、SqlSession工廠構建、開啟SqlSession以及最後執行查詢操作selectOne

測試結果

16:56:51.831 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0
  • 從結果上看已經滿足了我們的查詢需求。

3.5 集合結果查詢測試

@Test
public void test_queryUserList() {
    String resource = "mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        try {
            User req = new User();
            req.setAge(18);
            List<User> userList = session.selectList("org.itstack.demo.design.dao.IUserDao.queryUserList", req);
            logger.info("測試結果:{}", JSON.toJSONString(userList));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 這個測試內容與以上只是查詢方法有所不同;session.selectList,是查詢一個集合結果。

測試結果

16:58:13.963 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]

Process finished with exit code 0
  • 測試驗證集合的結果也是正常的,目前位置測試全部通過。

七、總結

  • 以上通過中介者模式的設計思想我們手寫了一個ORM框架,隱去了對數據庫操作的複雜度,讓外部的調用方可以非常簡單的進行操作數據庫。這也是我們平常使用的Mybatis的原型,在我們日常的開發使用中,只需要按照配置即可非常簡單的操作數據庫。
  • 除了以上這種組件模式的開發外,還有服務接口的包裝也可以使用中介者模式來實現。比如你們公司有很多的獎品接口需要在營銷活動中對接,那麼可以把這些獎品接口統一收到中台開發一個獎品中心,對外提供服務。這樣就不需要每一個需要對接獎品的接口,都去找具體的提供者,而是找中台服務即可。
  • 在上述的實現和測試使用中可以看到,這種模式的設計滿足了;單一職責開閉原則,也就符合了迪米特原則,即越少人知道越好。外部的人只需要按照需求進行調用,不需要知道具體的是如何實現的,複雜的一面已經有組件合作服務平台處理。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
  • 2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
  • 3. 重學 Java 設計模式:實戰橋接模式「多支付渠道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
  • 4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
  • 5. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」
  • 6. 重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

為什麼建議你使用枚舉?

枚舉是 JDK 1.5 新增的數據類型,使用枚舉我們可以很好的描述一些特定的業務場景,比如一年中的春、夏、秋、冬,還有每周的周一到周天,還有各種顏色,以及可以用它來描述一些狀態信息,比如錯誤碼等。

枚舉類型不止存在在 Java 語言中,在其它語言中也都能找到它的身影,例如 C# 和 Python 等,但我發現在實際的項目中使用枚舉的人很少,所以本文就來聊一聊枚舉的相關內容,好讓朋友們對枚舉有一個大概的印象,這樣在編程時起碼還能想到有“枚舉”這樣一個類型。

本文的結構目錄如下:

枚舉的 7 種使用方法

很多人不使用枚舉的一個重要的原因是對枚舉不夠熟悉,那麼我們就先從枚舉的 7 種使用方法說起。

用法一:常量

在 JDK 1.5 之前,我們定義常量都是 public static final... ,但有了枚舉,我們就可以把這些常量定義成一個枚舉類了,實現代碼如下:

public enum ColorEnum {  
  RED, GREEN, BLANK, YELLOW  
} 

用法二:switch

將枚舉用在 switch 判斷中,使得代碼可讀性更高了,實現代碼如下:

enum ColorEnum {
    GREEN, YELLOW, RED
}
public class ColorTest {
    ColorEnum color = ColorEnum.RED;

    public void change() {
        switch (color) {
            case RED:
                color = ColorEnum.GREEN;
                break;
            case YELLOW:
                color = ColorEnum.RED;
                break;
            case GREEN:
                color = ColorEnum.YELLOW;
                break;
        }
    }
}

用法三:枚舉中增加方法

我們可以在枚舉中增加一些方法,讓枚舉具備更多的特性,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ErrorCodeEnum errorCode = ErrorCodeEnum.SUCCESS;
        System.out.println("狀態碼:" + errorCode.code() + 
                           " 狀態信息:" + errorCode.msg());
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    PARAM_ERROR(1001, "parameter error"),
    SYS_ERROR(1003, "system error"),
    NAMESPACE_NOT_FOUND(2001, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }

    public static ErrorCodeEnum getErrorCode(int code) {
        for (ErrorCodeEnum it : ErrorCodeEnum.values()) {
            if (it.code() == code) {
                return it;
            }
        }
        return UNKNOWN_ERROR;
    }
}

以上程序的執行結果為:

狀態碼:1000 狀態信息:success

用法四:覆蓋枚舉方法

我們可以覆蓋一些枚舉中的方法用於實現自己的業務,比如我們可以覆蓋 toString() 方法,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        System.out.println(colorEnum.toString());
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    //  成員變量
    private String name;
    private int index;

    //  構造方法
    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    //覆蓋方法
    @Override
    public String toString() {
        return this.index + ":" + this.name;
    }
}

以上程序的執行結果為:

1:紅色

用法五:實現接口

枚舉類可以用來實現接口,但不能用於繼承類,因為枚舉默認繼承了 java.lang.Enum 類,在 Java 語言中允許實現多接口,但不能繼承多個父類,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        colorEnum.print();
        System.out.println("顏色:" + colorEnum.getInfo());
    }
}

interface Behaviour {
    void print();

    String getInfo();
}

enum ColorEnum implements Behaviour {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }

    @Override
    public String getInfo() {
        return this.name;
    }
}

以上程序的執行結果為:

1:紅色

顏色:紅色

用法六:在接口中組織枚舉類

我們可以在一個接口中創建多個枚舉類,用它可以很好的實現“多態”,也就是說我們可以將擁有相同特性,但又有細微實現差別的枚舉類聚集在一個接口中,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        // 賦值第一個枚舉類
        ColorInterface colorEnum = ColorInterface.ColorEnum.RED;
        System.out.println(colorEnum);
        // 賦值第二個枚舉類
        colorEnum = ColorInterface.NewColorEnum.NEW_RED;
        System.out.println(colorEnum);
    }
}

interface ColorInterface {
    enum ColorEnum implements ColorInterface {
        GREEN, YELLOW, RED
    }
    enum NewColorEnum implements ColorInterface {
        NEW_GREEN, NEW_YELLOW, NEW_RED
    }
}

以上程序的執行結果為:

RED

NEW_RED

用法七:使用枚舉集合

在 Java 語言中和枚舉類相關的,還有兩個枚舉集合類 java.util.EnumSetjava.util.EnumMap,使用它們可以實現更多的功能。

使用 EnumSet 可以保證元素不重複,並且能獲取指定範圍內的元素,示例代碼如下:

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

public class EnumTest {
    public static void main(String[] args) {
        List<ColorEnum> list = new ArrayList<ColorEnum>();
        list.add(ColorEnum.RED);
        list.add(ColorEnum.RED);  // 重複元素
        list.add(ColorEnum.YELLOW);
        list.add(ColorEnum.GREEN);
        // 去掉重複數據
        EnumSet<ColorEnum> enumSet = EnumSet.copyOf(list);
        System.out.println("去重:" + enumSet);

        // 獲取指定範圍的枚舉(獲取所有的失敗狀態)
        EnumSet<ErrorCodeEnum> errorCodeEnums = EnumSet.range(ErrorCodeEnum.ERROR, ErrorCodeEnum.UNKNOWN_ERROR);
        System.out.println("所有失敗狀態:" + errorCodeEnums);
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    ERROR(2001, "parameter error"),
    SYS_ERROR(2002, "system error"),
    NAMESPACE_NOT_FOUND(2003, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }
}

以上程序的執行結果為:

去重:[RED, GREEN, YELLOW]

所有失敗狀態:[ERROR, SYS_ERROR, NAMESPACE_NOT_FOUND, NODE_NOT_EXIST, NODE_ALREADY_EXIST, UNKNOWN_ERROR]

EnumMapHashMap 類似,不過它是一個專門為枚舉設計的 Map 集合,相比 HashMap 來說它的性能更高,因為它內部放棄使用鏈表和紅黑樹的結構,採用數組作為數據存儲的結構。

EnumMap 基本使用示例如下:

import java.util.EnumMap;

public class EnumTest {
    public static void main(String[] args) {
        EnumMap<ColorEnum, String> enumMap = new EnumMap<>(ColorEnum.class);
        enumMap.put(ColorEnum.RED, "紅色");
        enumMap.put(ColorEnum.GREEN, "綠色");
        enumMap.put(ColorEnum.BLANK, "白色");
        enumMap.put(ColorEnum.YELLOW, "黃色");
        System.out.println(ColorEnum.RED + ":" + enumMap.get(ColorEnum.RED));
    }
}

enum ColorEnum {
    RED, GREEN, BLANK, YELLOW;
}

以上程序的執行結果為:

RED:紅色

使用注意事項

阿里《Java開發手冊》對枚舉的相關規定如下,我們在使用時需要稍微注意一下。

【強制】所有的枚舉類型字段必須要有註釋,說明每個數據項的用途。

【參考】枚舉類名帶上 Enum 後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。正例:枚舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。

假如不使用枚舉

在枚舉沒有誕生之前,也就是 JDK 1.5 版本之前,我們通常會使用 int 常量來表示枚舉,實現代碼如下:

public static final int COLOR_RED = 1;
public static final int COLOR_BLUE = 2;
public static final int COLOR_GREEN = 3;

但是使用 int 類型可能存在兩個問題:

第一, int 類型本身並不具備安全性,假如某個程序員在定義 int 時少些了一個 final 關鍵字,那麼就會存在被其他人修改的風險,而反觀枚舉類,它“天然”就是一個常量類,不存在被修改的風險(原因詳見下半部分);

第二,使用 int 類型的語義不夠明確,比如我們在控制台打印時如果只輸出 1…2…3 這樣的数字,我們肯定不知道它代表的是什麼含義。

那有人就說了,那就使用常量字符唄,這總不會還不知道語義吧?實現示例代碼如下:

public static final String COLOR_RED = "RED";
public static final String COLOR_BLUE = "BLUE";
public static final String COLOR_GREEN = "GREEN";

但是這樣同樣存在一個問題,有些初級程序員會不按套路出牌,他們可能會直接使用字符串的值進行比較,而不是直接使用枚舉的字段,實現示例代碼如下:

public class EnumTest {
    public static final String COLOR_RED = "RED";
    public static final String COLOR_BLUE = "BLUE";
    public static final String COLOR_GREEN = "GREEN";
    public static void main(String[] args) {
        String color = "BLUE";
        if ("BLUE".equals(color)) {
            System.out.println("藍色");
        }
    }
}

這樣當我們修改了枚舉中的值,那程序就涼涼了。

枚舉使用場景

枚舉的常見使用場景是單例,它的完整實現代碼如下:

public class Singleton {
    // 枚舉類型是線程安全的,並且只會裝載一次
    private enum SingletonEnum {
        INSTANCE;
        // 聲明單例對象
        private final Singleton instance;
        // 實例化
        SingletonEnum() {
            instance = new Singleton();
        }
        private Singleton getInstance() {
            return instance;
        }
    }
    // 獲取實例(單例對象)
    public static Singleton getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }
    private Singleton() {
    }
    // 類方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}

因為枚舉只會在類加載時裝載一次,所以它是線程安全的,這也是《Effective Java》作者極力推薦使用枚舉來實現單例的主要原因。

知識擴展

枚舉為什麼是線程安全的?

這一點要從枚舉最終生成的字節碼說起,首先我們先來定義一個簡單的枚舉類:

public enum ColorEnumTest {
    RED, GREEN, BLANK, YELLOW;
}

然後我們再將上面的那段代碼編譯為字節碼,具體內容如下:

public final class ColorEnumTest extends java.lang.Enum<ColorEnumTest> {
  public static final ColorEnumTest RED;
  public static final ColorEnumTest GREEN;
  public static final ColorEnumTest BLANK;
  public static final ColorEnumTest YELLOW;
  public static ColorEnumTest[] values();
  public static ColorEnumTest valueOf(java.lang.String);
  static {};
}

從上述結果可以看出枚舉類最終會被編譯為被 final 修飾的普通類,它的所有屬性也都會被 staticfinal 關鍵字修飾,所以枚舉類在項目啟動時就會被 JVM 加載並初始化,而這個執行過程是線程安全的,所以枚舉類也是線程安全的類。

小貼士:代碼反編譯的過程是先用 javac 命令將 java 代碼編譯字節碼(.class),再使用 javap 命令查看編譯的字節碼。

枚舉比較小技巧

我們在枚舉比較時使用 == 就夠了,因為枚舉類是在程序加載時就創建了(它並不是 new 出來的),並且枚舉類不允許在外部直接使用 new 關鍵字來創建枚舉實例,所以我們在使用枚舉類時本質上只有一個對象,因此在枚舉比較時使用 == 就夠了。

並且我們在查看枚舉的 equlas() 源碼會發現,它的內部其實還是直接調用了 == 方法,源碼如下:

public final boolean equals(Object other) {
    return this==other;
}

總結

本文我們介紹了枚舉類的 7 種使用方法:常量、switch、枚舉中添加方法、覆蓋枚舉方法、實現接口、在接口中組織枚舉類和使用枚舉集合等,然後講了如果不使用枚舉類使用 int 類型和 String 類型存在的一些弊端:語義不夠清晰、容易被修改、存在被誤用的風險,所以我們在適合的環境下應該盡量使用枚舉類。並且我們還講了枚舉類的使用場景——單例,以及枚舉類為什麼是安全的,最後我們講了枚舉比較的小技巧,希望本文對你有幫助。

查看 & 鳴謝

https://www.iteye.com/blog/softbeta-1185573

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

與特斯拉 Mddel X 比拼 奧迪 2018 年左右將推純電動車

國外媒體美國汽車新聞網報導,奧迪將推出全新的純電動 SUV 車型與特斯拉 Model X 車型競爭,但奧迪認為插電混合動力車型才是目前新能源汽車市場最理想的車型。   奧迪將在 2018 年左右發布一款續航里程可達到 498 公里的純電動 SUV,將採用全新的造型設計理念,基於第二代 MLB 平台研發,並藉鑑新一代奧迪 Q5 車型的部分技術與設計。奧迪全新的純電動 SUV 車型將可在 20 分鐘內完成 80% 的充電,續航里程超過特斯拉 Model X 車型,對其構成不小的威脅。   不過,奧迪執行長斯泰德(Rupert Stadler)近日表示在未來的 10 至 15 年內,插電混合動力汽車將是消費者選擇新能源汽車時的首選車型。插電混合動力車型使用一台汽油引擎或柴油引擎和一台電動機聯合驅動,有一定的純電動續航里程,十分適合消費者在市區中駕駛。但是斯泰德同時表示隨著充電網絡建設的繼續,消費者未來對電動車的接受程度將增加。      

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

【【其他文章推薦】

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

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

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

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

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

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

特斯拉將公布儲能電池計畫 F-貿聯可望受惠

特斯拉傳將於 4 月 30 日公布儲能電池計畫,市場推測,該計畫將包括住宅與公共規模市場,業界傳出,本來就是特斯拉重要夥伴的 F-貿聯,有機會獲新電力線訂單。而今年在特斯拉需求持續強勁,與 Type C 等外接式擴充介面的需求爆發下,法人預期,該公司今年營運將逐季走高,全年營收看增 15%。   F-貿聯首季營收 19.04 億元,年增 10%,創下歷年同期新高。而特斯拉傳出將於下周公布最新的儲能電池計畫,據市場傳言表示,貿聯本來就是特斯拉在電動車電池模組用線與超級充電樁的線束的主力供應商,未來不排除大型公共規模用儲能電池,將與現有充電站結合。而貿聯有機會順勢切入取得新產品,並以公用市場應用的電力線為主,近日有望已小量出貨。   法人預估,F-貿聯今年除來自特斯拉的需求成長外,另在其他車用佈局方面,包括全地形車大客戶全車線束、擴大歐美客戶於倒車雷達等新產品的開發;另外針對美國官方對公營單位節能環保的需求,也配合客戶開發出得以改裝現有車為電動車的配備裝置,隨未來該商業模式成型,貢獻將逐步擴大。  

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

韓擬投資電動車及儲能 1.83 兆韓元 預計 2017 年起由民間主導新能源

據韓國產業通商資源部日前發表「活絡能源新產業及開發核心技術之規則」指出,韓國政府 2015 年將投入 1 兆 8,300 億韓元於電動車及能源儲存系統(ESS)等能源新產業領域,預計於 2017 年創造本土規模達 4 兆 6,000 億韓元規模的能源新產業市場、及 1 萬 4,000 個工作機會。   韓國今年在能源新產業領域投入的資金,分別由政府及公共機關出資 5,670 億韓元與 4,640 億韓元,民間企業則出資 8,020 億韓元。而具體的投資對象包括使用太陽能及風力發電之「能源自立島」、使用農家畜產廢棄物轉換為能源資源之「環保能源小鎮」及可減少消費者購買負擔之「電池型電動車租賃」等 8 大項目。   此外,韓國政府也計劃活絡剩餘電量可回賣電力市場的「需求資源交易市場」。產業通商資源部並表示,韓國將鼓勵民間將投資轉向能源新產業,以達成能源新產業市場自 2017 年起由民間主導的目標。

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

比亞迪Q1財報亮眼:淨利暴漲9倍

比亞迪昨(27)日晚間公佈2015年第1季財報,其營業收入為人民幣152.83億元(約新台幣762億元),較去年同期成長30.35%;歸屬於上市公司股東的淨利潤為人民幣1.21億元(約新台幣6億元),較去年同期成長910.75%;基本每股收益為人民幣0.05元(約新台幣0.25元)。   比亞迪表示,營業收入增長主要是汽車業務增加所致。同時比亞迪預估今年1至6月的淨利潤將達到人民幣4至4.8億元(約新台幣20至24億元),較去年同期的人民幣3.61億元(約新台幣18億元)成長10.90%至33.08%。   據證券時報網報導,在今年的佈局規劃方面,比亞迪將啟動「7+4」佈局,涵蓋私家車、公車在內的七個主要目標市場和四個特種車市場,並將憑藉自主品牌在SUV市場的突破性契機,將新能源汽車從轎車拓展至SUV車型,豐富比亞迪的產品類型。此外,比亞迪還將開發出集約用地的循環式立體充電機,積極尋求與協力廠商的合作,加快充電設施的普及。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

迄今最大海外訂單!比亞迪獲美國60輛電動大巴訂購

4月28日,比亞迪宣佈,洛杉磯大都會部分地區提供服務的長灘運輸署將訂購比亞迪的10輛電池動力巴士,並有增購不超過50輛電動巴士的選擇權。這是比亞迪迄今為止在國際市場上最大一筆電動巴士訂單。   據比亞迪的資訊顯示,其一年前在加州開設一家巴士工廠,以打入美國、加拿大和拉美市場,目前美國大約有50輛比亞迪電動巴士在運營。而比亞迪銷售總監侯雁上週在上海車展表示,比亞迪2015年計畫銷售大約15,000輛電動計程車和大約6,000輛電動巴士。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

特斯拉 Model 3 開發進度落後 量產恐待 2017 年底

期待特斯拉平價電動車 Model 3 明年上市的消費者可能要失望了,消息傳出 Model 3 Alpha 版原型車目前連影子都沒出現,意味者上市時間可能往後順延。   特斯拉執行長馬斯克(Elon Musk)宣布投入平價電動車市場時,曾信誓旦旦將在 2016 年底進入量產,但據科技網站 Venturebeat.com 報導,業界普遍預期 Model 3 量產最快可能要等到 2017 年底才會實現。   若跟隨 Model S 的開發時程走,Model 3 至少還需要 30 個月的開發時間,也就是 2017 年 12 月,首輛 Model 3 才有可能交車。

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

(七) SpringBoot起飛之路-整合SpringSecurity(Mybatis、JDBC、內存)

興趣的朋友可以去了解一下前五篇,你的贊就是對我最大的支持,感謝大家!

(一) SpringBoot起飛之路-HelloWorld

(二) SpringBoot起飛之路-入門原理分析

(三) SpringBoot起飛之路-YAML配置小結(入門必知必會)

(四) SpringBoot起飛之路-靜態資源處理

(五) SpringBoot起飛之路-Thymeleaf模板引擎

(六) SpringBoot起飛之路-整合JdbcTemplate-Druid-MyBatis

說明:

  • 這一篇的目的還是整合,也就是一個具體的實操體驗,原理性的沒涉及到,我本身也沒有深入研究過,就不獻醜了

  • SpringBoot 起飛之路 系列文章的源碼,均同步上傳到 github 了,有需要的小夥伴,隨意去 down

    • https://github.com/ideal-20/Springboot-Study-Code
  • 才疏學淺,就會點淺薄的知識,大家權當一篇工具文來看啦,不喜勿憤哈 ~

(一) 初識 Spring Security

(1) 引言

權限以及安全問題,雖然並不是一個影響到程序、項目運行的必須條件,但是卻是開發中的一項重要考慮因素,例如某些資源我們不想被訪問到或者我們某些方法想要滿足指定身份才可以訪問,我們可以使用 AOP 或者過濾器來實現要求,但是實際上,如果代碼涉及的邏輯比較多以後,代碼是極其繁瑣,冗餘的,而有很多開發框架,例如 Spring Security,Shiro,已經為我們提供了這種功能,我們只需要知道如何正確配置以及使用它了

(2) 基本介紹

先看一下官網的介紹

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一個功能強大且高度可定製的身份驗證和訪問控制框架。它是保護基於spring的應用程序的實際標準。

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security是一個框架,側重於為Java應用程序提供身份驗證和授權。與所有Spring項目一樣,Spring安全性的真正強大之處在於它很容易擴展以滿足定製需求

簡單的說,Spring Security 就是一個控制訪問權限,強大且完善的框架

Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分,同時它們也是 Spring Security 提供的核心功能

用戶認證:用戶認證就是指這個用戶身份是否合法,一般我們的用戶認證就是通過校驗用戶名密碼,來判斷用戶身份的合法性,確定身份合法后,用戶就可以訪問該系統

用戶授權:如果不同的用戶需要有不同等級的權限,就涉及到用戶授權,用戶授權就是對用戶能訪問的資源,所能執行的操作進行控制,根據不同用戶角色來劃分不同的權限

(二) 靜態頁面導入 And 環境搭建

(1) 關於靜態頁面

A:頁面介紹

頁面是我自己臨時弄得,有需要的朋友可以去我 GitHub:ideal-20 下載源碼,簡單說明一下這個頁面

做一個靜態頁面如果嫌麻煩,也可以單純的自己創建一些簡單的頁面,寫幾個標題文字,能體現出當前是哪個頁面就好了

我代碼中用的這些頁面,就是拿開源的前端組件框架進行了一點的美化,然後方便講解一些功能,頁面模板主要是配合 Thymeleaf

1、目錄結構

├── index.html                        // 首頁
├── images                            // 首頁圖片,僅美觀,無實際作用
├── css                               // 上線項目文件,放在服務器即可正常訪問
├── js                                // 項目截圖
├── views                             // 總子頁面文件夾,權限驗證的關鍵頁面
│   ├── login.html					  // 自製登錄頁面(用來替代 Spring Security 默認的 )
│   ├── L-A							  // L-A 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-B							  // L-B 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-C							  // L-C 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html

B:導入到項目

主要就是把基本一些鏈接,引入什麼的先替換成 Thymeleaf 的標籤格式,這裏語法用的不是特別多,即使對於 Thymeleaf 不是很熟悉也是很容易看懂的,當然如果仍然感覺有點吃力,可以單純的做成 html,將就一下,或者去看一下我以前的文章哈,裏面有關於 Thymeleaf 入門的講解

css、image、js 放到 resources –> static 下 ,views 和 index.html 放到 resources –> templates下

(2) 環境搭建

A:引入依賴

這一部分引入也好,初始化項目的時候,勾選好自動生成也好,只要依賴正常導入了即可

  • 引入 Spring Security 模塊
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

關鍵的依賴主要就是上面這個啟動器,但是還有一些就是常規或者補充的了,例如 web、thymeleaf、devtools

thymeleaf-extras-springsecurity5 這個後面講解中會提到,是用來配合 Thymeleaf 整合 Spring Security 的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

B:頁面跳轉 Controller

因為我們用了模板,頁面的跳轉就需要交給 Controller 了,很簡單,首先是首頁的,當然關於頁面這個就無所謂了,我隨便跳轉到了我的博客,接着還有一個登錄頁面的跳轉

有一個小 Tip 需要提一下,因為 L-A、L-B、L-C 文件夾下都有3個頁面 a.html 、b.html 、c.html,所以可以利用 @PathVariable 寫一個較為通用的跳轉方法

@Controller
public class PageController {

    @RequestMapping({"/", "index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/about")
    public String toAboutPage() {
        return "redirect:http://www.ideal-20.cn";
    }

    @RequestMapping("/toLoginPage")
    public String toLoginPage() {
        return "views/login";
    }

    @RequestMapping("/levelA/{name}")
    public String toLevelAPage(@PathVariable("name") String name) {
        return "views/L-A/" + name;
    }

    @RequestMapping("/levelB/{name}")
    public String toLevelBPage(@PathVariable("name") String name) {
        return "views/L-B/" + name;
    }

    @RequestMapping("/levelC/{name}")
    public String toLevelCPage(@PathVariable("name") String name) {
        return "views/L-C/" + name;
    }
}

C:環境搭建最終效果

  • 為了貼圖方便,我把頁面拉窄了一點
  • 首頁右上角應該為登錄的鏈接,這裡是因為,我運行的是已經寫好的代碼,不登錄頁面例如 L-A-a 等模塊就显示不出來,所以拿一個定義好的管理員身份登陸了
  • 關於如何使其自動切換显示登陸還是登錄后信息,在後面會講解

1、首頁

2、子頁面

L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是一樣的,只是文字有一點變化

3、登陸頁面

(三) 整合 Spring Security (內存中)

這一部分,為了簡化一些,容易理解一些,沒有從帶數據的場景出發(因為涉及代碼少一些,所以講解會多一點),而是直接將一些身份等等寫死了,寫到了內存中,方便理解,接着會在下一個標題中給出含有數據庫的寫法(講解會少一些,重點只說一些與前一種的不同點)

(1) 配置授權內容

A:源碼了解用戶授權方式

可以去官網看一下,官網有提供給我們一些樣例,其中有一個關於配置類的小樣例,也就是下面這個,我們通過這個例子,展開分析

https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#jc-custom-dsls

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .apply(customDsl())
                .flag(true)
                .and()
            ...;
    }
}

1、創建 config –> SecurityConfig 配置類

  • 創建一個配置類,像官網中一樣,繼承 WebSecurityConfigurerAdapter
  • 類上添加 @EnableWebSecurity 註解,代表開啟WebSecurity模式
  • 重寫 configure(HttpSecurity http) 方法
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

既然是重寫,那麼我們可以點進去,看一下父類中關於 configure(HttpSecurity http) 方法的源碼註釋,它有很多有用的信息

我摘選出這麼兩小段,第一段的意思就是 ,我們想要使用 HttpSecurity ,要通過重寫,不能通過 super 調用,否則會有覆蓋問題,第二段就是給出了一個默認的配置方式

* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();

2、按照源碼的註釋分析

我們先按照剛才看到的註釋寫出來,首先能看到,它是支持一個鏈式調用的

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and().formLogin()
            .and().httpBasic();
}
  • 通過字面意思也很好理解,authorizeRequests 是關於請求授權的,所以要涉及到關於請求授權(允許指定身份用戶訪問不同權限的資源)的問題就需要調用了

  • 其次,anyRequest().authenticated() 也就是說所有HTTP請求都需要被認證

  • 接着看,通過 and() 連接了一些新的內容,例如選擇表單登錄還是 HTTPBasic 的方式(這裏認證的過程就是讓你輸入用戶名密碼,檢測你的身份,兩種方式表單或者那種彈窗)

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證,通常需要配合HTTPS來保證信息傳輸的安全

給大家演示一下:

  • 如果不指定一種認證方式 .and().formLogin() 或者 .and().httpBasic() 訪問任何頁面都會提示 403 禁止訪問的錯誤
  • 指定 .and().formLogin() 認證,彈出一個表單頁面(自帶的,和自己創建的沒關係)
  • 指定 .and().httpBasic(); 認證,彈出一個窗口進行 HTTPBasic 認證

B:自定製用戶授權

1、先看源碼註釋

默認配置,設定了所有 HTTP 請求 都需要進行認證,所以我們在訪問首頁等的時候也會被攔截,但是實際情況下,有一些頁面是可以被任何人訪問的,例如首頁,或者自定義的登陸的等頁面,這時候需要用自己定義一些用戶授權的規則

在 WebSecurityConfigurerAdapter 的 formLogin() 註釋附近,又看到了一個有意思的內容

注:&quot 代表引號

* 		http
* 			.authorizeRequests(authorizeRequests ->
* 				authorizeRequests
* 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
* 			)

這就是我們想要找的,自定義的配置,通過一個一個 antMatchers 進行匹配,通過 hasRole 來規定其合法的身份,也就是說只有滿足這個身份的用戶才能訪問前面規定的路徑資源

Matchers 前面的 ant 前綴代表着,他可以用 ant 風格的路徑表達式(舉例的時候就能看懂了)

通配符 說明
? 匹配任何單字符
* 匹配0或者任意數量的字符
** 匹配0或者更多的目錄

補充: 如果想用正則表達式的方式,可以用這個方法 .regexMatchers()

當然了,有很多情況下,你想要讓任何人都可以訪問某個路徑,例如首頁,permitAll() 方法 就可以達到這種效果,在這裏補充一些常用的方法

  • permitAll() :允許任何訪問

  • denyAll():拒絕所有訪問

  • anonymous():允許匿名用戶訪問

  • authenticated() :允許認證的用戶進行訪問

  • hasRole(String) :如果用戶具備給定角色(用戶組)的話,就允許訪問/

  • hasAnyRole(String…) :如果用戶具有給定角色(用戶組)中的一個的話,允許訪問.

  • rememberMe() :如果用戶是通過Remember-me功能認證的,就允許訪問

  • fullyAuthenticated():如果用戶是完整認證的話(不是通過Remember-me功能認證的),就允許訪問

  • hasAuthority(String):如果用戶具備給定權限的話就允許訪問

  • hasAnyAuthority(String…) :如果用戶具備給定權限中的某一個的話,就允許訪問

  • hasIpAddress(String) :如果請求來自給定ip地址的話,就允許訪問.

  • not() :對其他訪問結果求反

說明:hasAnyAuthority(“ROLE_ADMIN”) 和 hasRole(“ADMIN”) 的區別就是,後者會自動使用 它會自動使用 “ROLE_” 前綴

2、我們來定製一下用戶授權

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
        	.antMatchers("/").permitAll()
        	.antMatchers("/levelA/**").hasRole("vip1")
        	.antMatchers("/levelB/**").hasRole("vip2")
        	.antMatchers("/levelC/**").hasRole("vip3")
        	.and().formLogin();
}

我們上面代碼的意思就是,當訪問 /levelA/ /levelB/ /levelC/ 這三個路徑下面的任意文件(這裡有 a/b/c.html)都需要認證,身份分別是對應 vip1、vip2、vip3,而其他頁面,就可以隨便訪問了

很顯然,雖然說規定了授權的內容,也就是哪些權限的用戶,可以訪問哪些資源,但是我們由於並沒有配置用戶的信息(合法的或者非法的),所以自然,前面的登錄頁面,都是會直接報錯的,下面我們來分析一下,如何進行認證

(2) 配置認證內容

A:源碼了解用戶認證方式

剛才的授權部分,我們重寫了 configure(HttpSecurity http) 方法,我們繼續看看重寫方法中,有沒有可能幫助我們驗證身份,進行用戶認證的方法,我們首先來看這個方法 configure(AuthenticationManagerBuilder auth)

先去看一下源碼的註釋(此部分的格式,我稍微修改了一下,方便觀看):

這是其中他局舉的一個例子,其實這個就是我們想要的,看註釋也可以看出來,他就是用來在內存中啟用基於用戶名的身份驗證的

* protected void configure(AuthenticationManagerBuilder auth) {
*  auth
*  // enable in memory based authentication with a user named
*  // &quot;user&quot; and &quot;admin&quot;
*  		.inMemoryAuthentication()
*   		.withUser(&quot;user&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;).and()
*        	.withUser(&quot;admin&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;, &quot;ADMIN&quot;);
* }

照貓畫虎,我們也先這麼做

B:自定製用戶認證

代碼如下:

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("admin")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2", "vip3")
            .and()
            .withUser("ideal-20")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2")
            .and()
            .withUser("jack")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1");
}

我們就是照着例子打的,但是,其中我們又加入了編碼的問題,它要求必須進行編碼,否則會報錯,官方推薦的是bcrypt加密方式,我們這裏就用這種,當然自己用常見的 MD5 等等都是可以的,可以自己寫一個工具類

到這裏,測試一下,實際上就可以按照身份的不同,從而擁有訪問不同路徑資源你的權限了,主要的功能已經實現了,下面補充一些,更加友好的功能,例如登錄註銷按鈕的显示,以及記住密碼等等

(3) 註銷問題

1、註銷配置

當然了,前面因為已經有很多配置了,所以可以通過 .and() 進行連接,例如 .and().xxx,或者像下面給出的,單獨再寫一個 http.xxx

@Override
protected void configure(HttpSecurity http) throws Exception {
   ......
    // 註銷配置
	http.logout().logoutSuccessUrl("/")
}

上面短短一句的代碼, logout() 代表開啟了註銷的配置,logoutSuccessUrl(“/”),代表註銷成功后,返回的頁面,我們令其註銷后回到首頁

前台的頁面中,我已經給出了註銷的按鈕的代碼,當然這不是固定的,不同的 ui 框架,不同的模板引擎都是不一樣的,但是路徑是 /logout

<a class="item" th:href="@{/logout}">
  <i class="address card icon"></i> 註銷
</a>

(4) 根據身份權限显示組件

A:登錄、註銷的显示

還有這樣一種問題,右上角,未登錄的時候,應該显示登陸按鈕,登錄后,應該显示用戶信息,以及註銷等等,這一部分,主要是頁面這邊的問題

显示的條件其實很簡單,就是判斷是否認證了,認證了就取出一些值,沒認證就显示登陸

1、這時,我們就需要引入一個 Thymeleaf 配合 Spring Security 的一個依賴 (當然了如果是別的技術,就不一樣了)

地址如下:

https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

2、導入命名空間

引入這個文件的目的,就是為了在頁面寫權限判斷等相關的內容的時候可以有提示

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

3、修改導航欄邏輯

<!--登錄註銷-->
<div class="right menu">

  <!--如果未登錄-->
  <div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLoginPage}">
      <i class="address card icon"></i> 登錄
    </a>
  </div>

  <!--如果已登錄-->
  <div sec:authorize="isAuthenticated()">
    <a class="item">
      <i class="address card icon"></i>
      用戶名:<span sec:authentication="principal.username"></span>
      <!--角色:<span sec:authentication="principal.authorities"></span>-->
    </a>
  </div>

  <div sec:authorize="isAuthenticated()">
    <a class="item" th:href="@{/logout}">
      <i class="address card icon"></i> 註銷
    </a>
  </div>
</div>

B:組件面板的显示

上面的代碼,解決了導航欄的問題,但是例如我們首頁中,一些板塊,對於不同的用戶的显示也是不同的嗎

正如上面的例子,沒有登錄的用戶,是不能訪問了 /levelA/、 /levelB/、 /levelC/ 下面的任何文件的,只有登錄的用戶,根據權限的大小,才能訪問某一個,或者所有

而我們首頁部分的三個面板就是用來显示這三塊的鏈接,對於沒有足夠身份的人,實際上显示這個面板就已經是多餘了,當然,你可以選擇显示,但是如果想要根據身份显示面板怎麼做呢?

關鍵就是在 div 中添加了這樣一句權限的代碼,沒有這個指定的身份,這個面板就不會显示sec:authorize="hasRole('vip1')"

<div class="column" sec:authorize="hasRole('vip1')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelA/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/b}">L-A-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/c}">L-A-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelB/a}">L-B-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/b}">L-B-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/c}">L-B-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelC/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/b}">L-C-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/c}">L-C-c</a>
    </div>
  </div>
</div>

演示一下:

(5) 記住用戶

如果重啟瀏覽器后,就需要重新登錄,對於一部分用戶來說,他們認為是麻煩的,所以很多網站登錄時都提供記住用戶這種選項

1、一個簡單的配置就可以達到目的,這種情況下,默認的登陸頁面,就會多出一個記住用戶的單選框

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//記住用戶
    http.rememberMe();
}

2、但是如果,登陸頁面是自定義(下面講)的怎麼辦呢?,其實只要修改為如下配置即可,

//定製記住我的參數!
http.rememberMe().rememberMeParameter("remember");

上面的 remember 對應 input 中的 name 屬性值

<input type="checkbox" name="remember"/>
<label>記住密碼</label>

3、它做了哪些事情呢?

可以打開頁面的控制台看一下,實際上配置后,用戶選擇記住密碼后,會自動幫我們增加一個 cookie 叫做 remember-me,過期時間為 14 天,當註銷的時候,這個 cookie 就會被刪除了

(6) 定製登錄頁面

1、配置

自帶的登陸頁面確實,還是比較丑的,版本更低一些的,更是不美觀,如果想要使用自己定製的登陸頁面,可以加入下面的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	// 登陸表單提交請求
    http.formLogin()
	.usernameParameter("username")
	.passwordParameter("password")
	.loginPage("/toLoginPage")
	.loginProcessingUrl("/login")
}
  • .loginPage("/toLoginPage") 就是說,當你訪問一些需要用戶權限認證的頁面時,就會發起這個請求,到你的登錄頁面
  • .loginProcessingUrl("/login") 就是表單中,真正要提交請求的一個路徑
  • 其餘兩個就是關於用戶名和密碼的一個獲取,其值和頁面中用戶名密碼的 name 屬性值一致

2、頁面跳轉

前面我們就提過這個,回顧一下

@RequestMapping("/toLoginPage")
public String toLoginPage() {
    return "views/login";
}

3、自定義登錄頁面的表單提交 action 設置

<form id="login" class="ui fluid form segment" th:action="@{/login}" method="post">
	......
</form>

(7) 關閉csrf

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
	http.csrf().disable();
}

(四) 整合 Spring Security (JDBC)

因為配置內存中的用戶還是相對簡單一些的,所以一些細節也都說了一下,基於上面的基礎,來看一下 如何用 JDBC 實現上面的功能,當然了這部分只能算補充,基本不會這麼用的,下面的整合 MyBatis 才是常用的()

(1) 創建表以及數據

這裏創建了三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(2) 創建實體

我使用了 lombok,不過自己寫 get set 構造方法 也是一樣的

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer uid;
    private String username;
    private String password;
    private String roles;
}

(3) 配置授權內容

這部分沒什麼區別

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //註銷
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
            .and().csrf().disable();
}

(4) 配置認證內容

A:配置數據庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

server:
  port: 8082

B:具體配置

以幾個注意的地方:

  • 查詢語句都是通過 username 查詢

  • usersByUsernameQuery()方法里的參數一定要有一個 true 的查詢結果,所以我直接在查詢語句中寫了一個 true

  • MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候簡單點

  • DataSource dataSource 要在前面注入進來(選擇 sql 的)

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("select username,password,true from user where username = ?")
            .authoritiesByUsernameQuery("select username,roles from user where username = ?")
            .passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence rawPassword) {
                    return MD5Util.MD5EncodeUtf8((String) rawPassword);
                }

                @Override
                public boolean matches(CharSequence rawPassword, String encodedPassword) {
                    return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
                }
            });
}

C:MD5工具類

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

D:修改頁面

到這裏,JDBC 的整合方式就成功了,至於前面的頁面只需要根據我們自己設計的權限進行修改,別的地方和前面內存中的方式是一樣的

<div class="ui stackable three column grid">
  <div class="column" sec:authorize="hasAnyRole('USER','ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelA/a}">L-A-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/b}">L-A-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/c}">L-A-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasAnyRole('ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelB/a}">L-B-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/b}">L-B-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/c}">L-B-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasRole('SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelC/a}">L-C-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/b}">L-C-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/c}">L-C-c</a>
      </div>
    </div>
  </div>
  <!-- <div class="column"></div> -->
</div>

(五) 整合 Spring Security (MyBatis)

因為這部分內容是比較常用的,所以,我盡可能給的完善一些

(1) 添加依賴

像 lombok、commons-lang3 都不是必須的,都是可以使用原生的一些手段替代的,寫到那裡我會提的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

(2) 創建表

和 JDBC 部分用同樣的表

三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(3) 整合 MyBatis

在進行 Spring Security 的配置前,最好先把 MyBatis 先整合好,這樣等會只考慮 Spring Security 的問題就可以了

說明:這部分我盡可能簡化了,例如連接池就用默認的,如果這部分感覺還是有點問題,可以參考一下我前幾篇,關於整合 MyBatis 的文章

A:配置數據庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: cn.ideal.pojo

server:
  port: 8081

B:配置 Mapper 以及 XML

UserMapper

@Mapper
public interface UserMapper {
    User queryUserByUserName(String username);
}

mapper/UserMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.ideal.mapper.UserMapper">
    <select id="queryUserByUserName" parameterType="String" resultType="cn.ideal.pojo.User">
         select * from user where username = #{username}
    </select>
</mapper>

這裏就不演示測試了,是沒有問題的

(4) 配置授權內容

這部分沒什麼好說的,和前面的都一樣,解釋在內存中配置用戶時已經詳細說過了

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //註銷
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
            .and().csrf().disable();
}

(5) 配置認證內容

A:創建 UserService

創建一個類,實現 UserDetailsService,其實主要就是為了 loadUserByname 方法,在這個類中,我們可以注入 mapper 等等,去查用戶,如果查不到,就還留在這個頁面,如果查到了,做出一定邏輯后(例如判空等等),就會把用戶信息封裝到 Spring Security 自己的的 User類中去,Spring Security 拿前台的數據和它比較,做出操作,例如認證成功或者錯誤

注意:

  • StringUtils 是 commons.lang3 下的,使用需要導包,我們用了一個判空功能,不想用的話,用原生的是一個道理,這不是重點
  • 注意區分自己的 User 和 Spring Security 的 User
@Service
public class UserService<T extends User> implements UserDetailsService{

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.queryUserByUserName(username);
        if (username == null){
            throw  new UsernameNotFoundException("用戶名不存在");
        }

        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        String role = user.getRoles();
        if (StringUtils.isNotBlank(role)){
            authorityList.add(new SimpleGrantedAuthority(role.trim()));
        }
        return new org.springframework.security.core.userdetails
            .User(user.getUsername(),user.getPassword(),authorityList);
    }
}

B:修改配置類

這裏也很熟悉,我們調用就可以調用 userDetailsService 了,同樣還需要指定編碼相關的內容 實例化 PasswordEncoder,就需要重寫 encode、 matches

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
        @Override
        public String encode(CharSequence rawPassword) {
            return MD5Util.MD5EncodeUtf8((String) rawPassword);
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
        }
    });
}

C:MD5 工具類補充

其實上面已經給出了,但是怕大家看起來不方便,這裏再貼一下

MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候可以簡單點

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

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

【【其他文章推薦】

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

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

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

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

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

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