SparkStreaming 尚硅谷

第1章 SparkStreaming 概述

在这里插入图片描述
在这里插入图片描述

1.1 Spark Streaming 是什么

在这里插入图片描述
在这里插入图片描述

1.2 Spark Streaming 的特点

➢ 易用
➢ 容错
➢ 易整合到 Spark 体系

1.3 Spark Streaming 架构

1.3.1 架构图

➢ 整体架构图
在这里插入图片描述
➢ SparkStreaming 架构图
在这里插入图片描述

1.3.2 背压机制

自动调整,接受的多处理的慢。接受的少处理的快的情况。
在这里插入图片描述

第 2 章 Dstream 入门

2.1 WordCount 案例实操

➢ 需求:使用 netcat 工具向 9999 端口不断的发送数据,通过 SparkStreaming 读取端口数据并
统计不同单词出现的次数

  1. 添加依赖
<dependency>
 <groupId>org.apache.spark</groupId>
 <artifactId>spark-streaming_2.12</artifactId>
 <version>3.0.0</version>
</dependency>
  1. 编写代码
object StreamWordCount {
 def main(args: Array[String]): Unit = {
 //1.初始化 Spark 配置信息
 val sparkConf = new 
SparkConf().setMaster("local[*]").setAppName("StreamWordCount")
 //2.初始化 SparkStreamingContext
 val ssc = new StreamingContext(sparkConf, Seconds(3))
 //3.通过监控端口创建 DStream,读进来的数据为一行行
 val lineStreams = ssc.socketTextStream("linux1", 9999)
 //将每一行数据做切分,形成一个个单词
 val wordStreams = lineStreams.flatMap(_.split(" "))
 //将单词映射成元组(word,1)
 val wordAndOneStreams = wordStreams.map((_, 1))
 //将相同的单词次数做统计
 val wordAndCountStreams = wordAndOneStreams.reduceByKey(_+_)
 //打印
 wordAndCountStreams.print()
 //启动 SparkStreamingContext
 ssc.start()
 ssc.awaitTermination()
 } }
package com.atguigu.bigdata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreaming01_WordCount {

    def main(args: Array[String]): Unit = {

        // TODO 创建环境对象
        // StreamingContext创建时,需要传递两个参数
        // 第一个参数表示环境配置
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        // 第二个参数表示批量处理的周期(采集周期)
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        // TODO 逻辑处理
        // 获取端口数据
        val lines: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)

        val words = lines.flatMap(_.split(" "))

        val wordToOne = words.map((_,1))

        val wordToCount: DStream[(String, Int)] = wordToOne.reduceByKey(_+_)

        wordToCount.print()

        // 由于SparkStreaming采集器是长期执行的任务,所以不能直接关闭
        // 如果main方法执行完毕,应用程序也会自动结束。所以不能让main执行完毕
        //ssc.stop()
        // 1. 启动采集器
        ssc.start()
        // 2. 等待采集器的关闭
        ssc.awaitTermination()
    }
}

  1. 启动程序并通过 netcat 发送数据:
nc -lk 9999

运行程序

2.2 WordCount 解析

Discretized Stream 是 Spark Streaming 的基础抽象,代表持续性的数据流和经过各种 Spark 原语操作后的结果数据流。在内部实现上,DStream 是一系列连续的 RDD 来表示。每个 RDD 含有一段时间间隔内的数据。
在这里插入图片描述

第 3 章 DStream 创建

3.1 RDD 队列

不断的往队列中 放入数据不断的从队列中取数据,测试时使用

3.1.1 用法及说明

在这里插入图片描述

3.1.2 案例实操

在这里插入图片描述

object RDDStream {
 def main(args: Array[String]) {
 //1.初始化 Spark 配置信息
 val conf = new SparkConf().setMaster("local[*]").setAppName("RDDStream")
 //2.初始化 SparkStreamingContext
         // StreamingContext创建时,需要传递两个参数
        // 第一个参数表示环境配置
         // 第二个参数表示批量处理的周期(采集周期)
 val ssc = new StreamingContext(conf, Seconds(4))
 //3.创建 RDD 队列
 val rddQueue = new mutable.Queue[RDD[Int]]()
 //4.创建 QueueInputDStream
 val inputStream = ssc.queueStream(rddQueue,oneAtATime = false)
 //5.处理队列中的 RDD 数据
 val mappedStream = inputStream.map((_,1))
 val reducedStream = mappedStream.reduceByKey(_ + _)
 //6.打印结果
 reducedStream.print()
 //7.启动任务
 ssc.start()
//8.循环创建并向 RDD 队列中放入 RDD
 for (i <- 1 to 5) {
 rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
 Thread.sleep(2000)
 }
 ssc.awaitTermination()
 } }

3.2 自定义数据源

3.2.1 用法及说明
需要继承 Receiver,并实现 onStart、onStop 方法来自定义数据源采集。
3.2.2 案例实操

  1. 使用自定义的数据源采集数据
package com.atguigu.bigdata.spark.streaming

import java.util.Random

import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.dstream.ReceiverInputDStream
import org.apache.spark.streaming.receiver.Receiver
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable

object SparkStreaming03_DIY {

    def main(args: Array[String]): Unit = {

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val messageDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver())
        messageDS.print()

        ssc.start()
        ssc.awaitTermination()
    }
    /*
    自定义数据采集器
    1. 继承Receiver,定义泛型, 传递参数
    2. 重写方法
     */
    class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY) {
        private var flg = true
        override def onStart(): Unit = {
            new Thread(new Runnable {
                override def run(): Unit = {
                    while ( flg ) {
                        val message = "采集的数据为:" + new Random().nextInt(10).toString
                        //存储数据,底层自动封装为指定的StorageLevel.MEMORY_ONLY
                        store(message)
                        Thread.sleep(500)
                    }
                }
            }).start()
        }

        override def onStop(): Unit = {
            flg = false;
        }
    }
}

2)需求:自定义数据源,实现监控某个端口号,获取该端口号内容。
socketTextStream的底层实现

 val lines: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)
class CustomerReceiver(host: String, port: Int) extends 
Receiver[String](StorageLevel.MEMORY_ONLY) {
 //最初启动的时候,调用该方法,作用为:读数据并将数据发送给 Spark
 override def onStart(): Unit = {
 new Thread("Socket Receiver") {
 override def run() {
 receive()
 }
 }.start()
 }
 //读数据并将数据发送给 Spark
 def receive(): Unit = {
 //创建一个 Socket
 var socket: Socket = new Socket(host, port)
 //定义一个变量,用来接收端口传过来的数据
 var input: String = null
 //创建一个 BufferedReader 用于读取端口传来的数据
 val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, 
StandardCharsets.UTF_8))
 //读取数据
 input = reader.readLine()
 //当 receiver 没有关闭并且输入数据不为空,则循环发送数据给 Spark
 while (!isStopped() && input != null) {
 store(input)
 input = reader.readLine()
 }
 //跳出循环则关闭资源
 reader.close()
 socket.close()
 //重启任务
 restart("restart")
 }
 override def onStop(): Unit = {}
}

3.3 Kafka 数据源(面试、开发重点)

3.3.1 版本选型

在这里插入图片描述

3.3.2 Kafka 0-8 Receiver 模式(当前版本不适用)

3.3.3 Kafka 0-8 Direct 模式(当前版本不适用)

3.3.4 Kafka 0-10 Direct 模式

1)需求:通过 SparkStreaming 从 Kafka 读取数据,并将读取过来的数据做简单计算,最终打印
到控制台。
2)导入依赖

<dependency>
 <groupId>org.apache.spark</groupId>
 <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
 <version>3.0.0</version>
</dependency>
<dependency>
 <groupId>com.fasterxml.jackson.core</groupId>
 <artifactId>jackson-core</artifactId>
 <version>2.10.1</version>
 </dependency>

3)编写代码

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{
  ConsumerStrategies, KafkaUtils,
  LocationStrategies
}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object DirectAPI {
  def main(args: Array[String]): Unit = {
    //1.创建 SparkConf
    val sparkConf: SparkConf = new
        SparkConf().setAppName("ReceiverWordCount").setMaster("local[*]")
    //2.创建 StreamingContext,指定每个隔几秒计算一次
    val ssc = new StreamingContext(sparkConf, Seconds(3))
    //3.定义 Kafka 参数
    val kafkaPara: Map[String, Object] = Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG ->
      "kafka1:9092,kafka2:9092,kafka3:9092",
      ConsumerConfig.GROUP_ID_CONFIG -> "groupId",
      "key.deserializer" ->
        "org.apache.kafka.common.serialization.StringDeserializer",
      "value.deserializer" ->
        "org.apache.kafka.common.serialization.StringDeserializer"
    )
    //4.读取 Kafka 数据创建 DStream
    val kafkaDStream: InputDStream[ConsumerRecord[String, String]] =
      KafkaUtils.createDirectStream[String, String](ssc,
       //由哪个Executor负责采集数据,由框架自己选择
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](Set("topicName"), kafkaPara))
    //5.将kafka每条消息的 value 取出
    val valueDStream: DStream[String] = kafkaDStream.map(record => record.value())
    //6.计算 WordCount
    valueDStream.flatMap(_.split(" "))
      .map((_, 1))
      .reduceByKey(_ + _)
      .print()
    //7.开启任务
    ssc.start()
    ssc.awaitTermination()
  }
}

第 4 章 DStream 转换

在这里插入图片描述

与Spark core 转化操作不同的是 DStream转换操作有状态的概念。
所谓的有状态和无状态其实就是看是否保存了某个周期的计算结果,如果保存就是有状态,如果不保存就是无状态。

无状态:
在这里插入图片描述

有状态:
在这里插入图片描述
在这里插入图片描述

4.1 无状态转化操作

在这里插入图片描述

4.1.1 Transform

transform方法可以将底层RDD获取到后进行操作
transform使用场景:

  1. DStream功能不完善
  2. 需要代码周期性的执行
    在这里插入图片描述
package com.atguigu.bigdata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreaming06_State_Transform {

    def main(args: Array[String]): Unit = {

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val lines = ssc.socketTextStream("localhost", 9999)

        // transform方法可以将底层RDD获取到后进行操作
        // transform使用场景
        // 1. DStream功能不完善
        // 2. 需要代码周期性的执行

        // Code : Driver端
        val newDS: DStream[String] = lines.transform(
            rdd => {
                // Code : Driver端,(周期性执行)
                rdd.map(
                    str => {
                        // Code : Executor端
                        str
                    }
                )
            }
        )
        // Code : Driver端
        val newDS1: DStream[String] = lines.map(
            data => {
                // Code : Executor端
                data
            }
        )

        ssc.start()
        ssc.awaitTermination()
    }

}

4.1.2 join

在这里插入图片描述

package com.atguigu.bigdata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreaming06_State_Join {

    def main(args: Array[String]): Unit = {

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(5))

        val data9999 = ssc.socketTextStream("localhost", 9999)
        val data8888 = ssc.socketTextStream("localhost", 8888)

        val map9999: DStream[(String, Int)] = data9999.map((_,9))
        val map8888: DStream[(String, Int)] = data8888.map((_,8))

        // 所谓的DStream的Join操作,其实就是两个RDD的join
        val joinDS: DStream[(String, (Int, Int))] = map9999.join(map8888)

        joinDS.print()

        ssc.start()
        ssc.awaitTermination()
    }
}

在这里插入图片描述

4.2 有状态转化操作

4.2.1 UpdateStateByKey

在这里插入图片描述

package com.atguigu.bigdata.spark.streaming

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreaming05_State {

    def main(args: Array[String]): Unit = {

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))
        //需要设定检查点路径
        ssc.checkpoint("D:/cp")

        // 无状态数据操作,只对当前的采集周期内的数据进行处理
        // 在某些场合下,需要保留数据统计结果(状态),实现数据的汇总
        // 使用有状态操作时,需要设定检查点路径
        val datas = ssc.socketTextStream("localhost", 9999)
        // map 只是转换操作不许状态
        val wordToOne = datas.map((_,1))
        //reduceByKey直接出结果,无状态操作
        //val wordToCount = wordToOne.reduceByKey(_+_)
		 // 使用 updateStateByKey 来更新状态,统计从运行开始以来单词总的次数
        // updateStateByKey:根据key对数据的状态进行更新
        // 传递的参数中含有两个值
        // 第一个值表示相同的key的value数据
        // 第二个值表示缓存区相同key的value数据
        val state = wordToOne.updateStateByKey(
            ( seq:Seq[Int], buff:Option[Int] ) => {
                //缓冲区有可能有值也有可能没有值
                //将这个次时间周期计算的值和上次时间周期的值(缓冲区的值)相加
                val newCount = buff.getOrElse(0) + seq.sum
                //将本周期计算的值放入缓冲区
                Option(newCount)
            }
        )
        state.print()
        ssc.start()
        ssc.awaitTermination()
    }
}

2) 启动程序并向 9999 端口发送数据

4.2.2 WindowOperations

Window Operations 可以设置窗口的大小和滑动窗口的间隔来动态的获取当前 Steaming 的允许状态。所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长。

➢ 窗口时长:计算内容的时间范围;
➢ 滑动步长:隔多久触发一次计算。

注意:这两者都必须为采集周期大小的整数倍。

在这里插入图片描述

在这里插入图片描述

val ipDStream = accessLogsDStream.map(logEntry => (logEntry.getIpAddress(), 1))
val ipCountDStream = ipDStream.reduceByKeyAndWindow(
 {(x, y) => x + y},
 {(x, y) => x - y},
 Seconds(30),
 Seconds(10))
 //加上新进入窗口的批次中的元素 //移除离开窗口的老批次中的元素 //窗口时长// 滑动步长

在这里插入图片描述

val ipDStream = accessLogsDStream.map{entry => entry.getIpAddress()}
val ipAddressRequestCount = ipDStream.countByValueAndWindow(Seconds(30), 
Seconds(10)) 
val requestCount = accessLogsDStream.countByWindow(Seconds(30), Seconds(10))

代码示例:
无状态的窗口操作

package com.atguigu.bigdata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreaming06_State_Window {

    def main(args: Array[String]): Unit = {

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        //设置采集周期
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val lines = ssc.socketTextStream("localhost", 9999)
        val wordToOne = lines.map((_,1))

        // 窗口的范围应该是采集周期的整数倍
        // 窗口可以滑动的,但是默认情况下,一个采集周期进行滑动
        // 这样的话,可能会出现重复数据的计算,为了避免这种情况,可以改变滑动的滑动(步长)
        //每隔6秒采集6秒内的数据:滚动窗口
        val windowDS: DStream[(String, Int)] = wordToOne.window(Seconds(6), Seconds(6))

        val wordToCount = windowDS.reduceByKey(_+_)

        wordToCount.print()

        ssc.start()
        ssc.awaitTermination()
    }

}

有状态的窗口操作
reduceByKeyAndWindow 适合 当窗口范围比较大,但是滑动幅度比较小,那么可以采用增加数据和删除数据的方式,无需重复计算,提升性能。

package com.atguigu.bigdata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreaming06_State_Window1 {

    def main(args: Array[String]): Unit = {

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        //计算周期
        val ssc = new StreamingContext(sparkConf, Seconds(3))
        //设置检查点
        ssc.checkpoint("cp")

        val lines = ssc.socketTextStream("localhost", 9999)
        val wordToOne = lines.map((_,1))

        // reduceByKeyAndWindow : 当窗口范围比较大,但是滑动幅度比较小,那么可以采用增加数据和删除数据的方式
        // 无需重复计算,提升性能。
        val windowDS: DStream[(String, Int)] =
            wordToOne.reduceByKeyAndWindow(
                (x:Int, y:Int) => { x + y},//窗口中增加的数据
                (x:Int, y:Int) => {x - y},//窗口中减少的数据
                Seconds(9), Seconds(3))

        windowDS.print()

        ssc.start()
        ssc.awaitTermination()
    }

}

第 5 章 DStream 输出

DStream必须有输出操作,否则会报错。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第 6 章 优雅关闭

在这里插入图片描述

  • 强制关闭直接关闭,不管计算节点是否还有数据没有计算完,
  • 优雅的关闭时先不在接受新的数据,等待计算节点中数据都计算完毕后再关闭。

    关闭是关闭所有的计算节点
    代码示例
package com.atguigu.bigdata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext, StreamingContextState}

object SparkStreaming08_Close {

    def main(args: Array[String]): Unit = {

        /*
           线程的关闭:
           val thread = new Thread()
           thread.start()

           thread.stop(); // 强制关闭

         */

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val lines = ssc.socketTextStream("localhost", 9999)
        val wordToOne = lines.map((_,1))

        wordToOne.print()

        ssc.start()

        // 如果想要关闭采集器,那么需要创建新的线程
        // 而且需要在第三方程序中增加关闭状态,关闭线程中通过状态值判断是否需要关闭
        new Thread(
            new Runnable {
                override def run(): Unit = {
                    // 优雅地关闭
                    // 计算节点不在接收新的数据,而是将现有的数据处理完毕,然后关闭
                    // Mysql : Table(stopSpark) => Row => data
                    // Redis : Data(K-V)
                    // ZK    : /stopSpark
                    // HDFS  : /stopSpark
                    /*
                    while ( true ) {
                        if (true) {
                            // 获取SparkStreaming状态
                            val state: StreamingContextState = ssc.getState()
                            if ( state == StreamingContextState.ACTIVE ) {
                            // //优雅的关闭时先不在接受新的数据,等待计算节点中数据都计算完毕后再关闭。
                                ssc.stop(true, true)
                            }
                        }
                        Thread.sleep(5000)
                    }
                     */
                    //运行5秒关闭
                    Thread.sleep(5000)
                    val state: StreamingContextState = ssc.getState()
                    //先判断spark的环境状态
                    if ( state == StreamingContextState.ACTIVE ) {
                        //优雅的关闭时先不在接受新的数据,等待计算节点中数据都计算完毕后再关闭。
                        ssc.stop(true, true)
                    }
                    System.exit(0)
                }
            }
        ).start()

        ssc.awaitTermination() // block 阻塞main线程
    }

}

根据HDS判断是否关闭

import java.net.URI
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FileSystem, Path}
import org.apache.spark.streaming.{StreamingContext, StreamingContextState}
class MonitorStop(ssc: StreamingContext) extends Runnable {
 override def run(): Unit = {
 val fs: FileSystem = FileSystem.get(new URI("hdfs://linux1:9000"), new 
Configuration(), "atguigu")
 while (true) {
 try
 Thread.sleep(5000)
 catch {
 case e: InterruptedException =>
 e.printStackTrace()
 }
 val state: StreamingContextState = ssc.getState
 val bool: Boolean = fs.exists(new Path("hdfs://linux1:9000/stopSpark"))
 if (bool) {
 if (state == StreamingContextState.ACTIVE) {
 ssc.stop(stopSparkContext = true, stopGracefully = true)
 System.exit(0)
 }
 }
 }
 } }

恢复数据

从检查点恢复数据,如果恢复不了在创建新的数据

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext, StreamingContextState}

object SparkStreaming09_Resume {

    def main(args: Array[String]): Unit = {

        //从检查点恢复数据,如果恢复不了在创建新的数据
        val ssc = StreamingContext.getActiveOrCreate("cp", ()=>{
            
            val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
            val ssc = new StreamingContext(sparkConf, Seconds(3))

            val lines = ssc.socketTextStream("localhost", 9999)
            val wordToOne = lines.map((_,1))

            wordToOne.print()

            ssc
        })

        ssc.checkpoint("cp")

        ssc.start()
        ssc.awaitTermination() // block 阻塞main线程


    }

}

第 7 章 SparkStreaming 案例实操

7.1 环境准备

7.1.1 pom 文件

<dependencies>
 <dependency>
 <groupId>org.apache.spark</groupId>
 <artifactId>spark-core_2.12</artifactId>
 <version>3.0.0</version>
 </dependency>
 <dependency>
 <groupId>org.apache.spark</groupId>
 <artifactId>spark-streaming_2.12</artifactId>
 <version>3.0.0</version>
 </dependency>
 <dependency>
 <groupId>org.apache.spark</groupId>
 <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
 <version>3.0.0</version>
 </dependency>
 <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
 <dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid</artifactId>
 <version>1.1.10</version>
 </dependency>
 <dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.27</version>
</dependency>
<dependency>
 <groupId>com.fasterxml.jackson.core</groupId>
 <artifactId>jackson-core</artifactId>
 <version>2.10.1</version>
</dependency>
</dependencies>
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值