【Spring註解驅動開發】使用@Scope註解設置組件的作用域

寫在前面

Spring容器中的組件默認是單例的,在Spring啟動時就會實例化並初始化這些對象,將其放到Spring容器中,之後,每次獲取對象時,直接從Spring容器中獲取,而不再創建對象。如果每次從Spring容器中獲取對象時,都要創建一個新的實例對象,該如何處理呢?此時就需要使用@Scope註解設置組件的作用域。

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

本文內容概覽

  • @Scope註解概述
  • 單實例bean作用域
  • 多實例bean作用域
  • 單實例bean作用域如何創建對象?
  • 多實例bean作用域如何創建對象?
  • 單實例bean注意的事項
  • 多實例bean注意的事項
  • 自定義Scope的實現

@Scope註解概述

@Scope註解能夠設置組件的作用域,我們先來看@Scope註解類的源碼,如下所示。

package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
	@AliasFor("scopeName")
	String value() default "";
    /**
	 * Specifies the name of the scope to use for the annotated component/bean.
	 * <p>Defaults to an empty string ({@code ""}) which implies
	 * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}.
	 * @since 4.2
	 * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
	 * @see ConfigurableBeanFactory#SCOPE_SINGLETON
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
	 * @see #value
	 */
	@AliasFor("value")
	String scopeName() default "";
    
	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}

從源碼中可以看出,在@Scope註解中可以設置如下值。

ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

很明顯,在@Scope註解中可以設置的值包括ConfigurableBeanFactory接口中的SCOPE_PROTOTYPE和SCOPE_SINGLETON,以及WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION。這些都是什麼鬼?別急,我們來一個個查看。

首先,我們進入到ConfigurableBeanFactory接口中,發現在ConfigurableBeanFactory類中存在兩個常量的定義,如下所示。

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
	String SCOPE_SINGLETON = "singleton";
	String SCOPE_PROTOTYPE = "prototype";
    /*****************此處省略N多行代碼*******************/
}

沒錯,SCOPE_SINGLETON就是singleton,SCOPE_PROTOTYPE就是prototype。

那麼,WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION又是什麼鬼呢?就是說,當我們使用了Web容器來運行Spring應用時,在@Scope註解中可以設置WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION的值,而SCOPE_REQUEST的值就是request,SCOPE_SESSION的值就是session。

綜上,在@Scope註解中的取值如下所示。

  • singleton:表示組件在Spring容器中是單實例的,這個是Spring的默認值,Spring在啟動的時候會將組件進行實例化並加載到Spring容器中,之後,每次從Spring容器中獲取組件時,直接將實例對象返回,而不必再次創建實例對象。從Spring容器中獲取對象,小夥伴們可以理解為從Map對象中獲取對象。
  • prototype:表示組件在Spring容器中是多實例的,Spring在啟動的時候並不會對組件進行實例化操作,而是每次從Spring容器中獲取組件對象時,都會創建一個新的實例對象並返回。
  • request:每次請求都會創建一個新的實例對象,request作用域用在spring容器的web環境中。
  • session:在同一個session範圍內,創建一個新的實例對象,也是用在web環境中。
  • application:全局web應用級別的作用於,也是在web環境中使用的,一個web應用程序對應一個bean實例,通常情況下和singleton效果類似的,不過也有不一樣的地方,singleton是每個spring容器中只有一個bean實例,一般我們的程序只有一個spring容器,但是,一個應用程序中可以創建多個spring容器,不同的容器中可以存在同名的bean,但是sope=aplication的時候,不管應用中有多少個spring容器,這個應用中同名的bean只有一個。

其中,request和session作用域是需要Web環境支持的,這兩個值基本上使用不到,如果我們使用Web容器來運行Spring應用時,如果需要將組件的實例對象的作用域設置為request和session,我們通常會使用request.setAttribute(“key”,object)和session.setAttribute(“key”, object)的形式來將對象實例設置到request和session中,通常不會使用@Scope註解來進行設置。

單實例bean作用域

首先,我們在io.mykit.spring.plugins.register.config包下創建PersonConfig2配置類,在PersonConfig2配置類中實例化一個Person對象,並將其放置在Spring容器中,如下所示。

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

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Scope註解設置的作用域
 */
@Configuration
public class PersonConfig2 {

    @Bean("person")
    public Person person(){
        return new Person("binghe002", 18);
    }
}

接下來,在SpringBeanTest類中創建testAnnotationConfig2()測試方法,在testAnnotationConfig2()方法中,創建ApplicationContext對象,創建完畢后,從Spring容器中按照id獲取兩個Person對象,並打印兩個對象是否是同一個對象,代碼如下所示。

@Test
public void testAnnotationConfig2(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    //從Spring容器中獲取到的對象默認是單實例的
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
    System.out.println(person1 == person2);
}

由於對象在Spring容器中默認是單實例的,所以,Spring容器在啟動時就會將實例對象加載到Spring容器中,之後,每次從Spring容器中獲取實例對象,直接將對象返回,而不必在創建新對象實例,所以,此時testAnnotationConfig2()方法會輸出true。如下所示。

這也驗證了我們的結論:對象在Spring容器中默認是單實例的,Spring容器在啟動時就會將實例對象加載到Spring容器中,之後,每次從Spring容器中獲取實例對象,直接將對象返回,而不必在創建新對象實例。

多實例bean作用域

修改Spring容器中組件的作用域,我們需要藉助於@Scope註解,此時,我們將PersonConfig2類中Person對象的作用域修改成prototype,如下所示。

@Configuration
public class PersonConfig2 {

    @Scope("prototype")
    @Bean("person")
    public Person person(){
        return new Person("binghe002", 18);
    }
}

其實,使用@Scope設置作用域就等同於在XML文件中為bean設置scope作用域,如下所示。

此時,我們再次運行SpringBeanTest類的testAnnotationConfig2()方法,此時,從Spring容器中獲取到的person1對象和person2對象還是同一個對象嗎?

通過輸出結果可以看出,此時,輸出的person1對象和person2對象已經不是同一個對象了。

單實例bean作用域何時創建對象?

接下來,我們驗證下在單實例作用域下,Spring是在什麼時候創建對象的呢?

首先,我們將PersonConfig2類中的Person對象的作用域修改成單實例,並在返回Person對象之前打印相關的信息,如下所示。

@Configuration
public class PersonConfig2 {
    @Scope
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

接下來,我們在SpringBeanTest類中創建testAnnotationConfig3()方法,在testAnnotationConfig3()方法中,我們只創建Spring容器,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
}

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

從輸出的結果信息可以看出,Spring容器在創建的時候,就將@Scope註解標註為singleton的組件進行了實例化,並加載到Spring容器中。

接下來,我們運行SpringBeanTest類中的testAnnotationConfig2(),結果信息如下所示。

說明,Spring容器在啟動時,將單實例組件實例化之後,加載到Spring容器中,以後每次從容器中獲取組件實例對象,直接返回相應的對象,而不必在創建新對象。

多實例bean作用域何時創建對象?

如果我們將對象的作用域修改成多實例,那什麼時候創建對象呢?

此時,我們將PersonConfig2類的Person對象的作用域修改成多實例,如下所示。

@Configuration
public class PersonConfig2 {

    @Scope("prototype")
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

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

可以看到,終端並沒有輸出任何信息,說明在創建Spring容器時,並不會實例化和加載多實例對象,那多實例對象是什麼時候實例化的呢?接下來,我們在SpringBeanTest類中的testAnnotationConfig3()方法中添加一行獲取Person對象的代碼,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
}

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

從結果信息中,可以看出,當向Spring容器中獲取Person實例對象時,Spring容器實例化了Person對象,並將其加載到Spring容器中。

那麼,問題來了,此時Spring容器是否只實例化一個Person對象呢?我們在SpringBeanTest類中的testAnnotationConfig3()方法中再添加一行獲取Person對象的代碼,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
}

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

從輸出結果可以看出,當對象的Scope作用域為多實例時,每次向Spring容器獲取對象時,都會創建一個新的對象並返回。此時,獲取到的person1和person2就不是同一個對象了,我們也可以打印結果信息來進行驗證,此時在SpringBeanTest類中的testAnnotationConfig3()方法中打印兩個對象是否相等,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
    System.out.println(person1 == person2);
}

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

可以看到,當對象是多實例時,每次從Spring容器中獲取對象時,都會創建新的實例對象,並且每個實例對象都不相等。

單實例bean注意的事項

單例bean是整個應用共享的,所以需要考慮到線程安全問題,之前在玩springmvc的時候,springmvc中controller默認是單例的,有些開發者在controller中創建了一些變量,那麼這些變量實際上就變成共享的了,controller可能會被很多線程同時訪問,這些線程併發去修改controller中的共享變量,可能會出現數據錯亂的問題;所以使用的時候需要特別注意。

多實例bean注意的事項

多例bean每次獲取的時候都會重新創建,如果這個bean比較複雜,創建時間比較長,會影響系統的性能,這個地方需要注意。

自定義Scope

如果Spring內置的幾種sope都無法滿足我們的需求的時候,我們可以自定義bean的作用域。

1.如何實現自定義Scope

自定義Scope主要分為三個步驟,如下所示。

(1)實現Scope接口

我們先來看下Scope接口的定義,如下所示。

package org.springframework.beans.factory.config;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.lang.Nullable;

public interface Scope {

    /**
    * 返回當前作用域中name對應的bean對象
    * name:需要檢索的bean的名稱
    * objectFactory:如果name對應的bean在當前作用域中沒有找到,那麼可以調用這個ObjectFactory來創建這個對象
    **/
    Object get(String name, ObjectFactory<?> objectFactory);

    /**
     * 將name對應的bean從當前作用域中移除
     **/
    @Nullable
    Object remove(String name);

    /**
     * 用於註冊銷毀回調,如果想要銷毀相應的對象,則由Spring容器註冊相應的銷毀回調,而由自定義作用域選擇是不是要銷毀相應的對象
     */
    void registerDestructionCallback(String name, Runnable callback);

    /**
     * 用於解析相應的上下文數據,比如request作用域將返回request中的屬性。
     */
    @Nullable
    Object resolveContextualObject(String key);

    /**
     * 作用域的會話標識,比如session作用域將是sessionId
     */
    @Nullable
    String getConversationId();

}

(2)將Scope註冊到容器

需要調用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下這個方法的聲明

/**
* 向容器中註冊自定義的Scope
*scopeName:作用域名稱
* scope:作用域對象
**/
void registerScope(String scopeName, Scope scope);

(3)使用自定義的作用域

定義bean的時候,指定bean的scope屬性為自定義的作用域名稱。

2.自定義Scope實現案例

例如,我們來實現一個線程級別的bean作用域,同一個線程中同名的bean是同一個實例,不同的線程中的bean是不同的實例。

這裏,要求bean在線程中是共享的,所以我們可以通過ThreadLocal來實現,ThreadLocal可以實現線程中數據的共享。

此時,我們在io.mykit.spring.plugins.register.scope包下新建ThreadScope類,如下所示。

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

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定義本地線程級別的bean作用域,不同的線程中對應的bean實例是不同的,同一個線程中同名的bean是同一個實例
 */
public class ThreadScope implements Scope {

    public static final String THREAD_SCOPE = "thread";

    private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new HashMap<>();
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get().get(name);
        if (Objects.isNull(bean)) {
            bean = objectFactory.getObject();
            beanMap.get().put(name, bean);
        }
        return bean;
    }

    @Nullable
    @Override
    public Object remove(String name) {
        return this.beanMap.get().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        //bean作用域範圍結束的時候調用的方法,用於bean清理
        System.out.println(name);
    }

    @Nullable
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Nullable
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

在ThreadScope類中,我們定義了一個常量THREAD_SCOPE,在定義bean的時候給scope使用。

接下來,我們在io.mykit.spring.plugins.register.config包下創建PersonConfig3類,並使用@Scope(“thread”)註解標註Person對象的作用域為Thread範圍,如下所示。

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

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Scope註解設置的作用域
 */
@Configuration
public class PersonConfig3 {

    @Scope("thread")
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

最後,我們在SpringBeanTest類中創建testAnnotationConfig4()方法,在testAnnotationConfig4()方法中創建Spring容器,並向Spring容器中註冊ThreadScope對象,接下來,使用循環創建兩個Thread線程,並分別在每個線程中獲取兩個Person對象,如下所示。

@Test
public void testAnnotationConfig4(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig3.class);
    //向容器中註冊自定義的scope
    context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());

    //使用容器獲取bean
    for (int i = 0; i < 2; i++) { 
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

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

從輸出中可以看到,bean在同樣的線程中獲取到的是同一個bean的實例,不同的線程中bean的實例是不同的。

注意:這裏,我將Person類進行了相應的調整,去掉Lombok的註解,手動寫構造函數和setter與getter方法,如下所示。

package io.mykit.spring.bean;

import java.io.Serializable;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試實體類
 */
public class Person implements Serializable {
    private static final long serialVersionUID = 7387479910468805194L;
    private String name;
    private Integer age;

    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

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

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

寫在最後

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

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

輕鬆解決Github連接緩慢、圖裂問題

1 簡介

  gayhub(誤)github作為全世界最大的開源代碼庫以及版本控制系統,是用來託管項目以及學習開源技術非常好的平台,是我心中最好的學習網站,我們公眾號(Python大數據分析)的眾多技術文章對應的數據和代碼也都一直託管在github上。

  但熟悉github的朋友應該都被其越來越慢的連接速度,以及“全員圖裂”所困擾:

圖1

  本文就將參考github倉庫(https://github.com/521xueweihan/GitHub520 ),教大家如何在不kexue上網的前提下,簡單幾步解決github訪問緩慢已經各種圖裂的問題。

2 通過修改本地hosts文件加速github

2.1 手動修改更新

  首先我們需要找到自己設備上的hosts文件,不同的平台其存放路徑各不相同,主要的平台hosts文件所在路徑如下:

  • Windows :C:\Windows\System32\drivers\etc\hosts
  • Linux:/etc/hosts
  • Mac:/etc/hosts

  以Windows為例,按照上面的說明,進入C:\Windows\System32\drivers\etc目錄,找到hosts文件:

圖2

  這是一個無拓展名的文件,我們可以使用記事本、notepad++等文本編輯器來打開它,將下面的內容(這部分內容在寫作本文的時候是有效的,如果你在嘗試時它們已經失效了,可以前往上文提到的github倉庫複製最新的,或者參考下文中的第2種方法)複製,並粘貼hosts文件的最後:

# GitHub520 Host Start
185.199.108.154                                   github.githubassets.com
199.232.68.133                                    camo.githubusercontent.com
199.232.68.133                                    github.map.fastly.net
199.232.69.194                                    github.global.ssl.fastly.net
140.82.112.3                                      github.com
140.82.113.5                                      api.github.com
199.232.68.133                                    raw.githubusercontent.com
199.232.68.133                                    user-images.githubusercontent.com
199.232.68.133                                    favicons.githubusercontent.com
199.232.68.133                                    avatars5.githubusercontent.com
199.232.68.133                                    avatars4.githubusercontent.com
199.232.68.133                                    avatars3.githubusercontent.com
199.232.68.133                                    avatars2.githubusercontent.com
199.232.68.133                                    avatars1.githubusercontent.com
199.232.68.133                                    avatars0.githubusercontent.com
# GitHub520 Host End

圖3

  如果保存時需要管理員權限,按照提示以管理員方式重新打開再保存即可,正常情況下在保存退出後會立即生效,如果依然加載不出圖,可以根據自己系統的不同來執行對應的命令刷新DNS重啟機器即可:

  • Windows:ipconfig /flushdns
  • Linux:sudo rcnscd restart
  • Mac:sudo killall -HUP mDNSResponder

  接下來我們來看看這種方法的效果如何,在遵循上述流程修改好hosts文件之後,重新打開圖1對應的README頁面:

圖4

  O(∩_∩)O哈哈~,這時我們成功地加載出了原本裂掉的圖,但這種方式麻煩的地方在於當你配置好hosts之後的確是可以正常訪問github的,但一旦你某天訪問github發現老毛病又出現了,就得重複一遍上述的過程,接下來我們來學習另一種能將上述過程自動化的方法。

2.2 利用SwitchHosts軟件自動更新hosts信息

  SwitchHosts是一個用於快速切換hosts文件的開源軟件(https://github.com/oldj/SwitchHosts ),我們可以通過其官方提供的百度雲盤地址(https://pan.baidu.com/s/1inED1 )下載適合自己系統的版本。

  下載后直接正常安裝,接着以管理員身份打開,點擊左下角+新建hosts,再按照圖5配置好,設置自動刷新時間間隔為你覺得合適的,我選的1小時刷新一次,這樣每隔一小時SwitchHosts就會自動訪問URL並更新hosts信息:

圖5

  點擊刷新按鈕刷新成功后,點擊OK創建完成。其中URL信息是碼雲同步可正常訪問版本(https://gitee.com/xueweihan/codes/6g793pm2k1hacwfbyesl464/raw?blob_name=GitHub520.yml ),因為原始倉庫中的URL為github源會連接失敗。

  創建完成后,把開關打開,讓SwitchHosts在後台靜靜的運行即可:

圖6

  完成后,保持軟件後台運行即可,之後訪問Github同樣解決了問題。

  以上就是本文的全部內容,如有疑問歡迎在評論區與我討論。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

一文講透Java序列化

 

本文目錄

  • 一、序列化是什麼
  • 二、為什麼需要序列化
  • 三、序列化怎麼用
  • 四、序列化深度探秘
    • 4.1 為什麼必須實現Serializable接口
    • 4.2 被序列化對象的字段是引用時該怎麼辦 
    • 4.3 同一個對象會被序列化多次嗎
    • 4.4 只想序列化對象的部分字段該怎麼辦
    • 4.5 被序列化對象具有繼承關係該怎麼辦
  • 五、serialVersionUID的作用及自動生成
  • 六、序列化的缺點
  • 七、參考文獻

 

前言

 

Oracle 公司計劃廢除 Java 中的古董:序列化技術,因為它帶來了許多嚴重的安全問題(如序列化存儲安全、反序列化安全、傳輸安全等),據統計,至少有3分之1的漏洞是序列化帶來的,這也是 1997 年誕生序列化技術的一個巨大錯誤。但是,序列化技術現在在 Java 應用中無處不在,特別是現在的持久化框架和分佈式技術中,都需要利用序列化來傳輸對象,如:Hibernate、Mybatis、Java RMI、Dubbo等,即對象要存儲或者傳輸都不可避免要用到序列化技術,所以刪除序列化技術將是一個長期的計劃。

 

你在實際工作中可能會很難有機會真正用到Java自帶的序列化技術了,工業界一般也會選擇一些更安全的對象編解碼方案例如Google的Protobuf等。所以,對於Java序列化,我們不必再投入過多的精力學習,你花20分鐘讀完本文所掌握的知識,對於應付日常源碼閱讀中遇到的遺留的Java序列化技術應該是足夠了。

 

一、序列化是什麼

 

序列化機制允許將實現序列化的Java對象轉換成字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以備以後重新恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在。

  • 序列化:將一個Java對象寫入IO流中
  • 反序列化:從IO流中恢復該Java對象

 

本文中用序列化來簡稱整個序列化和反序列化機制。 

 

二、為什麼需要序列化

 

所有可能在網絡上傳輸的對象的類都應該是可序列化的,否則程序將會出現異常,比如RMI(Remote Method Invoke,即遠程方法調用,是JavaEE的基礎)過程中的參數和返回值;所有需要保存到磁盤裡的對象的類都必須可序列化,比如Web應用中需要保存到HttpSession或ServletContext屬性的Java對象。

 

因為序列化是RMI過程的參數和返回值都必須實現的機制,而RMI又是Java EE技術的基礎——所有的分佈式應用常常需要跨平台、跨網絡,所以要求所有傳遞的參數、返回值必須實現序列化。因此序列化機制是Java EE平台的基礎。通常建議:程序創建的每個JavaBean類都實現Serializable。

 

三、序列化怎麼用

 

如果一個類的對象需要序列化,那麼在Java語法層面,這個類需要:

  • 實現Serializable接口
  • 使用ObjectOutputStream將對象輸出到流,實現對象的序列化;使用ObjectInputStream從流中讀取對象,實現對象的反序列化

 

下面我們通過代碼示例來看看序列化最基本的用法。我們創建了Person類,其擁有兩個基本類型的屬性,並實現了Serializable接口。testSerialize方法用來測試序列化,testDeserialize方法用來測試反序列化。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 148.2);
10         Person two = new Person(35, 177.8);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable {
40     int age;
41     double height;
42 
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }

 

四、序列化深度探秘

4.1 為什麼必須實現Serializable接口

如果某個類需要支持序列化功能,那麼它必須實現Serializable接口,否則會報 java.io.NotSerializableException。Serializable接口是一個標誌性接口(Marker Interface),也就是說,該接口並不包含任何具體的方法,是一個空接口,僅僅用來判斷該類是否能夠序列化。JDK8中Serializable接口的源碼如下:

1 package java.io;
2 
3 public interface Serializable {
4 }

 

在 ObjectOutputStream.java 的 writeObject0 方法中,我們確實可以看到對對象是否實現了 Serializable接口進行了驗證(第15行),否則會拋出 NotSerializableException 異常(第22行)。

 1     private void writeObject0(Object obj, boolean unshared)
 2         throws IOException
 3     {
 4         boolean oldMode = bout.setBlockDataMode(false);
 5         depth++;
 6         try {
 7             ...
 8             // remaining cases
 9             if (obj instanceof String) {
10                 writeString((String) obj, unshared);
11             } else if (cl.isArray()) {
12                 writeArray(obj, desc, unshared);
13             } else if (obj instanceof Enum) {
14                 writeEnum((Enum<?>) obj, desc, unshared);
15             } else if (obj instanceof Serializable) {
16                 writeOrdinaryObject(obj, desc, unshared);
17             } else {
18                 if (extendedDebugInfo) {
19                     throw new NotSerializableException(
20                         cl.getName() + "\n" + debugInfoStack.toString());
21                 } else {
22                     throw new NotSerializableException(cl.getName());
23                 }
24             }
25         } finally {
26             depth--;
27             bout.setBlockDataMode(oldMode);
28         }
29     }

 

4.2 被序列化對象的字段是引用時該怎麼辦

在第三部分“序列化怎麼用”部分的示例中,Person類的字段全都是基本類型,我們知道基本類型其地址中直接存放的就是它的值,那如果是引用類型呢?引用類型其地址中存放的是指向堆內存中的一個地址,難道序列化時就是將這個地址進行了保存嗎?顯然,這是說不通的,因為對象的內存地址是可變的,在同一系統的不同運行時刻或者是不同系統中,對象的地址肯定是不同的,因此,序列化內存地址沒有意義。

 

如果被序列化對象的字段是引用,那麼要求該引用的類型也是可序列化實現了Serializable接口的,否則無法序列化。當對某個對象進行序列化時,系統會自動把該對象的所有Field依次進行序列化,如果某個Field引用到另一個對象,則被引用的對象也會被序列化;如果被引用的對象的Field也引用了其他對象,則被引用的對象也會被序列化,這種情況被稱為遞歸序列化。

 

4.3 同一個對象會被序列化多次嗎

如果對象A和對象B同時引用了對象C,那麼,當序列化對象A和對象B時,對象C會被序列化兩次嗎?答案顯然是不會

 

要解釋這個問題,就不得不說一下Java序列化的基本算法了:

  • 所有序列化到二進制流的對象都有一個序列化編號
  • 當程序試圖序列化一個對象時,程序將先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬機中)被序列化過,系統才會將該對象轉換成字節序列並賦予一個唯一的編號
  • 如果某個對象已經序列化過,程序將只是直接輸出其序列化編號,而不是再次重新序列化該對象

 

4.4 只想序列化對象的部分字段該怎麼辦

 在一些特殊的場景下,如果一個類里包含的某些Field值是敏感信息,例如銀行賬戶信息等,這時不希望系統將該Field值進行序列化;或者某個Field的類型是不可序列化的,因此不希望對該Field進行遞歸序列化,以避免引發java.io.NotSerializableException異常。

 

此時,我們就需要自定義序列化了。自定義序列化的常用方式有兩種:

  • 使用transient關鍵字
  • 重寫writeObject與readObject方法

 

我們先看第一種方式,使用transient關鍵字。transient關鍵字只能用於修飾Field,不可修飾Java程序中的其他成分。使用transient修飾的屬性,java序列化時,會忽略掉此字段,所以反序列化出的對象,被transient修飾的屬性是默認值。對於引用類型,值是null;基本類型,值是0;boolean類型,值是false。

 

下列代碼中,我們把People的height字段設置為transient,在反序列化時,可觀察到輸出為默認值0.0。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected transient double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }

 

 程序輸出:

Person{age=12, height=0.0}
Person{age=16, height=0.0}

Process finished with exit code 0

 

使用transient關鍵字修飾Field雖然簡單、方便,但被transient修飾的Field將被完全隔離在序列化機制之外,這樣導致在反序列化恢復Java對象時無法取得該Field值。Java還提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程序控制如何序列化各Field,甚至完全不序列化某些Field(與使用transient關鍵字的效果相同)。在序列化和反序列化過程中需要特殊處理的類應該提供如下特殊簽名的方法,這些特殊的方法用以實現自定義序列化。

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

 

  • writeObject()方法負責寫入特定類的實例狀態,以便相應的readObject()方法可以恢復它。通過重寫該方法,程序員可以完全獲得對序列化機制的控制,可以自主決定哪些Field需要序列化,需要怎樣序列化。在默認情況下,該方法會調用out.defaultWriteObject來保存Java對象的各Field,從而可以實現序列化Java對象狀態的目的。
  • readObject()方法負責從流中讀取並恢復對象Field,通過重寫該方法,程序員可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化哪些Field,以及如何進行反序列化。在默認情況下,該方法會調用in.defaultReadObject來恢復Java對象的非靜態和非瞬態Field。在通常情況下,readObject()方法與writeObject()方法對應,如果writeObject()方法中對Java對象的Field進行了一些處理,則應該在readObject()方法中對其Field進行相應的反處理,以便正確恢復該對象。
  • 當序列化流不完整時,readObjectNoData()方法可以用來正確地初始化反序列化的對象。例如,接收方使用的反序列化類的版本不同於發送方,或者接收方版本擴展的類不是發送方版本擴展的類,或者序列化流被篡改時,系統都會調用readObjectNoData()方法來初始化反序列化的對象。

下面的示例代碼中,我們在writeObject方法中對Person的字段進行了簡單的加密處理,在readObject方法中對其進行了相應的解密。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     private void writeObject(java.io.ObjectOutputStream out)
52             throws IOException {
53         System.out.println("Encryption!");
54         out.writeInt(age + 1);
55         out.writeDouble(height - 1);
56     }
57     private void readObject(java.io.ObjectInputStream in)
58             throws IOException, ClassNotFoundException {
59         System.out.println("Decryption!");
60         this.age = in.readInt() - 1;
61         this.height = in.readDouble() + 1;
62     }
63 
64     @Override
65     public String toString() {
66         return "Person{" +
67                 "age=" + age +
68                 ", height=" + height +
69                 '}';
70     }
71 }

 

4.5 被序列化對象具有繼承關係該怎麼辦

被序列化對象具有繼承關係時無非就兩種情況,第一,該類具有子類,第二,該類具有父類。

 

當該類實現了Serializable接口且具有子類時,根據官方文檔中的說明,其子類天然具有可被序列化的屬性,不需要顯式實現Serializable接口;。

 All subtypes of a serializable class are themselves serializable. 

 

當該類實現了Serializable接口且具有父類時,,該類的父類需要實現Serializable接口嗎?在JDK8中Serializable接口的官方文檔中有這樣一段話:

 1 /**
 2  * ......
 3  *
 4  * To allow subtypes of non-serializable classes to be serialized, the
 5  * subtype may assume responsibility for saving and restoring the
 6  * state of the supertype's public, protected, and (if accessible)
 7  * package fields.  The subtype may assume this responsibility only if
 8  * the class it extends has an accessible no-arg constructor to
 9  * initialize the class's state.  It is an error to declare a class
10  * Serializable if this is not the case.  The error will be detected at
11  * runtime. 
12  *
13  * During deserialization, the fields of non-serializable classes will
14  * be initialized using the public or protected no-arg constructor of
15  * the class.  A no-arg constructor must be accessible to the subclass
16  * that is serializable.  The fields of serializable subclasses will
17  * be restored from the stream. 
18  */

 

閱讀文檔我們得知,為了使得不可序列化類的子類能夠序列化,其子類必須擔負起保存和恢復其超類的public、protected 和 package(if accessible)實例域的責任,且要求其父類必須有一個可訪問的無參構造函數以使得在反序列化時能夠初始化實例域。

 

我們寫代碼驗證一下,如果父類中沒有可訪問的無參構造函數會發生什麼,注意Person類中沒有無參構造函數。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Student.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Student.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42     
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }
56 
57 class Student extends Person implements Serializable{
58     private String id;
59 
60     public Student(int age, double height, String id) {
61         super(age, height);
62         this.id = id;
63     }
64 
65     @Override
66     public String toString() {
67         return "Student{" +
68                 "age=" + age +
69                 ", height=" + height +
70                 ", id='" + id + '\'' +
71                 '}';
72     }
73 }

 

程序輸出產生異常:

java.io.InvalidClassException: Student; no valid constructor
    at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)
    at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at SerializableTest.testDeserialize(SerializableTest.java:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    ...

Process finished with exit code 0

 

當我們為Person類添加默認構造函數時:

 1 class Person{
 2     protected int age;
 3     protected double height;
 4 
 5     public Person() {
 6     }
 7 
 8     public Person(int age, double height) {
 9         this.age = age;
10         this.height = height;
11     }
12 
13     @Override
14     public String toString() {
15         return "Person{" +
16                 "age=" + age +
17                 ", height=" + height +
18                 '}';
19     }
20 }

 

程序輸出如下,我們可觀察到,父類中的字段都是默認值,只有子類中的字段得到了正確的序列化。出現這種情況的原因是子類並沒有擔負起序列化父類中字段的責任。

Student{age=0, height=0.0, id='1234'}
Student{age=0, height=0.0, id='5678'}

Process finished with exit code 0

 

為了解決上述問題,我們需要藉助上一節中學到的知識,使用自定義的序列化方法writeObject和readObject來主動將父類中的字段進行序列化。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Studnet.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Studnet.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }
59 
60 class Student extends Person implements Serializable{
61     private String id;
62 
63     public Student(int age, double height, String id) {
64         super(age, height);
65         this.id = id;
66     }
67 
68     private void writeObject(java.io.ObjectOutputStream out)
69             throws IOException {
70         out.defaultWriteObject();
71         out.writeInt(age);
72         out.writeDouble(height);
73     }
74     
75     private void readObject(java.io.ObjectInputStream in)
76             throws IOException, ClassNotFoundException {
77         in.defaultReadObject();
78         this.age = in.readInt();
79         this.height = in.readDouble();
80     }
81 
82     @Override
83     public String toString() {
84         return "Student{" +
85                 "age=" + age +
86                 ", height=" + height +
87                 ", id='" + id + '\'' +
88                 '}';
89     }
90 }

 

程序輸出如下,可以看到完全正確。

Student{age=12, height=156.6, id='1234'}
Student{age=16, height=177.7, id='5678'}

Process finished with exit code 0

 

五、serialVersionUID的作用及自動生成

 

我們知道,反序列化必須擁有class文件,但隨着項目的升級,class文件也會升級,序列化怎麼保證升級前後的兼容性呢?

 

java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,對象也可以正確被反序列化回來。如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。下面是JDK 8中ArrayList的源碼中的serialVersionUID。

 

 1 public class ArrayList<E> extends AbstractList<E>
 2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
 3 {
 4     private static final long serialVersionUID = 8683452581122892189L;
 5 
 6     /**
 7      * Default initial capacity.
 8      */
 9     private static final int DEFAULT_CAPACITY = 10;
10     ...  
11 }

 

序列化版本號可自由指定,如果不指定,JVM會根據類信息自己計算一個版本號,這樣隨着class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class文件沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。

 

什麼情況下需要修改serialVersionUID呢?分三種情況。

  • 如果只是修改了方法,反序列化不容影響,則無需修改版本號
  • 如果只是修改了靜態Field或瞬態Field,則反序列化不受任何影響
  •  如果修改類時修改了非靜態Field、非瞬態Field,則可能導致序列化版本不兼容。如果對象流中的對象和新類中包含同名的Field,而Field類型不同,則反序列化失敗,類定義應該更新serialVersionUID Field值。如果只是新增了實例變量,則反序列化回來新增的是默認值;如果減少了實例變量,反序列化時會忽略掉減少的實例變量。

 

我們在日常編程實踐中,一般會選擇使用IDE來自動生成serialVersionUID,這樣可以最大化地減少重複的可能性。對於IntelliJ IDEA,自動生成serialVersionUID有三步:

  • 修改IDEA配置:File->Setting->Editor->Inspections->Serialization issues->Serializable class without ’serialVersionUID’
  • 類實現Serializable接口
  • 在類名上執行Alt+Enter,然後選擇生成serialVersionUID即可

 

六、序列化的缺點

 

Java序列化存在四個致命缺點,導致其不適用於網絡傳輸:

  • 無法跨語言:在網絡傳輸中,經常會有異構語言的進程的交互,但Java序列化技術是Java語言內部的私有協議,其他語言無法進行反序列化。目前所有流行的RPC框架都沒有使用Java序列化作為編解碼框架。
  • 潛在風險高:不可信流的反序列化可能導致遠程代碼執行(RCE)、拒絕服務(DoS)和一系列其他攻擊。
  • 序列化后的碼流太大
  • 序列化的性能較低

 

在真正的生產環境中,一般會選擇其它編解碼框架,領先的跨平台結構化數據表示是 JSON 和 Protocol Buffers,也稱為 protobuf。JSON 由 Douglas Crockford 設計用於瀏覽器與服務器通信,Protocol Buffers 由谷歌設計用於在其服務器之間存儲和交換結構化數據。JSON 和 protobuf 之間最顯著的區別是 JSON 是基於文本的,並且是人類可讀的,而 protobuf 是二進制的,但效率更高。

 

 七、參考文獻

 

  1. 《瘋狂Java講義》第2版,李剛著,电子工業出版社
  2. 《Java核心技術》第10版,霍斯特曼等著,机械工業出版本
  3. 《Netty權威指南》第2版,李林鋒著,电子工業出版社
  4. 《Effective Java》第2版,Joshua Bloch著,机械工業出版社

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

我們是如何做DevOps的?

一、DevOps的理解

DevOps的概念理解

DevOps 的概念在軟件開發行業中逐漸流行起來。越來越多的團隊希望實現產品的敏捷開發,DevOps 使一切成為可能。有了 DevOps ,團隊可以定期發布代碼、自動化部署、並將持續集成 / 持續交付作為發布過程的一部分。
一句話概括就是提高生產力,快速交付!

二、引入DevOps的背景

2.1 福祿技術棧介紹

  • 後端開發框架:基於C#的.netCore和Java的SpringCloud,少部分項目採用python和go開發

  • 前端開發框架:vue、react

  • 服務部署:前端站點基於ECS的nginx部署 ,後端服務統一部署在kubernetes上

  • 代碼倉庫:gitlab

  • 項目環境:目前有6套,開發、測試、壓測、集成、PRE和生產

2.2 後端服務的CICD現狀

                                                                                                 福祿後端CICD流程

CICD 流程說明

每一次的代碼push,根據創建的分支,根據在gitlab的CICD文件gitlab.yml定義構建步驟,觸發runner,從單元測試、通過dockerfile進行編譯和生成鏡像版本、將新鏡像部署到K8S生成pod,然後觸發接口自動化測試任務的執行

!!#00ffff 好像缺了點什麼 !!

  • 初次部署應用到kubernetes怎麼做的?

  • 服務的configmap在哪裡維護的?

  • 每個服務的gitlab.yml文件都不一樣,如何維護的?

  • 應用的域名解析怎麼做?

目前有6套環境進行管理,其中開發、測試、集成、壓測都是測試人員維護,預發布和生產運維人員維護;這也就要求每一個測試人員都必須對整個cicd流程和配置絕對掌握;所以當新人入職,需要掌握整個流程才能進入項目測試中,這是一個學習成本;

預發布和生產的kubernetes只有運維能夠操作,當有新的服務需要上線上述環境,或者configmap有變動,或者有時候排查問題需要查看容器日誌,我們只能通過運維的工單系統描述作業操作,中間文字描述可能存在理解差異,溝通成本和時間成本很大;

有的新應用我們去設置cicd的相關文件,比如dockerfile,我們發現應用的代碼目錄結構各種各樣,這樣往往就沒法套用一個模板快速配置完成

2.3 前端站點的CICD現狀

前端CICD流程說明

開發人員push代碼到gitlab,測試人員通過jenkins拉取最新的代碼到jenkins本地,然後通過jenkins與服務器之間的傳輸管道,將要部署的文件更新到目標服務器,並觸發UI自動化的job

完整的過程來看,也缺點內容

  • 一個新的站點部署,nginx需要做一些配置初始化工作,比如域名、路徑的配置
  • 前端的配置文件是如何管理的

跟後端應用一樣,前端的PRE和生產環境也是運維處理,所以當一個新的應用上線我們也需要發工單,描述具體操作,然後運維執行工單;配置文件一般不會變更,所以我們在jenkins推送更新文件到目標服務器的時候,將配置文件做了過濾處理。後續需要變更通過工單執行

2.3 痛點你看到了嗎

2.3.1 安全管控缺位
  • 代碼安全:CICD的起點在gitlab裏面,所以大家都有gitlab的賬號,代碼安全管控缺位
  • 線上安全:線上項目部署也是通過gitlab的cicd直接觸發,審批流程缺失
2.3.2 管理成本
  • 維護賬號多:gitlab賬號、jenkins賬號、kubernetes賬號(本地和阿里雲),每一個人員都需要上述賬號,運維管理麻煩,大家每個平台維護自己的賬號也麻煩
  • 工單溝通:工單編寫、溝通過程花費時間較多
  • 代碼規範:項目組多,微服務也多,代碼框架各自發揮,無論是流程維護還是問題排查都增加了難度

三、研發管理平台(RDMS)應運而生

3.1 如何理解這個平台

!!#ff0000 工具鏈到平台的轉變 !!

當前的cicd是對工具鏈進行了打通,但需要大家登錄各個工具平台操作,我們希望對工具集進行功能整合,打造一個系統平台,並且將CICD的技術細節進行屏蔽,開發人員能夠專心進行業務需求的開發,測試人員能夠專註到需求測試任務中,而運維人員能夠解放繁重的工單內容,投入到服務高可用的建設上!

3.2 業務功能設計

                                                                                                                                  福祿研發管理平台功能結構圖

3.2.1 功能說明
  • 項目管理:項目的創建和維護,默認提供了.netcore的api和控制台,java的api和前端站點的應用初始化代碼框架,開發人員開發新的應用直接根據應用類型選擇對應的模板就可以在git默認創建代碼倉庫和初始化框架代碼,並自動生成應用的http和https的域名
  • 構建記錄:獲取gitlab的pipeline,展示所有分支的構建記錄信息,可以一鍵跳轉到git倉庫
  • 部署管理:部署構建的鏡像到指定的環境,提供實時部署和定時部署功能
  • 容器管理:提供容器的查看功能,可以看到容器的存活狀態和容器實時日誌
  • 配置字段權限申請:針對PRE和生產環境查看配置,需要先走釘釘審批申請流程
  • 配置信息:進行配置的維護,包括新增、編輯、刪除,PRE和生產環境操作需要釘釘流程審批
  • 操作日誌:針對應用的操作日誌記錄
  • 用戶設置:在使用rdms前,需要先將用戶git倉庫的token設置在rdms上,這樣用戶在rdms操作與gitlab相關的業務才能正常使用
3.2.2 RDMS幾個核心頁面的展示

首頁-創建應用

構建記錄

部署管理

容器管理

3.3 技術架構

對接系統的說明

  • 通行證:RDMS的目標用戶是研發中心人員,這些人員在通行證中都有默認的賬戶信息,與通行證打通,可以直接登錄使用
  • GitlabAPI:目前RDMS的CI還是採用的gitlab的ci支撐,包括新應用在rdms的創建到git倉庫的代碼初始化等,都需要調用gitlab的api接口
  • 釘釘flow:安全管控的原因,PRE和生產的任何操作都會觸發釘釘審批流,所屬項目的項目經理審批通過後才會獲取到數據或者執行操作指令
  • 福祿開放平台:提供了網關相關的功能和菜單、角色等維護功能,公司所有後端服務都需要入駐開放平台
  • 蜂巢:公司的調度作業平台,rdms的定時部署功能依賴該服務的支撐
  • 運維工單系統:rdms的CD流程沒有直接與kubernets進行交互,而是通過運維的工單系統包裝了運維底層的shell腳本層,然後提供給rdms相關的api接口,也是基於安全控制的考慮
  • shell腳本層:shell腳本層會調用kubernetes的api進行kubernetes的相關操作(部署、配置更新、容器重啟、日誌查看等);調用阿里雲的dns解析接口,對應用的域名自動解析;調用oss的接口,進行前端站點文件目錄的維護

3.4 後端應用的devops實現詳解

舉個栗子進行介紹

根據模板,創建一個應用

根據名稱默認生成域名

初始化代碼倉庫,默認生成develop分支

在rdms第一次部署到對應環境(開發、測試、生產等)時,會默認讀取appsettings.Development.json的文件,並寫入kubernets的configmap

構建完成,進行部署

在kubernets生成pod

通過域名訪問接口文檔

3.5 前端站點的devops實現詳解

同樣的,舉個栗子介紹

首頁-創建前端站點

根據名稱生成域名

初始化代碼倉庫,默認生成develop分支

配置文件,默認生成幾套環境的配置文件,站點的配置維護就是維護這幾個文件

部署應用

kubernetes的nginx容器內可以看到部署的文件,實際就是掛載的oss到該pod上

四、展望

目前RDMS投產一個月左右,我們希望能將devops理念在這個系統上進行持續的優化和實踐,包括研發中心小夥伴也很踴躍參与共建,提出了很多好的方向和建議

  • 完善devops鏈條功能:通過字面來看有dev、ops兩部分,我們後期需要加入test的比重,比如在CI部分,引入靜態代碼掃描、單測覆蓋率;在CD部分集成我們的自動化測、性能測試
  • 工具平台:RDMS的初衷是整合,針對研發中心經常使用的工具或者有相關工具化的需求,我們可以整合到rdms或者在RDMS上進行開發

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

Centos7快速安裝RocketMQ

1. 為什麼要用MQ

消息隊列是一種“先進先出”的數據結構

其應用場景主要包含以下3個方面

  • 應用解耦

系統的耦合性越高,容錯性就越低。以電商應用為例,用戶創建訂單后,如果耦合調用庫存系統、物流系統、支付系統,任何一個子系統出了故障或者因為升級等原因暫時不可用,都會造成下單操作異常,影響用戶使用體驗。

使用消息隊列解耦合,系統的耦合性就會提高了。比如物流系統發生故障,需要幾分鐘才能來修復,在這段時間內,物流系統要處理的數據被緩存到消息隊列中,用戶的下單操作正常完成。當物流系統回復后,補充處理存在消息隊列中的訂單消息即可,終端系統感知不到物流系統發生過幾分鐘故障。

  • 流量削峰

應用系統如果遇到系統請求流量的瞬間猛增,有可能會將系統壓垮。有了消息隊列可以將大量請求緩存起來,分散到很長一段時間處理,這樣可以大大提到系統的穩定性和用戶體驗。

一般情況,為了保證系統的穩定性,如果系統負載超過閾值,就會阻止用戶請求,這會影響用戶體驗,而如果使用消息隊列將請求緩存起來,等待系統處理完畢后通知用戶下單完畢,這樣總不能下單體驗要好。

處於經濟考量目的:

業務系統正常時段的QPS如果是1000,流量最高峰是10000,為了應對流量高峰配置高性能的服務器顯然不划算,這時可以使用消息隊列對峰值流量削峰

  • 數據分發

通過消息隊列可以讓數據在多個系統更加之間進行流通。數據的產生方不需要關心誰來使用數據,只需要將數據發送到消息隊列,數據使用方直接在消息隊列中直接獲取數據即可

2. MQ的優點和缺點

優點:解耦、削峰、數據分發

缺點包含以下幾點:

  • 系統可用性降低

    系統引入的外部依賴越多,系統穩定性越差。一旦MQ宕機,就會對業務造成影響。

    如何保證MQ的高可用?

  • 系統複雜度提高

    MQ的加入大大增加了系統的複雜度,以前系統間是同步的遠程調用,現在是通過MQ進行異步調用。

    如何保證消息沒有被重複消費?怎麼處理消息丟失情況?那麼保證消息傳遞的順序性?

  • 一致性問題

    A系統處理完業務,通過MQ給B、C、D三個系統發消息數據,如果B系統、C系統處理成功,D系統處理失敗。

    如何保證消息數據處理的一致性?

3. 各種MQ產品的比較

常見的MQ產品包括KafkaActiveMQRabbitMQRocketMQ

4. 安裝RocketMQ

RocketMQ是阿里巴巴2016年MQ中間件,使用Java語言開發,在阿里內部,RocketMQ承接了例如“雙11”等高併發場景的消息流轉,能夠處理萬億級別的消息。

4.1 下載RocketMQ

RocketMQ最新版本:4.5.1

下載地址

4.2 環境要求

  • Linux64位系統

  • JDK1.8(64位)

yum install java-1.8.0-openjdk* -y

  • 源碼安裝需要安裝Maven 3.2.x

4.3 二進制包方式安裝RocketMQ

本教程以二進制包方式安裝

  1. 解壓安裝包
  2. 進入安裝目錄

目錄介紹

  • bin:啟動腳本,包括shell腳本和CMD腳本
  • conf:實例配置文件 ,包括broker配置文件、logback配置文件等
  • lib:依賴jar包,包括Nettycommons-langFastJSON

4.4 啟動RocketMQ

  1. 啟動NameServer
# 1.啟動NameServer
nohup sh bin/mqnamesrv &
# 2.查看啟動日誌
tail -f ~/logs/rocketmqlogs/namesrv.log

  1. 啟動Broker
# 1.啟動Broker
nohup sh bin/mqbroker -n localhost:9876 &
# 2.查看啟動日誌
tail -f ~/logs/rocketmqlogs/broker.log 

  • 問題描述:

    RocketMQ默認的虛擬機內存較大,啟動Broker如果因為內存不足失敗,需要編輯如下兩個配置文件,修改JVM內存大小

# 編輯runbroker.sh和runserver.sh修改默認JVM大小
vi runbroker.sh
vi runserver.sh
  • 參考設置:
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m  -XX:MaxMetaspaceSize=320m"

5 測試RocketMQ

5.1 發送消息

# 1.設置環境變量
export NAMESRV_ADDR=localhost:9876
# 2.使用安裝包的Demo發送消息
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer

5.2 接收消息

# 1.設置環境變量
export NAMESRV_ADDR=localhost:9876
# 2.接收消息
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

6. 關閉RocketMQ

# 1.關閉NameServer
sh bin/mqshutdown namesrv
# 2.關閉Broker
sh bin/mqshutdown broker

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

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