java8 函數式接口——Function/Predict/Supplier/Consumer

Function

我們知道Java8的最大特性就是函數式接口。所有標註了@FunctionalInterface註解的接口都是函數式接口,具體來說,所有標註了該註解的接口都將能用在lambda表達式上。

接口介紹

/**
 * Represents a function that accepts one argument and produces a result.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object)}.
 *
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 *
 * @since 1.8
 */

上述描述可知: Function中傳遞的兩個泛型:T,R分別代表 輸入參數類型和返回參數類型。下面將逐個介紹Function中的各個接口:

接口1: 執行具體內容接口
R apply(T t);

實例1:apply使用
    // 匿名類的方式實現
    Function<Integer, Integer> version1 = new Function<Integer, Integer>() {
        @Override
        public Integer apply(Integer integer) {
            return integer++;
        }
    };
    int result1 = version1.apply(20);


    // lamda表達式
    Function<Integer, Integer> version2 = integer -> integer++;
    int result2 = version1.apply(20);
    

接口2: compose
該方法是一個默認方法,這個方法接收一個function作為參數,將參數function執行的結果作為參數給調用的function,以此來實現兩個function組合的功能。

// compose 方法源碼
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
實例2:compose使用
public int compute(int a, Function<Integer, Integer> function1, Function<Integer, Integer> function2) {
    return function1.compose(function2).apply(a);
}

// 調用上述方法
test.compute(2, value -> value * 3, value -> value * value) 
// 執行結果: 12 (有源碼可以看出先執行before)

接口3 : andThen
了解了compose方法,我們再來看andThen方法就好理解了,聽名字就是“接下來”,andThen方法也是接收一個function作為參數,與compse不同的是,先執行本身的apply方法,將執行的結果作為參數給參數中的function。

public interface Function<T, R> {
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
}
實例3:andThen使用
public int compute2(int a, Function<Integer, Integer> function1, Function<Integer, Integer> function2) {
    return function1.andThen(function2).apply(a);
}

// 調用上述方法
test.compute2(2, value -> value * 3, value -> value * value) 
// 執行結果 : 36

反思: 多個參數

Function接口雖然很簡潔,但是由Function源碼可以看出,他只能傳一個參數,實際使用中肯定不能滿足需求。下面提供幾種思路:

  1. BiFunction可以傳遞兩個參數(Java8中還提供了其它相似Function)
  2. 通過封裝類來解決
  3. void函數還是無法解決

因為參數原因“自帶的Function”函數必然不能滿足業務上複雜多變的需求,那麼就自定義Function接口吧

@FunctionalInterface
    static interface ThiConsumer<T,U,W>{
        void accept(T t, U u, W w);

        default ThiConsumer<T,U,W> andThen(ThiConsumer<? super T,? super U,? super W> consumer){
            return (t, u, w)->{
                accept(t, u, w);
                consumer.accept(t, u, w);
            };
        }
    }

自此,Function接口介紹完畢。

斷言性接口:Predicate

接口介紹:

/**
 * Represents a predicate (boolean-valued function) of one argument.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #test(Object)}.
 *
 * @param <T> the type of the input to the predicate
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Predicate<T> {

Predicate是個斷言式接口其參數是<T,boolean>,也就是給一個參數T,返回boolean類型的結果。跟Function一樣,Predicate的具體實現也是根據傳入的lambda表達式來決定的。

源碼不再具體分析,主要有 test/and/or/negate方法,以及一個靜態方法isEqual,具體使用實例如下:

    private static void testPredict() {
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
        List<Integer> list = new ArrayList<>();
        for (int i : numbers) {
            list.add(i);
        }
        
        // 三個判斷
        Predicate<Integer> p1 = i -> i > 5;
        Predicate<Integer> p2 = i -> i < 20;
        Predicate<Integer> p3 = i -> i % 2 == 0;
        List test = list.stream()
                .filter(p1
                        .and(p2)
//                        .and(Predicate.isEqual(8))
                        .and(p3))
                .collect(Collectors.toList());
        System.out.println(test.toString());
        //print:[6, 8, 10, 12, 14]
    }

供給性接口:Supplier

接口介紹

/**
 * Represents a supplier of results.
 *
 * <p>There is no requirement that a new or distinct result be returned each
 * time the supplier is invoked.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #get()}.
 *
 * @param <T> the type of results supplied by this supplier
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Supplier<T> 

使用實例:

        Supplier supplier = "Hello"::toLowerCase;
        System.out.println(supplier);

消費性:Consumer

接口介紹

/**
 * Represents an operation that accepts a single input argument and returns no
 * result. Unlike most other functional interfaces, {@code Consumer} is expected
 * to operate via side-effects.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #accept(Object)}.
 *
 * @param <T> the type of the input to the operation
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Consumer<T> {

實際使用

    NameInfo info = new NameInfo("abc", 123);
    Consumer<NameInfo> consumer = t -> {
        String infoString = t.name + t.age;
        System.out.println("consumer process:" + infoString);
    };
    consumer.accept(info);

總結:
本文主要介紹Java8的接口式編程,以及jdk中提供的四種函數接口(FunctionalInterface)。Predict/Supplier/Consumer其實是Function的一種變形,所以沒有詳細介紹。
疑問: FunctionalInterface註解是如何和lamada表達式聯繫在一起,函數接口在編譯時又是如何處理的?後面再了解下

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

百萬級日活 App 的屏幕錄製功能是如何實現的

Android 從 4.0 開始就提供了手機錄屏方法,但是需要 root 權限,比較麻煩不容易實現。但是從 5.0 開始,系統提供給了 App 錄製屏幕的一系列方法,不需要 root 權限,只需要用戶授權即可錄屏,相對來說較為簡單。

基本上根據 官方文檔 便可以寫出錄屏的相關代碼。

屏幕錄製的基本實現步驟

在 Manifest 中申明權限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
獲取 MediaProjectionManager 並申請權限
private val mediaProjectionManager by lazy { activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager }
private var mediaProjection: MediaProjection? = null
if (mediaProjectionManager == null) {
    Log.d(TAG, "mediaProjectionManager == null,當前手機暫不支持錄屏")
    showToast(R.string.phone_not_support_screen_record)
    return
}
// 申請相關權限
PermissionUtils.permission(PermissionConstants.STORAGE, PermissionConstants.MICROPHONE)
        .callback(object : PermissionUtils.SimpleCallback {
            override fun onGranted() {
                Log.d(TAG, "start record")
                mediaProjectionManager?.apply {
                    // 申請相關權限成功后,要向用戶申請錄屏對話框
                    val intent = this.createScreenCaptureIntent()
                    if (activity.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
                        activity.startActivityForResult(intent, REQUEST_CODE)
                    } else {
                        showToast(R.string.phone_not_support_screen_record)
                    }
                }
            }
            override fun onDenied() {
                showToast(R.string.permission_denied)
            }
        })
        .request()
重寫 onActivityResult() 對用戶授權進行處理
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            mediaProjection = mediaProjectionManager!!.getMediaProjection(resultCode, data)
            // 實測,部分手機上錄製視頻的時候會有彈窗的出現,所以我們需要做一個 150ms 的延遲
            Handler().postDelayed({
                if (initRecorder()) {
                    mediaRecorder?.start()
                } else {
                    showToast(R.string.phone_not_support_screen_record)
                }
            }, 150)
        } else {
            showToast(R.string.phone_not_support_screen_record)
        }
    }
}

private fun initRecorder(): Boolean {
    Log.d(TAG, "initRecorder")
    var result = true
    // 創建文件夾
    val f = File(savePath)
    if (!f.exists()) {
        f.mkdirs()
    }
    // 錄屏保存的文件
    saveFile = File(savePath, "$saveName.tmp")
    saveFile?.apply {
        if (exists()) {
            delete()
        }
    }
    mediaRecorder = MediaRecorder()
    val width = Math.min(displayMetrics.widthPixels, 1080)
    val height = Math.min(displayMetrics.heightPixels, 1920)
    mediaRecorder?.apply {
        // 可以設置是否錄製音頻
        if (recordAudio) {
            setAudioSource(MediaRecorder.AudioSource.MIC)
        }
        setVideoSource(MediaRecorder.VideoSource.SURFACE)
        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setVideoEncoder(MediaRecorder.VideoEncoder.H264)
        if (recordAudio){
            setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        }
        setOutputFile(saveFile!!.absolutePath)
        setVideoSize(width, height)
        setVideoEncodingBitRate(8388608)
        setVideoFrameRate(VIDEO_FRAME_RATE)
        try {
            prepare()
            virtualDisplay = mediaProjection?.createVirtualDisplay("MainScreen", width, height, displayMetrics.densityDpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null)
            Log.d(TAG, "initRecorder 成功")
        } catch (e: Exception) {
            Log.e(TAG, "IllegalStateException preparing MediaRecorder: ${e.message}")
            e.printStackTrace()
            result = false
        }
    }
    return result
}   

上面可以看到,我們可以設置一系列參數,各種參數的意思就希望大家自己去觀摩官方文檔了。其中有一個比較重要的一點是我們通過 MediaProjectionManager 創建了一個 VirtualDisplay,這個 VirtualDisplay 可以理解為虛擬的呈現器,它可以捕獲屏幕上的內容,並將其捕獲的內容渲染到 Surface 上,MediaRecorder 再進一步把其封裝為 mp4 文件保存。

錄製完畢,調用 stop 方法保存數據

private fun stop() {
    if (isRecording) {
        isRecording = false
        try {
            mediaRecorder?.apply {
                setOnErrorListener(null)
                setOnInfoListener(null)
                setPreviewDisplay(null)
                stop()
                Log.d(TAG, "stop success")
            }
        } catch (e: Exception) {
            Log.e(TAG, "stopRecorder() error!${e.message}")
        } finally {
            mediaRecorder?.reset()
            virtualDisplay?.release()
            mediaProjection?.stop()
            listener?.onEndRecord()
        }
    }
}

/**
 * if you has parameters, the recordAudio will be invalid
 */
fun stopRecord(videoDuration: Long = 0, audioDuration: Long = 0, afdd: AssetFileDescriptor? = null) {
    stop()
    if (audioDuration != 0L && afdd != null) {
        syntheticAudio(videoDuration, audioDuration, afdd)
    } else {
        // saveFile
        if (saveFile != null) {
            val newFile = File(savePath, "$saveName.mp4")
            // 錄製結束后修改後綴為 mp4
            saveFile!!.renameTo(newFile)
            // 刷新到相冊
            val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
            intent.data = Uri.fromFile(newFile)
            activity.sendBroadcast(intent)
            showToast(R.string.save_to_album_success)
        }
        saveFile = null
    }

}

我們必須來看看 MediaRecorderstop() 方法的註釋。

/**
 * Stops recording. Call this after start(). Once recording is stopped,
 * you will have to configure it again as if it has just been constructed.
 * Note that a RuntimeException is intentionally thrown to the
 * application, if no valid audio/video data has been received when stop()
 * is called. This happens if stop() is called immediately after
 * start(). The failure lets the application take action accordingly to
 * clean up the output file (delete the output file, for instance), since
 * the output file is not properly constructed when this happens.
 *
 * @throws IllegalStateException if it is called before start()
 */
public native void stop() throws IllegalStateException; 

根據官方文檔,stop() 如果在 prepare() 后立即調用會崩潰,但對其他情況下發生的錯誤卻沒有做過多提及,實際上,當你真正地使用 MediaRecorder 做屏幕錄製的時候,你會發現即使你沒有在 prepare() 后立即調用 stop(),也可能拋出 IllegalStateException 異常。所以,保險起見,我們最好是直接使用 try...catch... 語句塊進行包裹。

比如你 initRecorder 中某些參數設置有問題,也會出現 stop() 出錯,數據寫不進你的文件。

完畢后,釋放資源
fun clearAll() {
    mediaRecorder?.release()
    mediaRecorder = null
    virtualDisplay?.release()
    virtualDisplay = null
    mediaProjection?.stop()
    mediaProjection = null
}

無法繞過的環境聲音

上面基本對 Android 屏幕錄製做了簡單的代碼編寫,當然實際上,我們需要做的地方還不止上面這些,感興趣的可以移步到 ScreenRecordHelper 進行查看。

但這根本不是我們的重點,我們極其容易遇到這樣的情況,需要我們錄製音頻的時候錄製系統音量,但卻不允許我們把環境音量錄進去。

似乎我們前面初始化 MediaRecorder 的時候有個設置音頻源的地方,我們來看看這個 MediaRecorder.setAudioSource() 方法都支持設置哪些東西。

從官方文檔 可知,我們可以設置以下這些音頻源。由於官方註釋太多,這裏就簡單解釋一些我們支持的可以設置的音頻源。

//設定錄音來源於同方向的相機麥克風相同,若相機無內置相機或無法識別,則使用預設的麥克風
MediaRecorder.AudioSource.CAMCORDER 
//默認音頻源
MediaRecorder.AudioSource.DEFAULT  
//設定錄音來源為主麥克風
MediaRecorder.AudioSource.MIC
//設定錄音來源為語音撥出的語音與對方說話的聲音
MediaRecorder.AudioSource.VOICE_CALL
// 攝像頭旁邊的麥克風
MediaRecorder.AudioSource.VOICE_COMMUNICATION
//下行聲音
MediaRecorder.AudioSource.VOICE_DOWNLINK
//語音識別
MediaRecorder.AudioSource.VOICE_RECOGNITION
//上行聲音
MediaRecorder.AudioSource.VOICE_UPLINK

咋一看沒有我們想要的選項,實際上你逐個進行測試,你也會發現,確實如此。我們想要媒體播放的音樂,總是無法擺脫環境聲音的限制。

奇怪的是,我們使用華為部分手機的系統錄屏的時候,卻可以做到,這就感嘆於 ROM 的定製性更改的神奇,當然,千奇百怪的第三方 ROM 也一直讓我們 Android 適配困難重重。

曲線救國剝離環境聲音

既然我們通過調用系統的 API 始終無法實現我們的需求:錄製屏幕,並同時播放背景音樂,錄製好保存的視頻需要只有背景音樂而沒有環境音量,我們只好另闢蹊徑。

不難想到,我們完全可以在錄製視頻的時候不設置音頻源,這樣得到的視頻就是一個沒有任何聲音的視頻,如果此時我們再把音樂強行剪輯進去,這樣就可以完美解決用戶的需要了。

對於音視頻的混合編輯,想必大多數人都能想到的是大名鼎鼎的 FFmpeg ,但如果要自己去編譯優化得到一個穩定可使用的 FFmpge 庫的話,需要花上不少時間。更重要的是,我們為一個如此簡單的功能大大的增大我們 APK 的體積,那是萬萬不可的。所以我們需要把目光轉移到官方的 MediaExtractor 上。

從 官方文檔 來看,能夠支持到 m4a 和 aac 格式的音頻文件合成到視頻文件中,根據相關文檔我們就不難寫出這樣的代碼。

/**
 * https://stackoverflow.com/questions/31572067/android-how-to-mux-audio-file-and-video-file
 */
private fun syntheticAudio(audioDuration: Long, videoDuration: Long, afdd: AssetFileDescriptor) {
    Log.d(TAG, "start syntheticAudio")
    val newFile = File(savePath, "$saveName.mp4")
    if (newFile.exists()) {
        newFile.delete()
    }
    try {
        newFile.createNewFile()
        val videoExtractor = MediaExtractor()
        videoExtractor.setDataSource(saveFile!!.absolutePath)
        val audioExtractor = MediaExtractor()
        afdd.apply {
            audioExtractor.setDataSource(fileDescriptor, startOffset, length * videoDuration / audioDuration)
        }
        val muxer = MediaMuxer(newFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        videoExtractor.selectTrack(0)
        val videoFormat = videoExtractor.getTrackFormat(0)
        val videoTrack = muxer.addTrack(videoFormat)

        audioExtractor.selectTrack(0)
        val audioFormat = audioExtractor.getTrackFormat(0)
        val audioTrack = muxer.addTrack(audioFormat)

        var sawEOS = false
        var frameCount = 0
        val offset = 100
        val sampleSize = 1000 * 1024
        val videoBuf = ByteBuffer.allocate(sampleSize)
        val audioBuf = ByteBuffer.allocate(sampleSize)
        val videoBufferInfo = MediaCodec.BufferInfo()
        val audioBufferInfo = MediaCodec.BufferInfo()

        videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
        audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)

        muxer.start()

        // 每秒多少幀
        // 實測 OPPO R9em 垃圾手機,拿出來的沒有 MediaFormat.KEY_FRAME_RATE
        val frameRate = if (videoFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
            videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE)
        } else {
            31
        }
        // 得出平均每一幀間隔多少微妙
        val videoSampleTime = 1000 * 1000 / frameRate
        while (!sawEOS) {
            videoBufferInfo.offset = offset
            videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset)
            if (videoBufferInfo.size < 0) {
                sawEOS = true
                videoBufferInfo.size = 0
            } else {
                videoBufferInfo.presentationTimeUs += videoSampleTime
                videoBufferInfo.flags = videoExtractor.sampleFlags
                muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo)
                videoExtractor.advance()
                frameCount++
            }
        }
        var sawEOS2 = false
        var frameCount2 = 0
        while (!sawEOS2) {
            frameCount2++
            audioBufferInfo.offset = offset
            audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset)

            if (audioBufferInfo.size < 0) {
                sawEOS2 = true
                audioBufferInfo.size = 0
            } else {
                audioBufferInfo.presentationTimeUs = audioExtractor.sampleTime
                audioBufferInfo.flags = audioExtractor.sampleFlags
                muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo)
                audioExtractor.advance()
            }
        }
        muxer.stop()
        muxer.release()
        videoExtractor.release()
        audioExtractor.release()

        // 刪除無聲視頻文件
        saveFile?.delete()
    } catch (e: Exception) {
        Log.e(TAG, "Mixer Error:${e.message}")
        // 視頻添加音頻合成失敗,直接保存視頻
        saveFile?.renameTo(newFile)

    } finally {
        afdd.close()
        Handler().post {
            refreshVideo(newFile)
            saveFile = null
        }
    }
}

於是成就了錄屏幫助類 ScreenRecordHelper

經過各種兼容性測試,目前在 DAU 超過 100 萬的 APP 中穩定運行了兩個版本,於是抽出了一個工具類庫分享給大家,使用非常簡單,代碼註釋比較全面,感興趣的可以直接點擊鏈接進行訪問:https://github.com/nanchen2251/ScreenRecordHelper

使用就非常簡單了,直接把 [README] (https://github.com/nanchen2251/ScreenRecordHelper/blob/master/README.md) 貼過來吧。

Step 1. Add it in your root build.gradle at the end of repositories:
allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}       
Step 2. Add the dependency
dependencies {
    implementation 'com.github.nanchen2251:ScreenRecordHelper:1.0.2'
}
Step 3. Just use it in your project
// start screen record
if (screenRecordHelper == null) {
    screenRecordHelper = ScreenRecordHelper(this, null, PathUtils.getExternalStoragePath() + "/nanchen")
}
screenRecordHelper?.apply {
    if (!isRecording) {
        // if you want to record the audio,you can set the recordAudio as true
        screenRecordHelper?.startRecord()
    }
}

// You must rewrite the onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && data != null) {
        screenRecordHelper?.onActivityResult(requestCode, resultCode, data)
    }
}
    
// just stop screen record
screenRecordHelper?.apply {
    if (isRecording) {
        stopRecord()     
    }
}
Step 4. if you want to mix the audio into your video,you just should do
// parameter1 -> The last video length you want
// parameter2 -> the audio's duration
// parameter2 -> assets resource
stopRecord(duration, audioDuration, afdd)
Step 5. If you still don’t understand, please refer to the demo

由於個人水平有限,雖然目前抗住了公司產品的考驗,但肯定還有很多地方沒有支持全面,希望有知道的大佬不嗇賜教,有任何兼容性問題請直接提 issues,Thx。

參考文章:http://lastwarmth.win/2018/11/23/media-mix/
https://juejin.im/post/5afaee7df265da0ba2672608

【精選推薦文章】

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

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

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

SpringCloud-分佈式配置中心【入門介紹】

案例代碼:https://github.com/q279583842q/springcloud-e-book

一、 為什麼需要使用配置中心

1 服務配置的現狀

2 常用的配置管理解決方案的缺點

3 為什麼要使用 spring cloud config 配置中心?

4 spring cloud config配置中心,它解決了什麼問題?

二、 編寫配置中心入門案例

1.編寫配置中心的服務端

1.1 創建服務端項目

  創建一個SpringCloud項目。

1.2 修改pom文件

  我們需要添加config-server的依賴,具體如下

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.RELEASE</version>
    </parent>
    <groupId>com.bobo</groupId>
    <artifactId>config-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

1.3 修改配置文件

  在此處的配置文件中我們需要關聯碼雲或者GitHub。以碼雲為例

碼雲處理

  首先我們需要在碼雲上註冊一個賬號(https://gitee.com) 然後創建一個新的項目。

配置文件處理

  在配置文件中添加如下配置

spring.application.name=config-server
server.port=9050
# 設置服務註冊中心地址,指向另一個註冊中心
eureka.client.serviceUrl.defaultZone=http://dpb:123456@eureka1:8761/eureka/,http://dpb:123456@eureka2:8761/eureka/

#Git 配置
spring.cloud.config.server.git.uri=https://gitee.com/dengpbs/config
#spring.cloud.config.server.git.username=
#spring.cloud.config.server.git.password=

創建四個配置文件

四個配置文件都有一個e-book屬性,只是值不一樣。然後將這個四個配置文件上傳到碼雲中我們新創建的倉庫

然後將項目中的四個配置文件刪除

1.4 修改啟動類

  我們需要在啟動類中添加eureka客戶端和config服務端的註解,具體如下:

@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ConfigServerStart {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerStart.class, args);
    }
}

1.5 訪問測試

  啟動服務,訪問測試
http://localhost:9050/config-client/test

http://localhost:9050/config-client/default

http://localhost:9050/config-client/dev

通過訪問,我們獲取到了位於碼雲倉庫中的屬性信息。

1.6 配置文件的命名規則與訪問

  注意,上面案例中的配置文件的名稱,並不是隨便命名的,而是有一定的規則來約束的,具體如下:

2.編寫客戶端程序

2.1 創建項目

  創建一個SpringCloud項目

2.2 pom文件修改

  配置中心的客戶端使用的依賴需要注意,不是config-server了,具體如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.RELEASE</version>
    </parent>
    <groupId>com.bobo</groupId>
    <artifactId>config-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.3 修改配置文件

  注意在配置中心的客戶端服務中,配置文件的名稱必須是bootstrap.properties或者bootstrap.yml文件。
官方解釋:

Spring Cloud 構建於 Spring Boot 之上,在 Spring Boot 中有兩種上下文,一種是 bootstrap, 另外一種是 application, bootstrap 是應用程序的父上下文,也就是說 bootstrap 加載優先於 applicaton。bootstrap 主要用於從額外的資源來加載配置信息,還可以在本地外部配置文件中解密屬性。這兩個上下文共用一個環境,它是任何Spring應用程序的外部屬性的來源。bootstrap 裏面的屬性會優先加載,它們默認也不能被本地相同配置覆蓋。

spring.application.name=config-client
server.port=9051
#設置服務註冊中心地址,指向另一個註冊中心
eureka.client.serviceUrl.defaultZone=http://dpb:123456@eureka1:8761/eureka/,http://dpb:123456@eureka2:8761/eureka/

#默認 false,這裏設置 true,表示開啟讀取配置中心的配置
spring.cloud.config.discovery.enabled=true
#對應 eureka 中的配置中心 serviceId,默認是 configserver
spring.cloud.config.discovery.serviceId=config-server
#指定環境
spring.cloud.config.profile=dev
#git 標籤
spring.cloud.config.label=master

2.4 修改啟動類

@SpringBootApplication
@EnableEurekaClient
public class ConfigClientStart {

    public static void main(String[] args) {
        SpringApplication.run(ConfigClientStart.class, args);
    }
}

2.5 創建控制器

  在控制中我們嘗試獲取配置中心的數據,具體如下:

@RestController
public class ShowController {
    
    @Value("${e-book}")
    private String msg;
    
    @RequestMapping("/showMsg")
    public String showMsg(){
        return msg;
    }
}

2.6 啟動測試

訪問:http://localhost:9051/showMsg

搞定~

【精選推薦文章】

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

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

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

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

python算法與數據結構-順序表(37)

 

1、順序表介紹

  順序表是最簡單的一種線性結構,邏輯上相鄰的數據在計算機內的存儲位置也是相鄰的,可以快速定位第幾個元素,中間不允許有空,所以插入、刪除時需要移動大量元素。順序表可以分配一段連續的存儲空間Maxsize,用elem記錄基地址,用length記錄實際的元素個數,即順序表的長度, 

  上圖1表示的是順序表的基本形式,數據元素本身連續存儲,每個元素所佔的存儲單元大小固定相同,元素的下標是其邏輯地址,而元素存儲的物理地址(實際內存地址)可以通過存儲區的起始地址Loc (e0)加上邏輯地址(第i個元素)與存儲單元大小(c)的乘積計算而得,即:Loc(element i) = Loc(e0) + c*i

所以、訪問指定元素時無需從頭遍歷,通過計算便可獲得對應地址,其時間複雜度為O(1)。

  如果元素的大小不統一,則須採用圖2的元素外置的形式,將實際數據元素另行存儲,而順序表中各單元位置保存對應元素的地址信息(即鏈接)。由於每個鏈接所需的存儲量相同,通過上述公式,可以計算出元素鏈接的存儲位置,而後順着鏈接找到實際存儲的數據元素。注意,圖2中的c不再是數據元素的大小,而是存儲一個鏈接地址所需的存儲量,這個量通常很小。

圖2這樣的順序表也被稱為對實際數據的索引,這是最簡單的索引結構。

2、順序表的結構 

  

  一個順序表的完整信息包括兩部分,一部分是表中的元素集合,另一部分是為實現正確操作而需記錄的信息,即有關表的整體情況的信息,這部分信息主要包括元素存儲區的容量和當前表中已有的元素個數兩項。

3、順序表的兩種基本實現方式

  1為一體式結構,存儲表信息的單元與元素存儲區以連續的方式安排在一塊存儲區里,兩部分數據的整體形成一個完整的順序表對象。一體式結構整體性強,易於管理。但是由於數據元素存儲區域是表對象的一部分,順序表創建后,元素存儲區就固定了。

  2為分離式結構,表對象里只保存與整個表有關的信息(即容量和元素個數),實際數據元素存放在另一個獨立的元素存儲區里,通過鏈接與基本表對象關聯。

 4、元素存儲區替換

  一體式結構由於順序表信息區與數據區連續存儲在一起,所以若想更換數據區,則只能整體搬遷,即整個順序表對象(指存儲順序表的結構信息的區域)改變了。分離式結構若想更換數據區,只需將表信息區中的數據區鏈接地址更新即可,而該順序表對象不變。

5、元素存儲區擴充

  採用分離式結構的順序表,若將數據區更換為存儲空間更大的區域,則可以在不改變表對象的前提下對其數據存儲區進行了擴充,所有使用這個表的地方都不必修改。只要程序的運行環境(計算機系統)還有空閑存儲,這種表結構就不會因為滿了而導致操作無法進行。人們把採用這種技術實現的順序表稱為動態順序表,因為其容量可以在使用中動態變化。

擴充的兩種策略

  • 每次擴充增加固定數目的存儲位置,如每次擴充增加10個元素位置,這種策略可稱為線性增長。

    特點:節省空間,但是擴充操作頻繁,操作次數多。

  • 每次擴充容量加倍,如每次擴充增加一倍存儲空間。

    特點:減少了擴充操作的執行次數,但可能會浪費空間資源。以空間換時間,推薦的方式。

6、順序表的增刪改查操作的Python代碼實現

# 創建順序表
class Sequence_Table():
    
    # 初始化
    def __init__(self):
        self.date = [None]*100
        self.length = 0
    
    # 判斷是否已經滿了
    def isFull(self):
        if self.length>100:
            print("該順序表已滿,無法添加元素")
            return 1
        else:
            return 0
    
    # 按下錶索引查找
    def selectByIndex(self,index):
        if index>=0 and index<=self.length-1:
            return self.date[index]
        else:
            print("你輸入的下標不對,請重新輸入\n")
            return 0
        
    # 按元素查下標
    def selectByNum(self,num):
        isContain = 0
        for i in range(0,self.length):
            if self.date[i] == num:
                isContain = 1
                print("你要查找的元素下標是%d\n"%i)
        if isContain == 0:
            print("沒有找你你要的數據")
    
    # 追加數據
    def addNum(self,num):
        if self.isFull() == 0:
            self.date[self.length] = num
            self.length += 1
            
    # 打印順序表
    def printAllNum(self):
        for i in range(self.length):
            print("a[%s]=%s"%(i,self.date[i]),end=" ")
        print("\n")
        
    # 按下標插入數據
    def insertNumByIndex(self,num,index):
        if index<0 or index>self.length:
            return 0
        self.length += 1
        for i in range(self.length-1,index,-1):
            temp = self.date[i]
            self.date[i] = self.date[i-1]
            self.date[i-1] = temp
        self.date[index] = num
        return 1
    # 按下標刪除數據
    def delectNumByIndex(self,index):
        if self.length <= 0:
            print("該順序表內沒有數據,不用刪除")
            
        for i in range(index,self.length-1):
            temp = self.date[i]
            self.date[i] = self.date[i + 1]
            self.date[i + 1] = temp
        self.date[self.length-1] = 0
        self.length -= 1

def main():
    # 創建順序表對象
    seq_t = Sequence_Table()
    
    # 插入三個元素
    seq_t.addNum(1)
    seq_t.addNum(2)
    seq_t.addNum(3)
    
    # 打印驗證
    seq_t.printAllNum()
    
    # 按照索引查找
    num = seq_t.selectByIndex(2)
    print("你要查找的數據是%d\n" % num)
    
    # 按照索引插入數據
    seq_t.insertNumByIndex(4, 1)
    seq_t.printAllNum()
    
    # 按照数字查下標
    seq_t.selectByNum(4)
    
    #刪除數據
    seq_t.delectNumByIndex(1)
    seq_t.printAllNum()
     
if __name__ == "__main__":
    main()

運行結果為:

a[0]=1 a[1]=2 a[2]=3 

你要查找的數據是3

a[0]=1 a[1]=4 a[2]=2 a[3]=3 

你要查找的元素下標是1

a[0]=1 a[1]=2 a[2]=3 

7、順序表的增刪改查操作的C語言代碼實現

#include<stdio.h>
// 1、定義順序表的儲存結構
typedef struct
{
    //用數組存儲線性表中的元素
    int data[100];
    // 順序表中的元素個數
    int length;
}Sequence_table,*p_Sequence_table;

// 2、順序表的初始化,
void initSequenceTable(p_Sequence_table T)
{
    // 判斷傳過來的表是否為空,為空直接退出
    if (T == NULL)
    {
        return;
    }
    // 設置默認長度為0
    T->length = 0;
}

// 3、求順序表的長度
int lengthOfSequenceTable(p_Sequence_table T)
{
    if (T==NULL)
    {
        return 0;
    }
    return T->length;
}

// 4、判斷順序表是否已滿
int isFull(p_Sequence_table T)
{
    if (T->length>=100)
    {
        printf("該順序表已經裝滿,無法再添加元素");
        return 1;
    }
    return 0;
}

// 5、按序號查找
int selectSequenceTableByIndex(p_Sequence_table T,int index)
{
    if (index>=0&&index<=T->length-1)
    {
        return T->data[index];
    }
    printf("你輸入的序號不對,請重新輸入\n");
    return 0;
}

// 6、按內容查找是否存在
void selectSequenceTableByNum(p_Sequence_table T,int num)
{
    int isContain = 0;
    for (int i=0; i<T->length; i++)
    {
        if (T->data[i] == num)
        {
            isContain = 1;
            printf("你要找的元素的下標是:%d\n",i);
        }
    }
    if (isContain == 0)
    {
        printf("沒有找到你要的數據\n");
    }
}

// 7、添加元素(在隊尾添加)
void addNumber(p_Sequence_table T,int num)
{
    // 順序表還沒有滿的時候
    if (isFull(T) == 0)
    {
        T->data[T->length] = num;
        T->length++;
    }
}

// 8、順序表的遍歷
void printAllNumOfSequenceTable(p_Sequence_table T)
{
    for (int i = 0; i<T->length; i++)
    {
        printf("T[%d]=%d ",i,T->data[i]);
    }
    printf("\n");
}

//9、插入操作
int insertNumByIndex(p_Sequence_table T, int num,int index)
{
    if (index<0||index>T->length)
    {
        return 0;
    }
    T->length++;
    for (int i = T->length-1; i>index; i--)
    {
        int temp = T->data[i];
        T->data[i] = T->data[i-1];
        T->data[i-1] = temp;
    }
    T->data[index] = num;
    return 1;
}

// 10、刪除元素
void delectNum(p_Sequence_table T,int index)
{
    if (T->length <= 0)
    {
        printf("該順序表中沒有數據,不用刪除");
    }
    for (int i = index;i<T->length-1; i++)
    {
        int temp = T->data[i];
        T->data[i] = T->data[i+1];
        T->data[i+1] = temp;
    }
    T->data[T->length-1] = 0;
    T->length--;
}



int main(int argc, const char * argv[]) {
    
    // 創建順序表的結構體
    Sequence_table seq_t;
    // 初始化
    initSequenceTable(&seq_t);
    // 添加數據
    addNumber(&seq_t, 1);
    addNumber(&seq_t, 2);
    addNumber(&seq_t, 3);
    // 打印驗證
    printAllNumOfSequenceTable(&seq_t);
    // 根據索引下標查內容
    int num = selectSequenceTableByIndex(&seq_t, 2);
    printf("你查的數據是:%d\n",num);
    // 插入
    insertNumByIndex(&seq_t, 4, 1);
    printAllNumOfSequenceTable(&seq_t);
    // 根據內容查下標
    selectSequenceTableByNum(&seq_t, 4);
    // 根據下標刪除數據
    delectNum(&seq_t, 1);
    printAllNumOfSequenceTable(&seq_t);
    return 0;
}

運行結果為:

T[0]=1 T[1]=2 T[2]=3 
你查的數據是:3
T[0]=1 T[1]=4 T[2]=2 T[3]=3 
你要找的元素的下標是:1
T[0]=1 T[1]=2 T[2]=3 

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

不吹不黑,關於 Java 類加載器的這一點,市面上沒有任何一本圖書講到,實戰分析Tomcat的類加載器結構(使用Eclipse MAT驗證),還是Tomcat,關於類加載器的趣味實驗,重寫類加載器,實現簡單的熱替換,@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼,@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

類加載器第7彈:

實戰分析Tomcat的類加載器結構(使用Eclipse MAT驗證)

還是Tomcat,關於類加載器的趣味實驗

了不得,我可能發現了Jar 包衝突的秘密

重寫類加載器,實現簡單的熱替換

@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼

@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

 

 

一、一個程序員的思考

大家都知道,Tomcat 處理業務,靠什麼?最終是靠我們自己編寫的 Servlet。你可能說你不寫 servlet,你用 spring MVC,那也是人家幫你寫好了,你只需要配置就行。在這裏,有一個邊界,Tomcat 算容器,容器的相關 jar 包都放在它自己的 安裝目錄的 lib 下面; 我們呢,算是業務,算是webapp,我們的 servlet ,不管是自定義的,還是 spring mvc 的DispatcherServlet,都是放在我們的 war 包裏面 WEB-INF/lib下。 看過前面文章的同學是曉得的, 這二者是由不同的類加載器加載的。在 Tomcat 的實現中,會委託 webappclassloader 去加載WAR 包中的 servlet ,然後 反射生成對應的 servlet。後續有請求來了,調用生成的 servlet 的 service 方法即可。

在 org.apache.catalina.core.StandardWrapper#loadServlet 中,即負責 生成 servlet:

 

 org.apache.catalina.core.DefaultInstanceManager#newInstance(java.lang.String)
@Override
public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException { Class<?> clazz = loadClassMaybePrivileged(className, classLoader); return newInstance(clazz.newInstance(), clazz); }

 

在上圖中,會利用 instanceManager 根據參數中指定的 servletClass 去生成 servlet 實例。newInstance 代碼如下,主要就是用 當前 context 的classloader 去加載 該 servlet,然後 反射生成 servlet 對象。

我們重點關注的是那個紅框圈出的強轉:為什麼由 webappclassloader 加載的對象,可以轉換 為 Tomcat common classloader 加載的 Servlet 呢? 按理說,兩個不同的類加載器加載的類都是互相隔離的啊,不應該拋一個 ClassCastException 嗎?說真的,我翻了不少書,從來沒提到這個,就連網上也很含糊。

 

再來一個,關於SPI的問題。  在 SPI 中(有興趣的同學可以自行查詢,網上很多,我隨便找了一篇:https://www.jianshu.com/p/46b42f7f593c),主要是由 java 社區指定規範,比如 JDBC,廠家有那麼多,mysql,oracle,postgre,大家都有自己的 jar包,要是沒有 JDBC 規範,我們估計就得針對各個廠家的實現類編程了,那遷移就麻煩了,你針對 mysql 數據庫寫的代碼,換成 oracle 的話,代碼不改是肯定不能跑的。所以, JCP組織制定了 JDBC 規範,JDBC 規範中指定了一堆的 接口,我們平時開發,只需要針對接口來編程,而實現怎麼辦,交給各廠家唄,由廠家來實現 JDBC 規範。這裏以代碼舉例,oracle.jdbc.OracleDriver 實現了 java.sql.Driver,同時,在 oracle.jdbc.OracleDriver 的 static 初始化塊中,有下面的代碼:

 

    static { try { if (defaultDriver == null) { defaultDriver = new oracle.jdbc.OracleDriver();  DriverManager.registerDriver(defaultDriver);  } // 省略
    }

其中,標紅這句,就是 Oracle Driver 要向 JDBC 接口註冊自己,java.sql.DriverManager#registerDriver(java.sql.Driver)的實現如下:

java.sql.DriverManager#registerDriver(java.sql.Driver) 

public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException { registerDriver(driver, null); }

 

可以看到,registerDriver(java.sql.Driver)  方法的參數為 java.sql.Driver,而我們傳的參數為 oracle.jdbc.OracleDriver 類型,這兩個類型,分別由不同的類加載器加載(java.sql.Driver 由 jdk 的 啟動類加載器加載,而 oracle.jdbc.OracleDriver ,如果為 web應用,則為 tomcat 的 webappclassloader 來加載,不管怎麼說,反正不是由 jdk 加載的),這樣的兩個類型,連 類加載器都不一樣,怎麼就能正常轉換呢,為啥不拋 ClassCastException?

 

 二、不同類加載器加載的類,可以轉換的關鍵

經過上面兩個例子的觀察,不知道大家發現沒, 我們都是把一個實現,轉換為一個接口。也許,這就是問題的關鍵。我們可以大膽地推測,基於類的雙親委派機制,在 加載 實現類的時候,jvm 遇到 實現類中引用到的其他類,也會觸發加載,加載的過程中,會觸發 loadClass,比如,加載 webappclassloader 在 加載 oracle.jdbc.OracleDriver 時,觸發加載 java.sql.Driver,但是 webappclassloader 明顯是不能去加載 java.sql.Driver 的,於是會委託給 jdk 的類加載,所以,最終,oracle.jdbc.OracleDriver 中 引用的 java.sql.Driver ,其實就是由 jdk 的類加載器去加載的。 而  registerDriver(java.sql.Driver driver) 中的 driver 參數的類型 java.sql.Driver 也是由 jdk 的類加載器去加載的,二者相同,所以自然可以相互轉換。

 

這裏總結一句(不一定對),在同時滿足以下幾個條件的情況下:

前置條件1、接口 jar包 中,定義一個接口 Test

前置條件2、實現 jar 包中,定義 Test 的實現類,比如 TestImpl。(但是不要在該類中包含該 接口,你說沒法編譯,那就把接口 jar包放到 classpath)

前置條件3、接口 jar 包由 interface_classLoader 加載,實現 jar 包 由 impl_classloader 加載,其中 impl_classloader 會在自己無法加載時,委派給 interface_classLoader 

 

則,定義在 實現jar 中的Test 接口的實現類,反射生成的對象,可以轉換為 Test 類型。

 

猜測說完了,就是求證過程。

 

三、求證

1、定義接口 jar 

D:\classloader_interface\ITestSample.java  

/**
* desc: * * @author : * creat_date: 2019/6/16 0016 * creat_time: 19:28 **/ public interface ITestSample { }

 

cmd下,執行:

D:\classloader_interface>javac ITestSample.java
D:\classloader_interface>jar cvf interface.jar ITestSample.class 已添加清單 正在添加: ITestSample.class(輸入 = 103) (輸出 = 86)(壓縮了 16%)

 

此時,即可在當前目錄下,生成 名為 interface.jar 的接口jar包。

 

2、定義接口的實現 jar

在不同目錄下,新建了一個實現類。

D:\classloader_impl\TestSampleImpl.java /**
 * Created by Administrator on 2019/6/25. */ public class TestSampleImpl implements ITestSample{ }

編譯,打包:

1 D:\classloader_impl>javac -cp D:\classloader_interface\interface.jar TestSampleI 2 mpl.java 3 
4 D:\classloader_impl>jar -cvf impl.jar TestSampleImpl.class 5 已添加清單 6 正在添加: TestSampleImpl.class(輸入 = 221) (輸出 = 176)(壓縮了 20%)

 

請注意上面的標紅行,不加編譯不過。

 

3、測試

測試的思路是,用一個urlclassloader 去加載 interface.jar 中的 ITestSample,用另外一個 URLClassLoader 去加載 impl.jar 中的 TestSampleImpl ,然後用java.lang.Class#isAssignableFrom 判斷後者是否能轉成前者。

 

 1 import java.lang.reflect.Method;  2 import java.net.URL;  3 import java.net.URLClassLoader;  4 
 5 /**
 6  * desc:  7  *  8  * @author : caokunliang  9  * creat_date: 2019/6/14 0014 10  * creat_time: 17:04 11  **/
12 public class MainTest { 13 
14 
15     public static void testInterfaceByOneAndImplByAnother()throws Exception{ 16         URL url = new URL("file:D:\\classloader_interface\\interface.jar"); 17         URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}); 18         Class<?> iTestSampleClass = urlClassLoader.loadClass("ITestSample"); 19 
20 
21         URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); 22         URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader); 23         Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); 24 
25 
26         System.out.println("實現類能轉否?:"  + iTestSampleClass.isAssignableFrom(testSampleImplClass)); 27 
28  } 29 
30     public static void main(String[] args) throws Exception { 31  testInterfaceByOneAndImplByAnother(); 32  } 33 
34 }

 

打印如下:

 

4、延伸測試1

如果我們做如下改動,你猜會怎樣? 這裏的主要差別是:

改之前,urlClassloader 作為 parentClassloader: 

        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);

改之後,不傳,默認會以 jdk 的應用類加載器作為 parent:

        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl});

 

打印結果是:

Exception in thread "main" java.lang.NoClassDefFoundError: ITestSample at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:760) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:455) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:367) at java.net.URLClassLoader$1.run(URLClassLoader.java:361) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:360) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at MainTest.testInterfaceByOneAndImplByAnother(MainTest.java:23) at MainTest.main(MainTest.java:33) Caused by: java.lang.ClassNotFoundException: ITestSample at java.net.URLClassLoader$1.run(URLClassLoader.java:372) at java.net.URLClassLoader$1.run(URLClassLoader.java:361) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:360) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 13 more

 

 結果就是,第23行,Class<?> testSampleImplClass = implUrlClassLoader.loadClass(“TestSampleImpl”); 這裏報錯了,提示找不到 ITestSample。

這就是因為,在加載了 implUrlClassLoader 后,觸發了對 ITestSample 的隱式加載,這個隱式加載會用哪個加載器去加載呢,沒有默認指明的情況下,就是用當前的類加載器,而當前類加載器就是 implUrlClassLoader ,但是這個類加載器開始加載 ITestSample,它是遵循雙親委派的,它的parent 加載器 即為 appclassloader,(jdk的默認應用類加載器),但appclassloader 根本不能加載 ITestSample,於是還是還給 implUrlClassLoader ,但是 implUrlClassLoader  也不能加載,於是拋出異常。

 

5、延伸測試2

我們再做一個改動, 改動處和上一個測試一樣,只是這次,我們傳入了一個特別的類加載器,作為其 parentClassLoader。 它的特殊之處在於,almostSameUrlClassLoader 和 前面加載 interface.jar 的類加載器一模一樣,只是是一個新的實例。

 URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});  URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);

 

這次,看看結果吧,也許你猜到了?

 

這次沒報錯了,畢竟 almostSameUrlClassLoader  知道去哪裡加載 ITestSample,但是,最後的結果显示,實現類的 class 並不能 轉成 ITestSample。

 

6、延伸測試3

說實話,有些同學可能對 java.lang.Class#isAssignableFrom 不是很熟悉,我們換個你更不熟悉的,如何?

        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url}); URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader); Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); Object o = testSampleImplClass.newInstance(); Object cast = iTestSampleClass.cast(o); // 將 o 轉成 接口的那個類 System.out.println(cast);

結果:

 

如果換成下面這樣,就沒啥問題:

        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url}); URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader); Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); Object o = testSampleImplClass.newInstance(); Object cast = iTestSampleClass.cast(o); System.out.println(cast);

 

執行:

 

總結

大家將就看吧,第三章的測試如果仔細看下來,基本就能理解了。 其實,除了 接口這種方式,貌似 繼承 的方式也是可以的,改天再試驗下。 這一塊,不知道為啥,我是真的在網上書上沒找到,但其實很重要,改天找找虛擬機層面的實現代碼吧。 大家如果覺得有幫助,麻煩點個推薦,對於寫作的人來說,這莫過於最大的獎勵了。

 

參考:

深入探討 Java 類加載器

 

 

 

 

 

【精選推薦文章】

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

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

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

SpringBoot之ApplicationContextInitializer的理解和使用

一、 ApplicationContextInitializer 介紹

  首先看spring官網的介紹:

   翻譯一下:

  • 用於在spring容器刷新之前初始化Spring ConfigurableApplicationContext的回調接口。(剪短說就是在容器刷新之前調用該類的 initialize 方法。並將 ConfigurableApplicationContext 類的實例傳遞給該方法)
  • 通常用於需要對應用程序上下文進行編程初始化的web應用程序中。例如,根據上下文環境註冊屬性源或激活配置文件等。
  • 可排序的(實現Ordered接口,或者添加@Order註解)

  看完這段解釋,為了講解方便,我們先看自定義 ApplicationContextInitializer 的三種方式。再通過SpringBoot的源碼,分析生效的時間以及實現的功能等。

二、三種實現方式

  首先新建一個類 MyApplicationContextInitializer 並實現 ApplicationContextInitializer 接口。

1 public class MyApplicationContextInitializer implements ApplicationContextInitializer {
2     @Override
3     public void initialize(ConfigurableApplicationContext applicationContext) {
4         System.out.println("-----MyApplicationContextInitializer initialize-----");
5     }
6 }

  2.1、mian函數中添加

  優雅的寫一個SpringBoot的main方法

1 @SpringBootApplication
2 public class MySpringBootApplication {
3     public static void main(String[] args) {
4         SpringApplication application = new SpringApplication(MySpringBootApplication.class);
5         application.addInitializers(new MyApplicationContextInitializer());
6         application.run(args);
7     }
8 }

 

  運行,查看控制台:生效了

  

  2.2、配置文件中配置

context.initializer.classes=org.springframework.boot.demo.common.MyApplicationContextInitializer 

 

  

  2.3、SpringBoot的SPI擴展—META-INF/spring.factories中配置

org.springframework.context.ApplicationContextInitializer=org.springframework.boot.demo.common.MyApplicationContextInitializer

 

  

 

三、排序問題

  如圖所示改造一下mian方法。打一個斷點,debug查看排序情況。

  

  給 MyApplicationContextInitializer 加上Order註解:我們指定其擁有最高的排序級別。(越高越早執行)

1 @Order(Ordered.HIGHEST_PRECEDENCE)
2 public class MyApplicationContextInitializer implements ApplicationContextInitializer{
3     @Override
4     public void initialize(ConfigurableApplicationContext applicationContext) {
5         System.out.println("-----MyApplicationContextInitializer initialize-----");
6     }
7 }

 

  下面我們通過debug分別驗證二章節中提到的三種方法排序是否都是可以的。

  首先驗證2.1章節中採用的main函數中添加:debug,斷點處查看 application.getInitializers() 這行代碼的結果可見,排序生效了。

  

  然後再分別驗證2.2和2.3章節中的方法。排序都是可以實現的。

  然而當採用2.3中的SPI擴展的方式,排序指定 @Order(Ordered.LOWEST_PRECEDENCE) 排序並沒有生效。當然採用實現Ordered接口的方式,排序驗證結果都是一樣的。

 四、通過源碼分析ApplicationContextInitializer何時被調用

  debug差看上文中自定的 MyApplicationContextInitializer 的調用棧。

  

  可見 ApplicationContextInitializer 在容器刷新前的準備階段被調用。 refreshContext(context); 

  在SpringBoot的啟動函數中, ApplicationContextInitializer 

 1     public ConfigurableApplicationContext run(String... args) {
 2         //記錄程序運行時間
 3         StopWatch stopWatch = new StopWatch();
 4         stopWatch.start();
 5         // ConfigurableApplicationContext Spring 的上下文
 6         ConfigurableApplicationContext context = null;
 7         Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
 8         configureHeadlessProperty();
 9         //從META-INF/spring.factories中獲取監聽器
10         //1、獲取並啟動監聽器
11         SpringApplicationRunListeners listeners = getRunListeners(args);
12         listeners.starting();
13         try {
14             ApplicationArguments applicationArguments = new DefaultApplicationArguments(
15                     args);
16             //2、構造容器環境
17             ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
18             //處理需要忽略的Bean
19             configureIgnoreBeanInfo(environment);
20             //打印banner
21             Banner printedBanner = printBanner(environment);
22             ///3、初始化容器
23             context = createApplicationContext();
24             //實例化SpringBootExceptionReporter.class,用來支持報告關於啟動的錯誤
25             exceptionReporters = getSpringFactoriesInstances(
26                     SpringBootExceptionReporter.class,
27                     new Class[]{ConfigurableApplicationContext.class}, context);
28             //4、刷新容器前的準備階段
29             prepareContext(context, environment, listeners, applicationArguments, printedBanner);
30             //5、刷新容器
31             refreshContext(context);
32             //刷新容器后的擴展接口
33             afterRefresh(context, applicationArguments);
34             stopWatch.stop();
35             if (this.logStartupInfo) {
36                 new StartupInfoLogger(this.mainApplicationClass)
37                         .logStarted(getApplicationLog(), stopWatch);
38             }
39             listeners.started(context);
40             callRunners(context, applicationArguments);
41         } catch (Throwable ex) {
42             handleRunFailure(context, ex, exceptionReporters, listeners);
43             throw new IllegalStateException(ex);
44         }
45 
46         try {
47             listeners.running(context);
48         } catch (Throwable ex) {
49             handleRunFailure(context, ex, exceptionReporters, null);
50             throw new IllegalStateException(ex);
51         }
52         return context;
53     }

 

   然後看在 refreshContext(context); 具體是怎麼被調用的。

1 private void prepareContext(ConfigurableApplicationContext context,
2                             ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
3                             ApplicationArguments applicationArguments, Banner printedBanner) {
4     context.setEnvironment(environment);
5     postProcessApplicationContext(context);
6     applyInitializers(context);
7     ...
8 }

 

   然後在 applyInitializers 中遍歷調用每一個被加載的 ApplicationContextInitializer 的  initialize(context);  方法,並將 ConfigurableApplicationContext 的實例傳遞給 initialize 方法。

1 protected void applyInitializers(ConfigurableApplicationContext context) {
2     for (ApplicationContextInitializer initializer : getInitializers()) {
3         Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
4                 initializer.getClass(), ApplicationContextInitializer.class);
5         Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
6         initializer.initialize(context);
7     }
8 }

 

  OK,到這裏通過源碼說明了 ApplicationContextInitializer 是何時及如何被調用的。

 

【精選推薦文章】

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

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

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

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

從0到1:全面理解RPC遠程調用

上一篇關於 WSGI 的硬核長文,不知道有多少同學,能夠從頭看到尾的,不管你們有沒有看得很過癮,反正我是寫得很爽,總有一種將一樣知識吃透了的錯覺。

今天我又給自己挖坑了,打算將 rpc 遠程調用的知識,好好地梳理一下,花了周末整整两天的時間。

什麼是RPC呢?

百度百科給出的解釋是這樣的:“RPC(Remote Procedure Call Protocol)——遠程過程調用協議,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議”。這個概念聽起來還是比較抽象,沒關係,繼續往後看,後面概念性的東西,我會講得足夠清楚,讓你完全掌握 RPC 的基礎內容。在後面的篇章中還會結合其在 OpenStack 中實際應用,一步一步揭開 rpc 的神秘面紗。

有的讀者,可能會問,為啥我舉的例子老是 OpenStack 里的東西呢?

因為每個人的業務中接觸的框架都不一樣(我主要接觸的就是 OpenStack 框架),我無法為每個人去定製寫一篇文章,但其技術原理都是一樣的。即使如此,我也會儘力將文章寫得通用,不會因為你沒接觸過 OpenStack 而成為你理解 rpc 的瓶頸。

01. 既 REST,何 RPC ?

在 OpenStack 里的進程間通信方式主要有兩種,一種是基於HTTP協議的RESTFul API方式,另一種則是RPC調用。

那麼這兩種方式在應用場景上有何區別呢?

有使用經驗的人,就會知道:

  • 前者(RESTful)主要用於各組件之間的通信(如nova與glance的通信),或者說用於組件對外提供調用接口
  • 而後者(RPC)則用於同一組件中各個不同模塊之間的通信(如nova組件中nova-compute與nova-scheduler的通信)。

關於OpenStack中基於RESTful API的通信方式主要是應用了WSGI,這個知識點,我在前一篇文章中,有深入地講解過,你可以點擊查看。

對於不熟悉 OpenStack 的人,也別擔心聽不懂,這樣吧,我給你提兩個問題:

  1. RPC 和 REST 區別是什麼?
  2. 為什麼要採用RPC呢?

第一個問題:RPC 和 REST 區別是什麼?

你一定會覺得這個問題很奇怪,是的,包括我,但是你在網絡上一搜,會發現類似對比的文章比比皆是,我在想可能很多初學者由於基礎不牢固,才會將不相干的二者拿出來對比吧。既然是這樣,那為了讓你更加了解陌生的RPC,就從你熟悉得不能再熟悉的 REST 入手吧。

01、所屬類別不同

REST,是Representational State Transfer 的簡寫,中文描述表述性狀態傳遞(是指某個瞬間狀態的資源數據的快照,包括資源數據的內容、表述格式(XML、JSON)等信息。)

REST 是一種軟件架構風格。 這種風格的典型應用,就是HTTP。其因為簡單、擴展性強的特點而廣受開發者的青睞。

而RPC 呢,是 Remote Procedure Call Protocol 的簡寫,中文描述是遠程過程調用,它可以實現客戶端像調用本地服務(方法)一樣調用服務器的服務(方法)。

RPC 是一種基於 TCP 的通信協議,按理說它和REST不是一個層面上的東西,不應該放在一起討論,但是誰讓REST這麼流行呢,它是目前最流行的一套互聯網應用程序的API設計標準,某種意義下,我們說 REST 可以其實就是指代 HTTP 協議。

02、使用方式不同

從使用上來看,HTTP 接口只關注服務提供方,對於客戶端怎麼調用並不關心。接口只要保證有客戶端調用時,返回對應的數據就行了。而RPC則要求客戶端接口保持和服務端的一致。

  • REST 是服務端把方法寫好,客戶端並不知道具體方法。客戶端只想獲取資源,所以發起HTTP請求,而服務端接收到請求后根據URI經過一系列的路由才定位到方法上面去
  • PRC是服務端提供好方法給客戶端調用,客戶端需要知道服務端的具體類,具體方法,然後像調用本地方法一樣直接調用它。

03、面向對象不同

從設計上來看,RPC,所謂的遠程過程調用 ,是面向方法的 ,REST:所謂的 Representational state transfer ,是面向資源的,除此之外,還有一種叫做 SOA,所謂的面向服務的架構,它是面向消息的,這個接觸不多,就不多說了。

04、序列化協議不同

接口調用通常包含兩個部分,序列化和通信協議。

通信協議,上面已經提及了,REST 是 基於 HTTP 協議,而 RPC 可以基於 TCP/UDP,也可以基於 HTTP 協議進行傳輸的。

常見的序列化協議,有:json、xml、hession、protobuf、thrift、text、bytes等,REST 通常使用的是 JSON或者XML,而 RPC 使用的是 JSON-RPC,或者 XML-RPC。

通過以上幾點,我們知道了 REST 和 RPC 之間有很明顯的差異。

第二個問題:為什麼要採用RPC呢?

那到底為何要使用 RPC,單純的依靠RESTful API不可以嗎?為什麼要搞這麼多複雜的協議,渣渣表示真的學不過來了。

關於這一點,以下幾點僅是我的個人猜想,僅供交流哈:

  1. RPC 和 REST 兩者的定位不同,REST 面向資源,更注重接口的規範,因為要保證通用性更強,所以對外最好通過 REST。而 RPC 面向方法,主要用於函數方法的調用,可以適合更複雜通信需求的場景。
  2. RESTful API客戶端與服務端之間採用的是同步機制,當發送HTTP請求時,客戶端需要等待服務端的響應。當然對於這一點是可以通過一些技術來實現異步的機制的。
  3. 採用RESTful API,客戶端與服務端之間雖然可以獨立開發,但還是存在耦合。比如,客戶端在發送請求的時,必須知道服務器的地址,且必須保證服務器正常工作。而 rpc + ralbbimq中間件可以實現低耦合的分佈式集群架構。

說了這麼多,我們該如何選擇這兩者呢?我總結了如下兩點,供你參考:

  • REST 接口更加規範,通用適配性要求高,建議對外的接口都統一成 REST(也有例外,比如我接觸過 zabbix,其 API 就是基於 JSON-RPC 2.0協議的)。而組件內部的各個模塊,可以選擇 RPC,一個是不用耗費太多精力去開發和維護多套的HTTP接口,一個RPC的調用性能更高(見下條)
  • 從性能角度看,由於HTTP本身提供了豐富的狀態功能與擴展功能,但也正由於HTTP提供的功能過多,導致在網絡傳輸時,需要攜帶的信息更多,從性能角度上講,較為低效。而RPC服務網絡傳輸上僅傳輸與業務內容相關的數據,傳輸數據更小,性能更高。

02. 實現遠程調用的三種方式

“遠程調用”意思就是:被調用方法的具體實現不在程序運行本地,而是在別的某個地方(分佈到各個服務器),調用者只想要函數運算的結果,卻不需要實現函數的具體細節。

01、基於 xml-rpc

Python實現 rpc,可以使用標準庫里的 SimpleXMLRPCServer,它是基於XML-RPC 協議的。

有了這個模塊,開啟一個 rpc server,就變得相當簡單了。執行以下代碼:

import SimpleXMLRPCServer

class calculate:
    def add(self, x, y):
        return x + y

    def multiply(self, x, y):
        return x * y

    def subtract(self, x, y):
        return abs(x-y)

    def divide(self, x, y):
        return x/y


obj = calculate()
server = SimpleXMLRPCServer.SimpleXMLRPCServer(("localhost", 8088))
# 將實例註冊給rpc server
server.register_instance(obj)

print "Listening on port 8088"
server.serve_forever()

有了 rpc server,接下來就是 rpc client,由於我們上面使用的是 XML-RPC,所以 rpc clinet 需要使用xmlrpclib 這個庫。

import xmlrpclib

server = xmlrpclib.ServerProxy("http://localhost:8088")

然後,我們通過 server_proxy 對象就可以遠程調用之前的rpc server的函數了。

>> server.add(2, 3)
5
>>> server.multiply(2, 3)
6
>>> server.subtract(2, 3)
1
>>> server.divide(2, 3)
0

SimpleXMLRPCServer是一個單線程的服務器。這意味着,如果幾個客戶端同時發出多個請求,其它的請求就必須等待第一個請求完成以後才能繼續。

若非要使用 SimpleXMLRPCServer 實現多線程併發,其實也不難。只要將代碼改成如下即可。

from SimpleXMLRPCServer import SimpleXMLRPCServer
from SocketServer import ThreadingMixIn
class ThreadXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):pass

class MyObject:
    def hello(self):
        return "hello xmlprc"

obj = MyObject()
server = ThreadXMLRPCServer(("localhost", 8088), allow_none=True)
server.register_instance(obj)

print "Listening on port 8088"
server.serve_forever()

02、基於json-rpc

SimpleXMLRPCServer 是基於 xml-rpc 實現的遠程調用,上面我們也提到 除了 xml-rpc 之外,還有 json-rpc 協議。

那 python 如何實現基於 json-rpc 協議呢?

答案是很多,很多web框架其自身都自己實現了json-rpc,但我們要獨立這些框架之外,要尋求一種較為乾淨的解決方案,我查找到的選擇有兩種

第一種是 jsonrpclib

pip install jsonrpclib -i https://pypi.douban.com/simple

第二種是 python-jsonrpc

pip install python-jsonrpc -i https://pypi.douban.com/simple

先來看第一種 jsonrpclib

它與 Python 標準庫的 SimpleXMLRPCServer 很類似(因為它的類名就叫做 SimpleJSONRPCServer ,不明真相的人真以為它們是親兄弟)。或許可以說,jsonrpclib 就是仿照 SimpleXMLRPCServer 標準庫來進行編寫的。

它的導入與 SimpleXMLRPCServer 略有不同,因為SimpleJSONRPCServer分佈在jsonrpclib庫中。

服務端

from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer

server = SimpleJSONRPCServer(('localhost', 8080))
server.register_function(lambda x,y: x+y, 'add')
server.serve_forever()

客戶端

import jsonrpclib

server = jsonrpclib.Server("http://localhost:8080")

再來看第二種python-jsonrpc,寫起來貌似有些複雜。

服務端

import pyjsonrpc


class RequestHandler(pyjsonrpc.HttpRequestHandler):

    @pyjsonrpc.rpcmethod
    def add(self, a, b):
        """Test method"""
        return a + b

http_server = pyjsonrpc.ThreadingHttpServer(
    server_address=('localhost', 8080),
    RequestHandlerClass=RequestHandler
)
print "Starting HTTP server ..."
print "URL: http://localhost:8080"
http_server.serve_forever()

客戶端

import pyjsonrpc

http_client = pyjsonrpc.HttpClient(
    url="http://localhost:8080/jsonrpc"
)

還記得上面我提到過的 zabbix API,因為我有接觸過,所以也拎出來講講。zabbix API 也是基於 json-rpc 2.0協議實現的。

因為內容較多,這裏只帶大家打個,zabbix 是如何調用的:直接指明要調用 zabbix server 的哪個方法,要傳給這個方法的參數有哪些。

03、基於 zerorpc

以上介紹的兩種rpc遠程調用方式,如果你足夠細心,可以發現他們都是http+rpc 兩種協議結合實現的。

接下來,我們要介紹的這種(zerorpc),就不再使用走 http 了。

zerorpc 這個第三方庫,它是基於TCP協議、 ZeroMQ 和 MessagePack的,速度相對快,響應時間短,併發高。zerorpc 和 pyjsonrpc 一樣,需要額外安裝,雖然SimpleXMLRPCServer不需要額外安裝,但是SimpleXMLRPCServer性能相對差一些。

pip install zerorpc -i https://pypi.douban.com/simple

服務端代碼

import zerorpc

class caculate(object):
    def hello(self, name):
        return 'hello, {}'.format(name)

    def add(self, x, y):
        return x + y

    def multiply(self, x, y):
        return x * y

    def subtract(self, x, y):
        return abs(x-y)

    def divide(self, x, y):
        return x/y

s = zerorpc.Server(caculate())

s.bind("tcp://0.0.0.0:4242")
s.run()

客戶端

import zerorpc

c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")

客戶端除了可以使用zerorpc框架實現代碼調用之外,它還支持使用“命令行”的方式調用。

客戶端可以使用命令行,那服務端是不是也可以呢?

是的,通過 Github 上的文檔幾個 demo 可以體驗到這個第三方庫做真的是優秀。

比如我們可以用下面這個命令,創建一個rpc server,後面這個 time Python 標準庫中的 time 模塊,zerorpc 會將 time 註冊綁定以供client調用。

zerorpc --server --bind tcp://127.0.0.1:1234 time

在客戶端,就可以用這條命令來遠程調用這個 time 函數。

zerorpc --client --connect tcp://127.0.0.1:1234 strftime %Y/%m/%d

03. 往rpc中引入消息中間件

經過了上面的學習,我們已經學會了如何使用多種方式實現rpc遠程調用。

通過對比,zerorpc 可以說是脫穎而出,一支獨秀。

但為何在 OpenStack 中,rpc client 不直接 rpc 調用 rpc server ,而是先把 rpc 調用請求發給 RabbitMQ ,再由訂閱者(rpc server)來取消息,最終實現遠程調用呢?

為此,我也做了一番思考:

OpenStack 組件繁多,在一個較大的集群內部每個組件內部通過rpc通信頻繁,如果都採用rpc直連調用的方式,連接數會非常地多,開銷大,若有些 server 是單線程的模式,超時會非常的嚴重。

OpenStack 是複雜的分佈式集群架構,會有多個 rpc server 同時工作,假設有 server01,server02,server03 三個server,當 rpc client 要發出rpc請求時,發給哪個好呢?這是問題一。

你可能會說輪循或者隨機,這樣對大家都公平。這樣的話還會引出另一個問題,倘若請求剛好發到server01,而server01剛好不湊巧,可能由於機器或者其他因為導致服務沒在工作,那這個rpc消息可就直接失敗了呀。要知道做為一個集群,高可用是基本要求,如果出現剛剛那樣的情況其實是很尷尬的。這是問題二。

集群有可能根據實際需要擴充節點數量,如果使用直接調用,耦合度太高,不利於部署和生產。這是問題三。

引入消息中間件,可以很好的解決這些問題。

解決問題一:消息只有一份,接收者由AMQP的負載算法決定,默認為在所有Receiver中均勻發送(round robin)。

解決問題二:有了消息中間件做緩衝站,client 可以任性隨意的發,server 都掛掉了?沒有關係,等 server 正常工作后,自己來消息中間件取就行了。

解決問題三:無論有多少節點,它們只要認識消息中間件這一个中介就足夠了。

04. 消息隊列你應該知道什麼?

由於後面,我將實例講解 OpenStack 中如何將 rpc 和 mq broker 結合使用。

而在此之前,你必須對消息隊列的一些基本知識有個概念。

首先,RPC只是定義了一個通信接口,其底層的實現可以各不相同,可以是 socket,也可以是今天要講的 AMQP。

AMQP(Advanced Message Queuing Protocol)是一種基於隊列的可靠消息服務協議,作為一種通信協議,AMQP同樣存在多個實現,如Apache Qpid,RabbitMQ等。

以下是 AMQP 中的幾個必知的概念:

  • Publisher:消息發布者

  • Receiver:消息接收者,在RabbitMQ中叫訂閱者:Subscriber。

  • Queue:用來保存消息的存儲空間,消息沒有被receiver前,保存在隊列中。

  • Exchange:用來接收Publisher發出的消息,根據Routing key 轉發消息到對應的Message Queue中,至於轉到哪個隊列里,這個路由算法又由exchange type決定的。

    exchange type:主要四種描述exchange的類型。

    direct:消息路由到滿足此條件的隊列中(queue,可以有多個): routing key = binding key

    topic:消息路由到滿足此條件的隊列中(queue,可以有多個):routing key 匹配 binding pattern. binding pattern是類似正則表達式的字符串,可以滿足複雜的路由條件。

    fanout:消息路由到多有綁定到該exchange的隊列中。

  • binding :binding是用來描述exchange和queue之間的關係的概念,一個exchang可以綁定多個隊列,這些關係由binding建立。前面說的binding key /binding pattern也是在binding中給出。

在網上找了個圖,可以很清晰地描述幾個名詞的關係。

關於AMQP,有幾下幾點值得注意:

  1. 每個receiver/subscriber 在接收消息前都需要創建binding。
  2. 一個隊列可以有多個receiver,隊列里的一個消息只能發給一個receiver。
  3. 一個消息可以被發送到一個隊列中,也可以被發送到多個多列中。多隊列情況下,一個消息可以被多個receiver收到並處理。Openstack RPC中這兩種情況都會用到。

05. OpenStack中如何使用RPC?

前面鋪墊了那麼久,終於到了講真實應用的場景。在生產中RPC是如何應用的呢?

其他模型我不太清楚,在 OpenStack 中的應用模型是這樣的

至於為什麼要如此設計,前面我已經給出了自己的觀點。

接下來,就是源碼解讀 OpenStack ,看看其是如何通過rpc進行遠程調用的。如若你對此沒有興趣(我知道很多人對此都沒有興趣,所以不浪費大家時間),可以直接跳過這一節,進入下一節。

目前Openstack中有兩種RPC實現,一種是在oslo messaging,一種是在openstack.common.rpc。

openstack.common.rpc是舊的實現,oslo messaging是對openstack.common.rpc的重構。openstack.common.rpc在每個項目中都存在一份拷貝,oslo messaging即將這些公共代碼抽取出來,形成一個新的項目。oslo messaging也對RPC API 進行了重新設計,對多種 transport 做了進一步封裝,底層也是用到了kombu這個AMQP庫。(注:Kombu 是Python中的messaging庫。Kombu旨在通過為AMQ協議提供慣用的高級接口,使Python中的消息傳遞盡可能簡單,併為常見的消息傳遞問題提供經過驗證和測試的解決方案。)

關於oslo_messaging庫,主要提供了兩種獨立的API:

  1. oslo.messaging.rpc(實現了客戶端-服務器遠程過程調用)
  2. oslo.messaging.notify(實現了事件的通知機制)

因為 notify 實現是太簡單了,所以這裏我就不多說了,如果有人想要看這方面內容,可以收藏我的博客(http://python-online.cn) ,我會更新補充 notify 的內容。

OpenStack RPC 模塊提供了 rpc.call,rpc.cast, rpc.fanout_cast 三種 RPC 調用方法,發送和接收 RPC 請求。

  • rpc.call 發送 RPC 同步請求並返回請求處理結果。
  • rpc.cast 發送 RPC 異步請求,與 rpc.call 不同之處在於,不需要請求處理結果的返回。
  • rpc.fanout_cast 用於發送 RPC 廣播信息無返回結果

rpc.call 和 rpc.rpc.cast 從實現代碼上看,他們的區別很小,就是call調用時候會帶有wait_for_reply=True參數,而cast不帶。

要了解 rpc 的調用機制呢,首先要知道 oslo_messaging 的幾個概念

  • transport:RPC功能的底層實現方法,這裡是rabbitmq的消息隊列的訪問路徑

    transport 就是定義你如何訪連接消息中間件,比如你使用的是 Rabbitmq,那在 nova.conf中應該有一行transport_url的配置,可以很清楚地看出指定了 rabbitmq 為消息中間件,並配置了連接rabbitmq的user,passwd,主機,端口。

    transport_url=rabbit://user:passwd@host:5672

    def get_transport(conf, url=None, allowed_remote_exmods=None):
        return _get_transport(conf, url, allowed_remote_exmods,
                              transport_cls=RPCTransport)
  • target:指定RPC topic交換機的匹配信息和綁定主機。

    target用來表述 RPC 服務器監聽topic,server名稱和server監聽的exchange,是否廣播fanout。

    class Target(object):
            def __init__(self, exchange=None, topic=None, namespace=None,
                     version=None, server=None, fanout=None,
                     legacy_namespaces=None):
            self.exchange = exchange
            self.topic = topic
            self.namespace = namespace
            self.version = version
            self.server = server
            self.fanout = fanout
            self.accepted_namespaces = [namespace] + (legacy_namespaces or [])

    rpc server 要獲取消息,需要定義target,就像一個門牌號一樣。

    rpc client 要發送消息,也需要有target,說明消息要發到哪去。

  • endpoints:是可供別人遠程調用的對象

    RPC服務器暴露出endpoint,每個 endpoint 包涵一系列的可被遠程客戶端通過 transport 調用的方法。直觀理解,可以參考nova-conductor創建rpc server的代碼,這邊的endpoints就是 nova/manager.py:ConductorManager()

  • dispatcher:分發器,這是 rpc server 才有的概念 只有通過它 server 端才知道接收到的rpc請求,要交給誰處理,怎麼處理?

    在client端,是這樣指定要調用哪個方法的。

    而在server端,是如何知道要執行這個方法的呢?這就是dispatcher 要乾的事,它從 endpoint 里找到這個方法,然後執行,最後返回。

  • Serializer:在 python 對象和message(notification) 之間數據做序列化或是反序列化的基類。

    主要方法有四個:

    1. deserialize_context(ctxt) :對字典變成 request contenxt.
    2. deserialize_entity(ctxt, entity) :對entity做反序列化,其中ctxt是已經deserialize過的,entity是要處理的。
    3. serialize_context(ctxt) :將Request context變成字典類型
    4. serialize_entity(ctxt, entity) :對entity做序列化,其中ctxt是已經deserialize過的,entity是要處理的。
  • executor:服務的運行方式,單線程或者多線程

    每個notification listener都和一個executor綁定,來控制收到的notification如何分配。默認情況下,使用的是blocking executor(具體特性參加executor一節)

    oslo_messaging.get_notification_listener(transport, targets, endpoints, executor=’blocking’, serializer=None, allow_requeue=False, pool=None)

rpc server 和rpc client 的四個重要方法

  1. reset():Reset service.
  2. start():該方法調用后,server開始poll,從transport中接收message,然後轉發給dispatcher.該message處理過程一直進行,直到stop方法被調用。executor決定server的IO處理策略。可能會是用一個新進程、新協程來做poll操作,或是直接簡單的在一個循環中註冊一個回調。同樣,executor也決定分配message的方式,是在一個新線程中dispatch或是….. *
  3. stop():當調用stop之後,新的message不會被處理。但是,server可能還在處理一些之前沒有處理完的message,並且底層driver資源也還一直沒有釋放。
  4. wait():在stop調用之後,可能還有message正在被處理,使用wait方法來阻塞當前進程,直到所有的message都處理完成。之後,底層的driver資源會釋放。

06. 模仿OpenStack寫rpc調用

模仿是一種很高效的學習方法,我這裏根據 OpenStack 的調用方式,抽取出核心內容,寫成一個簡單的 demo,有對 OpenStack 感興趣的可以了解一下,大部分人也可以直接跳過這章節

以下代碼不能直接運行,你還需要配置 rabbitmq 的連接方式,你可以寫在配置文件中,通過 get_transport 從cfg.CONF 中讀取,也可以直接將其寫成url的格式做成參數,傳給 get_transport 。

簡單的 rpc client

#coding=utf-8
import oslo_messaging
from oslo_config import cfg

# 創建 rpc client
transport = oslo_messaging.get_transport(cfg.CONF, url="")
target = oslo_messaging.Target(topic='test', version='2.0')
client = oslo_messaging.RPCClient(transport, target)

# rpc同步調用
client.call(ctxt, 'test', arg=arg)

簡單的 rpc server

#coding=utf-8
from oslo_config import cfg
import oslo_messaging
import time

# 定義endpoint類
class ServerControlEndpoint(object):
    target = oslo_messaging.Target(namespace='control',
                                   version='2.0')

    def __init__(self, server):
        self.server = server

    def stop(self, ctx):
        if self.server:
            self.server.stop()

            
class TestEndpoint(object):

    def test(self, ctx, arg):
        return arg

    
# 創建rpc server
transport = oslo_messaging.get_transport(cfg.CONF, url="")
target = oslo_messaging.Target(topic='test', server='server1')
endpoints = [
    ServerControlEndpoint(None),
    TestEndpoint(),
]
server = oslo_messaging.get_rpc_server(transport, target,endpoints,executor='blocking')
try:
    server.start()
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    print("Stopping server")

server.stop()
server.wait()

以上,就是本期推送的全部內容,周末两天沒有出門,都在寫這篇文章。真的快掏空了我自己,不過寫完后真的很暢快。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

一次向linux開源社區提交補丁的經歷

背景

在開發過程中,偶然發現了spinand驅動的一個bug,滿懷欣喜地往社區提補丁。這是怎麼樣的一個bug呢?

static int spinand_mtd_read(struct mtd_info *mtd, loff_t from,
                            struct mtd_oob_ops *ops)
{
        ......
        nanddev_io_for_each_page(nand, from, ops, &iter) {
                ......
                ret = spinand_read_page(spinand, &iter.req, enable_ecc);
                if (ret < 0 && ret != -EBADMSG)     /* 讀取數據出錯 */
                        break;

                if (ret == -EBADMSG) {
                        /* -EBADMSG 返回表示壞塊 */
                        ecc_failed = true;
                        mtd->ecc_stats.failed++;
                        ret = 0;
                } else {
                        /* 出現位翻轉或者讀取正常,則記錄歷史位翻轉最大值 */
                        mtd->ecc_stats.corrected += ret;
                        max_bitflips = max_t(unsigned int, max_bitflips, ret);
                }

                ops->retlen += iter.req.datalen; 
                ops->oobretlen += iter.req.ooblen;
        }

        if (ecc_failed && !ret)
                ret = -EBADMSG;

        return ret ? ret : max_bitflips;
}

代碼邏輯如下:

  1. 遍歷讀取每一個page
  2. 如果讀出錯則直接返回
  3. 如果出現壞塊,則置位ecc_failed,在函數最後會檢查此標誌
  4. 如果出現位翻轉,則暫存最大位翻轉的bit位數量
  5. 全部讀取完后,如果有置位ecc_failed,則返回壞塊錯誤碼;如果出現位翻轉,則返回最大位翻轉;否則返回0,表示正常

問題出在於,如果剛好最後一次讀取出現位翻轉,此時ret != 0就直接退出循環,此時會導致壞塊標識無效,且返回最後的位翻轉量而非歷史位翻轉最大值。這是代碼不嚴謹的地方。

第一次提交

修改補丁如下,補丁邏輯不再解釋。

In function spinand_mtd_read, if the last page to read occurs bitflip,
this function will return error value because veriable ret not equal to 0.

Signed-off-by: liaoweixiong <liaoweixiong@allwinnertech.com>
---
 drivers/mtd/nand/spi/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
index 556bfdb..6b9388d 100644
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -511,12 +511,12 @@ static int spinand_mtd_read(struct mtd_info *mtd, loff_t from,
        if (ret == -EBADMSG) {
            ecc_failed = true;
            mtd->ecc_stats.failed++;
-           ret = 0;
        } else {
            mtd->ecc_stats.corrected += ret;
            max_bitflips = max_t(unsigned int, max_bitflips, ret);
        }
 
+       ret = 0;
        ops->retlen += iter.req.datalen;
        ops->oobretlen += iter.req.ooblen;
    }

21:13分發出的郵件,21:45分陸續收到兩個回復:

<maintainer A>:

Actually, that's exactly what the MTD core expects (see [1]), so you're
the one introducing a regression here.
<maintainer B>:

To me it looks like the patch description is somewhat incorrect, but the 
fix itself looks okay, unless I'm getting it wrong.

In case of the last page containing bitflips (ret > 0), 
spinand_mtd_read() will return that number of bitflips for the last 
page. But to me it looks like it should instead return max_bitflips like 
it does when the last page read returns with 0.

以及隔天回復

<maintainer A>:

Oh, you're right. liaoweixiong, can you adjust the commit message
accordingly?

好吧,問題出在與我沒把問題描述清楚,改改再提交

第二次提交

只改了comment和補丁標題:

Subject: [PATCH v2] mtd: spinand: read return badly if the last page has bitflips

In case of the last page containing bitflips (ret > 0), 
spinand_mtd_read() will return that number of bitflips for the last 
page. But to me it looks like it should instead return max_bitflips like 
it does when the last page read returns with 0.

然後嘩啦啦收到兩個Reviewed-by,附帶一個建議:

Reviewed-by: <maintainer B>

This should probably be resent with the following tags:

Cc: stable@vger.kernel.org
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI 
NANDs")

得,再提交一次吧

第三次提交

此時的我提交補丁到社區經驗並不多,Maintainer讓我resend,我就忐忑開始胡思亂想了:

版本號需要累加么?該怎麼標記是重新發送?有兩個maintainer已經”認可”了我的補丁(reviewed-by),我改怎麼體現到新的郵件中?

仔細想想內容並沒改,因此不需要累加版本號;查詢前人提交,在郵件標題可以加上RESEND字樣;搜索含RESEND字樣的前人郵件,剛好找到一個在maintainer reviewed后resend為acked,寫在signed-off-by區。

OK,確定下來就重新發吧

Subject: [RESEND PATCH v2] mtd: spinand: read return badly if the last page has bitflips

......
Signed-off-by: liaoweixiong <liaoweixiong@allwinnertech.com>
Acked-by: <maintainer A>
Acked-by: <maintainer B>
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

很快,就挨批了…

第四次提交

晚上10點多,收到回復:

<maintainer B>

Why did you change our Reviewed-by tags to Acked-by tags?

額…我也是看別人這麼做我才這麼做的,大佬生氣了!趕緊補救

......
Reviewed-by: <maintainer A>
Reviewed-by: <maintainer B>
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

第五次提交

埋下的坑終究是要踩的,很快,再次挨批了

<maintainer C>

This is not the correct way to submit patches for inclusion in the
stable kernel tree.  Please read:
    https://www.kernel.org/doc/html/latest/process/stable-kernel-rules.html
for how to do this properly.
<maintainer A>

FYI, you should not send the patch to stable@vger.kernel.org, but 
instead, as I said in my other reply, add the tag "Cc: 
stable@vger.kernel.org". See "Option 1" in the document Greg referred to.

小白趕緊狠補基礎操作規範…

第六次提交

......
Reviewed-by: <maintainer A>
Reviewed-by: <maintainer B>
Cc: stable@vger.kernel.org
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

總結

哎,我只是挪了一行代碼的位置而已啊,Maintainer嚴審下,我竟然提交了6次!6次!突然感覺心好累。

累歸累,問題總結還是需要的

  1. 新手不具備提交代碼的一些常識,包括 a) 提交中各個tag的含義,在什麼時候加這些tag,例如Reviewed-by和Acked-by的差別 b) 提交補丁到stable的注意事項
  2. 對補丁的問題描述不夠仔細清楚,導致 無法理解,幸好 幫我澄清了

解決方法:

  1. linux提交有規範文檔的,抽時間擼一遍,並翻譯發博客
  2. 在發補丁之前,讓身邊的人幫忙看一下補丁說明是否足夠清晰

希望我的經歷能幫助到正在或者準備向Linux內核開源社區的小夥伴

【精選推薦文章】

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

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

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

前端必備性能知識 – http2.0

前端開發中,性能是一定繞不開的,今天就來說一下前後台通信中最重要的一個通道–HTTP2.0

 最開始的通訊協議叫http1.0,作為始祖級的它,定義了最基本的數據結構,請求頭和請求體,以及每一個字段的含義,它順應了當時的互聯網需求,首次實現了瀏覽器與後端的交互,但它有一個時代烙印,就是短連接,每次請求就會建立一個TCP連接,三次握手四次揮手,用完就關閉,假如瀏覽器有300個請求,那麼它就建立了300個連接,這樣就給服務端帶來的很大的壓力,即使它只是一個很小很小的請求,後來,大家發現這樣不行啊,內容一多,服務端就頂不住了,然後就開始想辦法擴展它,

這樣http1.1就出現了,建立了長連接,通過keepalived開啟連接復用,什麼意思?還拿這300個請求來說,瀏覽器默認一次支持6個請求,當這6個請求結束以後,會繼續復用這6個請求,每個請求都是異步的,不會讓這6個連接閑着,直到300個請求結束。

好像這樣就解決了問題,可是細想一下好像不對,硬件更新快,後端硬件性能提升很快,它可以支持很多線程進行計算,但瀏覽器還是6個,那豈不是白白浪費了硬件設備,所以http2.0就出來了,多路復用的單一長連接

什麼意思呢,看這個

 

 

http1.1中,當建立連接,並響應完以後,會繼續復用該條連接去請求資源,但這6條請求是不變的,只不過復用了而已,在2.0中就不一樣了,只用這一條長連接,請求多個資源,一下子就減少了5條連接(包括每次連接時的三次握手和四次揮手),還有tcp慢啟動帶來的網絡延時,而它之所以一個連接上能放這麼多內容,底層是由於它以數據幀的形式進行傳輸的,一個數據包中包含多個資源。

http頭部壓縮和緩存

我們在請求內容的時候,有時候會出現你請求的內容很少,但是請求頭字段的體積比內容的體積都大的情況,而且每次請求就帶着這個相同請求頭,一旦1萬個請求過來了,壓力就明顯了,下面是例子。

壓縮以後減少了一般的體積,而且它還會緩存請求頭,因為每次的請求頭都一樣,所以在底層,它將這個請求頭用一個符號比如1來表示去發請求,而後端也會去解析這個1進而進行處理,所以原來是一大段的字段內容,而現在就是一個符號就表示出來了。

兼容http1.1,基於https進行部署,服務端主動推送內容

如果發現瀏覽器不支持2.0,則自動向下兼容

部署升級,則如下

瀏覽器與nginx交互用https進行加密傳輸,反向代理nginx與服務器的comact是明文傳輸

 

【精選推薦文章】

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

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

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

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

[simple-orm-mybaits]基於Mybatis的ORM封裝介紹

目錄

  • 前言
  • ORM框架現狀
  • Mybatis優缺點
  • simple-orm-mybatis設計思路介紹
  • simple-orm-mybatis使用說明
  • simple-orm-mybatis實際使用
  • 推薦最佳實踐
  • 更多說明

simple-orm-mybatis在github開源,直接跳轉查看,歡迎大家fork和star。有什麼建議也可以提出,我會儘快修復或實現。

前言

最早接觸Java的web開發框架就是SSH,其中的H就是Hibernate。Hibernate作為最出名的Java的ORM框架,現在的版本已經到了5.3.10.Final,6.0.0.Alpha2。圍繞數據持久化或者DAL層也發展了多種工具,Hibernate Validator目前也是在很多的企業框架中被用作數據有效性檢查。
接下來還有用過JPA來實現ORM。JPA全稱Java Persistence API,是JSR-220(EJB3.0)規範的一部分,在JSR-220中規定實體對象(EntityBean)由JPA進行支持。在我的使用中,實際上底層實現仍然使用了Hibernate,只是標註或者操作類都是使用了JPA的接口標準而沒有直接使用Hibernate。
Hibernate具有強大的ORM封裝能力,極大的簡化了CUD的操作,而且無需做太多的配置,使用標準註解就能解決很多問題。簡單的查詢操作也不在話下,多級關聯、延遲查詢等豐富特性基本上也可以說是極大覆蓋了開發過程的各種需求。但是實際上在這麼多團隊中,很多人的反饋是這樣的:“Hibernate的ORM確實很方便,但是在一些特殊條件下很難用,比如複雜查詢就很難控制語句生成的效率”。Hibernate有三種查詢方式:HQL、Criteria、Native Sql。確實在複雜查詢下,如果使用Criteria,確實能夠拼出想要的語句,但是可能對於其中的方法要非常熟悉,學習曲線很高,沒辦法做到團隊中每個成員都能數量輕易的掌握,而且後期的審核很困難無法直接看清邏輯。HQL和NativeSql對我的感覺,似乎回到了JSP時代,HTML和Java代碼混寫,很難忍受。這時候大家找到了另外的框架Mybatis。

ORM框架現狀

截止(2019/5/27)Mybatis的Star是10806,UsedBy140381;Hibernate的Star是3817,UsedBy157879。看使用量Hibernate和Mybatis其實已經差不多了,實際企業開發中,Mybatis可能還會更多一些。

Mybatis優缺點

Mybatis放棄了Hibernate的強封裝,主要包含了映射的部分,而且放棄了自動解析生成Sql,而直接使用用戶XML配置Sql的方式,只是在Sql的拼接上提供了一些標籤來避免重複代碼。這樣的討巧之處在於:

  • 門檻降低:開發人員不需要了解框架的內部語法,只需要了解Sql語法即可。
  • 可讀性提高:審核代碼時,很容易的就能看清楚多級關聯以及關聯使用的條件、語法是否正確。
  • 維護方便:當查詢語法出錯時,Mybatis只需要修改XML,而Hibernate則需要修改代碼重新編譯,操作相對複雜。

缺點也有幾處:

  • 簡單操作複雜:由於放棄了自動解析生成Sql,所以普通的CUD也需要手動編寫Sql
  • 實體映射複雜:必須在XML文件中配置大量的字段映射
  • SqlMapper中的sql id風險:由於XML中的sql id是一個字符串,只能複製粘貼出來,所以出錯的幾率也比較大。

實際上,針對上面缺點,Mybatis也提供了解決方案。
先說sql id的,在新的Mybatis中,實際上已經採用了面向接口的編碼方式,如下面的例子:

接口類

public interface UserMapper {
    public User findUserById(Integer id);
}

mapper的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="com.software5000.mapper.dao.UserMapper">
    <select id="findUserById" resultType="com.software5000.entity.User" parameterType="int" >
        select * from user where id =#{id}
    </select>
</mapper>

這樣就可以直接調用sql語句:

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.findUserById(1);

再說映射複雜的,Mybatis提供了MyBatis Generator作為解決方案,一個命令生成下列內容:

  • 匹配表結構的Java POJOs。可能包括:
    • 表結構中主鍵字段(如果存在主鍵)
    • 表結構中的非主鍵字段(除了BLOB字段)
    • 表結構中的BLOB字段(如果有BLOB字段)
    • 動態查詢、更新和刪除的接口類

自動生成也能夠合理的生成類之間的繼承關係。注意生成器也能夠定義生產不同的POJO層次 – 例如如果你想要可以為每個表格生成一個單獨的領域對象。

  • Mybatis/iBATIS兼容的SQL MAP的XML文件。按照配置針對每個表生成簡單的增刪改查的SQL方法。生成的SQL包括:
    • 插入
    • 按主鍵更新
    • 使用example更新 (使用動態的where條件)
    • 按主鍵刪除
    • 使用example刪除 (使用動態的where條件)
    • 按主鍵查詢
    • 使用example查詢 (使用動態的where條件)
    • 使用example統計

生成的SQL語句取決於表格的結構(例如,表格如果沒有主鍵,就不會生成按主鍵更新的方法)

Java客戶端生成類能夠合理使用上面的對象。生成的Java客戶端類也是有選擇性的。

  • 使用Mybatis 3.x的會生成:
    • 一個mapper接口和XML中的語句id相同,用於直接調用。
  • 使用iBATIS 2.x的會生成:
    • 適用於Spring框架的DAO層
    • 使用IBATIS SQL映射API的DAO層。這些DAO可以使用兩種方式:使用構造函數提供SqlMapClient,或者通過注入方式提供。
    • DAOs that conform to the iBATIS DAO Framework (an optional part of iBATIS, this framework is now deprecated and we suggest that you use the Spring framework instead)
    • iBATIS的DAO框架符合的DAO層(iBATIS的一個額外部分,但是現在這個框架已經失效了,所以建議使用Spring DAO的框架代替)

但是,我對於MyBatis Generator的態度是堅定的反對。原因是我認為自動生成違反了簡單的原則,“如無必要,勿增實體”。自動生成的可復用性和可讀性一定是比較差的。我覺得最好的代碼就是不存在的代碼。因此我希望能夠有一個簡單框架在Mybatis之上,接入簡單無侵入,能夠提供基本的增刪改查方法。這就是下面的 simple-orm-mybatis 。

simple-orm-mybatis設計思路介紹

simple-orm-mybatis設計的初衷就是希望提供一個簡單無侵入,而且無需編寫或者生成任何代碼就可以使用直接操作對象的方式來進行增刪改查的操作。要實現這樣的要求,主要是兩個主要技術點:

  1. 利用反射機制對應實體對象與數據庫結構
  2. 解析對象並且生成對應操作的SQL語句

第一點核心就是反射,設計要點如下:

  • Java對象與數據庫結構的對應規則
  • 虛字段(無數據庫對應字段)的處理
  • 考慮多層繼承的對象解析
  • 值的設置與獲取方式

第二點核心在於SQL解析,設計要點如下:

  • 根據不同入口區分基本CRUD語法結構
  • 字段(列名)需要分為值字段與查詢字段兩類
  • 更新操作的時候,Null,空,有值的區分
  • 支持多種匹配操作符(大於、小於、Like等)

simple-orm-mybatis使用說明

  1. 首先引入依賴,項目已經發布在Maven Central上,可以直接引入。
<dependency>
    <groupId>com.software5000</groupId>
    <artifactId>simple-orm-mybatis</artifactId>
    <version>1.0.2</version>
</dependency>
  1. 接着需要將 BaseDaoMapper.xml文件放在你的 mapper 的掃描文件夾內。

  2. 最後需要在你的代碼中添加一個 BaseDao 實現類,用於提供數據庫操作服務(注意:需要在spring的掃描包內,因為需要注入某些屬性),整個複製即可,類名可以改為你自己需要的名字
import com.software5000.base.BaseDao;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 整個類以 <code>org.mybatis:mybatis-spring:2.0.0</code> 的 <code>org.mybatis.spring.supportSqlSessionDaoSupport</code>
 * 為參考編寫而成
 */
@Component
public class MyBaseDao extends BaseDao {
    private SqlSessionTemplate sqlSessionTemplate;
    
    public MyBaseDao() {
            // 在默認構造函數中設置 數據庫是否蛇形, 數據庫格式大小寫, 通用忽略的字段名稱
            this.initConfig(true,false,"serialVersionUID");
    }
        
    @Resource
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
            this.sqlSessionTemplate = this.createSqlSessionTemplate(sqlSessionFactory);
        }
    }

    protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Override
    public SqlSession getSqlSession() {
        return this.sqlSessionTemplate;
    }
}

simple-orm-mybatis實際使用

這裏給了一個單元測試的例子,實際一般是在service層直接使用,無需添加任何代碼。
示例中的 DailyRecord是一個普通實體類,沒有任何繼承。

// 獲取啟動類,加載配置,確定裝載 Spring 程序的裝載方法,它回去尋找 主配置啟動類(被 @SpringBootApplication 註解的)
@SpringBootTest
// 讓 JUnit 運行 Spring 的測試環境, 獲得 Spring 環境的上下文的支持
@RunWith(SpringRunner.class)
public class BaseDaoTest {

    @Autowired
    private MyBaseDao myBaseDao;

    @Test
    public void testBaseDao(){
        DailyRecord dailyRecord = new DailyRecord();
        List<DailyRecord> dailyRecords = myBaseDao.selectEntity(dailyRecord);
        System.out.println(dailyRecords.size());
    }
}

推薦最佳實踐

雖說整體的設計基於無侵入,基本沒有任何前提,但是還是有一些推薦的實踐希望大家能夠去嘗試:

1、 數據結構設計建議包含int類型的自增主鍵設計,名稱叫id。
原因:很多時候我們的業務主鍵是有一套特定規則,但是很有可能面對修改,所以底層關聯主鍵統一使用id關聯在後期面對修改時影響較小。
弊端:
1. 如mysql中int最長2147483647,考慮到自增id可能會有跳過不連續的情況,需要考慮實際可用的存儲量。不過大部分的業務表是到不了這個數量級的。
2. mysql的InnoDB有自增主鍵鎖表的問題,超大併發插入可能會影響效率。不過在5.1.22有提供了改進的策略。

2、 數據結構與實體結構盡量符合統一轉換規則。
原因:這樣研發過程中,實體與數據庫的映射會比較簡單,無需大量的自定義。建議的規則有三類:
– 兩邊都使用駝峰。
– 實體使用駝峰,數據庫使用全小寫蛇形
– 實體使用駝峰,數據庫使用全大寫蛇形

3、 代碼中實體的字段類型使用封裝類型而不是基本類型
原因:基本類型是有默認值存在,而數據庫中我們一旦設置字段可空,就會有NULL值存在。所以建議全部使用封裝類型。下面附上各種基本類型的默認值

基本類型 默認值
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char ‘\u0000’
boolean false

4、 分頁算法
原因:分頁推薦使用PageHelper,是利用Mybatis的底層插件機制修改Sql語句,也是無侵入。

更多說明

更多說明,可以參考github上的wiki頁面

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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