向SpringBoot2.0遷移的爬坑指南

公司的項(xiàng)目需要從SpringMVC遷移到SpringBoot2.0,本人用了三天的時(shí)間才基本完成遷移吩跋,今天就來大體的做一下總結(jié)

HikariCP

SpringBoot2.0將HikariCP替換原來的Tomcat作為默認(rèn)的數(shù)據(jù)庫連接池(眾心所向)。

下面就說一下在配置中我們需要做的變化

原來我們在配置讀寫分離的數(shù)據(jù)庫卵酪,是這樣配置的

spring.datasource.readwrite.url=jdbc:mysql://127.0.0.1:3306/bookSystem?characterEncoding=utf-8&useSSL=false
spring.datasource.readwrite.username=root
spring.datasource.readwrite.password=123456
spring.datasource.readwrite.driver-class-name=com.mysql.jdbc.Driver

如果升級后還保持原有配置會(huì)出現(xiàn)錯(cuò)誤

HikariPool-1 - jdbcUrl is required with driverClassName

而在升級以后我們需要如何配置呢畜埋?

spring.datasource.readwrite.jdbc-url=jdbc:mysql://127.0.0.1:3306/bookSystem?characterEncoding=utf-8&useSSL=false
spring.datasource.readwrite.username=root
spring.datasource.readwrite.password=123456
spring.datasource.readwrite.driver-class-name=com.mysql.jdbc.Driver

可以看出url前面加上了jdbc

當(dāng)然既然是使用了讀寫分離的數(shù)據(jù)庫,光做這些是不夠的喧枷,需要進(jìn)行手動(dòng)配置

    @Bean
    // 設(shè)置為首選的數(shù)據(jù)源
    @Primary
    // 讀取配置
    @ConfigurationProperties(prefix="spring.datasource.readwrite")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

也許有的朋友還不知道配置文件是如何讀取到配置類的我們就簡單說一下

  • 配置文件中寫一個(gè)name
  • 在需要匹配的類中有一個(gè)name屬性
  • 這樣就會(huì)一一對應(yīng)進(jìn)行讀取

可能上面說的有點(diǎn)抽象,下面通過一個(gè)實(shí)例來進(jìn)行進(jìn)一步的解釋

application.yml寫了這樣幾行配置

props:
  map:
    key: 123
    key1: 456
  test: 123456

讀取類

@Component
@Data
@ConfigurationProperties(prefix = "props")
public class Props {

    private Map<String, String> map = new HashMap<>();

    private String test;

}

可以發(fā)現(xiàn)我們先配置一個(gè)前綴弓坞,讓配置類找到props隧甚,然后通過屬性與配置的一一對應(yīng)進(jìn)行匹配,現(xiàn)在明白了如何配置渡冻,我們就來看一下HikariConfig

private String driverClassName;
private String jdbcUrl;

我們可以從源碼中看到這兩個(gè)屬性戚扳,這也就是我們要設(shè)置jdbc-url的原因

故事到這里只是剛剛開始,請大家耐心去看

Gradle

springboot2默認(rèn)需要4.0以上的gradle了族吻,所以我們修改一下gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

還有一個(gè)重要的地方帽借,gradle的依賴管理進(jìn)行了升級珠增,在gradle中加入一個(gè)插件即可

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

在打包時(shí)的命令也發(fā)生了變化,可以使用gradle bootjar或者gradle bootwar來進(jìn)行打包砍艾,然后gradle bootrun運(yùn)行

當(dāng)然蒂教,要補(bǔ)充一點(diǎn),在boot2.0遷移的官方文檔中說脆荷,推薦我們加入

runtime("org.springframework.boot:spring-boot-properties-migrator")

只要將其作為依賴添加到項(xiàng)目中凝垛,它不僅會(huì)分析應(yīng)用程序的環(huán)境并在啟動(dòng)時(shí)打印診斷信息,而且還會(huì)在運(yùn)行時(shí)為項(xiàng)目臨時(shí)遷移屬性

ps:boot2的報(bào)錯(cuò)真的有點(diǎn)少蜓谋,我遇到了多次什么報(bào)錯(cuò)信息都沒有Hikari就自動(dòng)關(guān)閉的情況

ORM

由于我們的項(xiàng)目還是使用的Hibernate梦皮,所以起初想著平滑遷移,便沒有改變桃焕,但是發(fā)現(xiàn)在Hibernate5.2.1以上已經(jīng)不推薦Criteria剑肯,這代表著正在逐漸向JPA標(biāo)準(zhǔn)化進(jìn)行過度,所以下面給出兩種替換方式

  • JPA

demo

// 傳入Pageable對象和quizId观堂,返回一個(gè)Page對象
public interface QuizRepository extends JpaRepository<QuizEntity, Integer> {
    Page<QuizEntity> findByQuizId(long quizId, Pageable pageable);
}

Pageable

PageRequest pageRequest = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime"));
  • CriteriaBuilder

demo

@Repository
public class QuizDao {

    // 注入EntityManager
    @Resource
    private EntityManager entityManager;

    public Pair<Long, List<QuizEntity>> search(String keyword, int page, int size) {
        // 創(chuàng)建構(gòu)造器
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        // 設(shè)置語句查詢對應(yīng)的實(shí)體     
        CriteriaQuery<QuizEntity> criteria = builder.createQuery(QuizEntity.class);
        // 設(shè)置from的來源
        Root<QuizEntity> root = criteria.from(QuizEntity.class);
        // 設(shè)置查詢的條件
        criteria.where(builder.ge(root.get("status"), 0));
        // 設(shè)置排序的屬性
        criteria.orderBy(builder.asc(root.get("createTime")));
        TypedQuery<QuizEntity> query = entityManager.createQuery(criteria);
        // 獲取總數(shù)據(jù)量
        long total = query.getResultList().size();
        // 設(shè)置第幾頁让网,和每頁的數(shù)據(jù)
        query.setFirstResult((page - 1) * size);
        query.setMaxResults(size);
        List<QuizEntity> resultList = query.getResultList();
        return new Pair<>(total, resultList);
    }
}

可以發(fā)現(xiàn)一種更加方便快捷,一種更加靈活型将,大家可以自行選型,但當(dāng)我使用JPA時(shí)荐虐,遇到了問題七兜。

在我使用UPDATE時(shí)發(fā)生了error,最后發(fā)現(xiàn)需要進(jìn)行事務(wù)和標(biāo)注

demo

@Transactional(rollbackFor = Exception.class)
public interface QuizRepository extends JpaRepository<QuizEntity, Integer> {
    @Modifying
    @Query("update tr_quiz q set q.readState=true where q.quizId = ?1 and q.lessonId = ?2 and q.status >= 0")
    void readAll(long quizId, long lessonId);
}

@Transactional和@Modifying注解大家一定不要忘記福扬。

lombok

lombok相信大家基本都用過腕铸,就是可以通過注解來生成構(gòu)造函數(shù),getset方法等的包铛碑,而在boot2中引入最新版時(shí)狠裹,遇到了一些問題

通過查看官方文檔,發(fā)現(xiàn)了下面這句話

BREAKING CHANGE: lombok config key lombok.addJavaxGeneratedAnnotation now defaults to false instead of true. Oracle broke this annotation with the release of JDK9, necessitating this breaking change.

lombok在最新版本中默認(rèn)lombok.addJavaxGeneratedAnnotation為false

這導(dǎo)致了通過http請求獲取數(shù)據(jù)進(jìn)行轉(zhuǎn)化時(shí)的失敗汽烦,需要我們手動(dòng)配置一下涛菠,所以我選擇了降級到1.16.18省去配置的麻煩

Redis

如果使用的client為Jedis,那恭喜你撇吞,你又需要做轉(zhuǎn)變了俗冻,因?yàn)閎oot2.0中默認(rèn)為lettuce,我們需要修改一下gradle的配置

compile('org.springframework.boot:spring-boot-starter-data-redis') {
        exclude module: 'lettuce-core'
    }
    compile('redis.clients:jedis')

Cassandra

我們集群中的Cassandra版本比較老牍颈,所以不能使用

compile('org.springframework.boot:spring-boot-starter-cassandra')

需要使用

compile('com.datastax.cassandra:cassandra-driver-core:2.1.7.1') compile('com.datastax.cassandra:cassandra-driver-mapping:2.1.7.1')

但是迄薄,引入包后一直發(fā)生錯(cuò)誤,又是Hikari自動(dòng)停止煮岁,后來仔細(xì)觀察了包的依賴關(guān)系(使用./gradlew dependencyInsight --dependency cassandra-driver-core可以分析)

發(fā)現(xiàn)在mapping中包含了core讥蔽,于是去掉core涣易,果然項(xiàng)目跑起來了- -,很激動(dòng)冶伞。

Kafka

最后再來說一下kafka在boot中的基本使用新症,首先介紹最簡單的一種單線程消費(fèi)模式

我們只需要?jiǎng)?chuàng)建兩個(gè)類

@Slf4j
@Component
public class Consumer {
    @KafkaListener(topics = {"test"})
    public void process(ConsumerRecord record) {
        String topic = record.topic();
        String key = record.key().toString();
        String message = record.value().toString();
    }
}
@Slf4j
@Component
public class Producer {

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;


    public void send(String topic, String message) {
        log.info("send message: topic: " + topic + " message: " + message);
        kafkaTemplate.send(topic, message);
    }

    public void send(String topic, String key, String message) {
        log.info("send message: topic: " + topic + " key: " + key + " message: " + message);
        kafkaTemplate.send(topic, key, message);
    }
}

怎么樣?是不是很簡單碰缔,但一定不要忘了在application.properties里配置一下账劲,下面給出基本的配置

#kafka
#producer
// bootstrap-servers代替原來的broker.list
spring.kafka.bootstrap-servers=localhost:9092
// 生產(chǎn)者key的序列化方式
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
// 生產(chǎn)者value的序列化方式
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

#consumer
spring.kafka.consumer.group-id=test_group
spring.kafka.consumer.enable-auto-commit=true
// 消費(fèi)者key的反序列化方式
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
// 消費(fèi)者value的反序列化方式
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

下面來介紹一下Kafka如何使用多線程來進(jìn)行消費(fèi)

@Slf4j
@Component
public class ConsumerGroup {

    private ExecutorService executor;

    // 注入消費(fèi)者
    @Resource
    private Consumer consumer;

    public static Map<Integer, ThreadHolder> map = Maps.newHashMap();

    public ConsumerGroup(
            @Value("${consumer.concurrency}") int concurrency,
            @Value("${spring.kafka.bootstrap-servers}") String servers,
            @Value("${consumer.group-id}") String group,
            @Value("${consumer.topic}") String topics) {
        // 配置參數(shù)
        Map<String, Object> config = new HashMap<>();
        config.put("bootstrap.servers", servers);
        config.put("group.id", group);
        config.put("enable.auto.commit", false);
        config.put("key.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
        config.put("value.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
        Map<String, Integer> topicMap = new HashMap<>();
        String[] topicList = topics.split(",");
        for (String topic : topicList) {
            topicMap.put(topic, concurrency);
        }
        // 設(shè)置一個(gè)消費(fèi)者開關(guān),傳入值大于等于1時(shí)金抡,才開啟消費(fèi)者
        if (concurrency >= 1) {
            this.executor = Executors.newFixedThreadPool(concurrency);
            int threadNum = 0;
            for (String topic : topicMap.keySet()) {
                executor.submit(new ConsumerThread(config, topic, ++threadNum));
            }
        }
    }

    public class ConsumerThread implements Runnable {

        /**
         * 每個(gè)線程私有的KafkaConsumer實(shí)例
          */
        private KafkaConsumer<String, String> kafkaConsumer;

        private int id;

        private String name;

        public ConsumerThread(Map<String, Object> consumerConfig, String topic, int threadId) {
            this.id = threadId;
            this.name = topic;
            Properties props = new Properties();
            props.putAll(consumerConfig);
            this.kafkaConsumer = new KafkaConsumer<>(props);
            // 訂閱topic
            kafkaConsumer.subscribe(Collections.singletonList(topic));
        }

        @Override
        public void run() {
            log.info("consumer task start, id = " + id);
            try {
                while (true) {
                     // 循環(huán)輪詢消息
                    ConsumerRecords<String, String> records = kafkaConsumer.poll(1000);
                    for (ConsumerRecord<String, String> record : records) {
                        int partition = record.partition();
                        long offset = record.offset();
                        String key = record.key();
                        String value = record.value();
                        log.info(String.format("partition:%d, offset:%d, key:%s, message:%s", partition, offset, key, value));
                        consumer.process(record);
                        // 使用手動(dòng)提交瀑焦,當(dāng)消費(fèi)成功后才進(jìn)行消費(fèi)
                        kafkaConsumer.commitAsync();
                    }
                }
            } catch (Exception e) {
                log.warn("process message failure!", e);
            } finally {
                // 報(bào)錯(cuò)時(shí)關(guān)閉消費(fèi)者
                kafkaConsumer.close();
                log.info("consumer task shutdown, id = " + id);
            }
        }
    }
}
@Slf4j
@Component
public class Consumer {

    public void process(ConsumerRecord record) {
        long startTime = System.currentTimeMillis();
        String topic = record.topic();
        String key = "";
        if (record.key() != null) {
            key = record.key().toString();
        }
        String message = record.value().toString();
        if ("test".equals(topic)) {
                // 消費(fèi)邏輯
        }
        long endTime = System.currentTimeMillis();
        log.info("SubmitConsumer.time=" + (endTime - startTime));
    }
}

最后,當(dāng)調(diào)試時(shí)不要忘記在application梗肝。properties中設(shè)置

debug=true

打開debug可以看到更清晰的調(diào)試信息榛瓮。

吃水不忘挖井人,附上boot2.0的官方遷移文檔
官方遷移文檔

順便附上本人的兩個(gè)開源項(xiàng)目地址:

  1. 基于token驗(yàn)證的用戶中心:https://github.com/stalary/UserCenter
  2. 輕量級的java消息隊(duì)列LightMQ:https://github.com/stalary/lightMQ
    支持點(diǎn)對點(diǎn)和訂閱發(fā)布模式巫击,內(nèi)部基于ArrayBlockingQueue簡單實(shí)現(xiàn)禀晓,客戶端輪詢拉取數(shù)據(jù),可直接maven引入jar包通過注解使用坝锰。

最激動(dòng)人心的不是站在高處時(shí)的耀眼粹懒,而是無人問津時(shí)的默默付出

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市顷级,隨后出現(xiàn)的幾起案子凫乖,更是在濱河造成了極大的恐慌,老刑警劉巖弓颈,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帽芽,死亡現(xiàn)場離奇詭異,居然都是意外死亡翔冀,警方通過查閱死者的電腦和手機(jī)导街,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纤子,“玉大人搬瑰,你說我怎么就攤上這事】嘏穑” “怎么了跌捆?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長象颖。 經(jīng)常有香客問我佩厚,道長,這世上最難降的妖魔是什么说订? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任抄瓦,我火速辦了婚禮潮瓶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钙姊。我一直安慰自己毯辅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布煞额。 她就那樣靜靜地躺著思恐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪膊毁。 梳的紋絲不亂的頭發(fā)上胀莹,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音婚温,去河邊找鬼描焰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛栅螟,可吹牛的內(nèi)容都是我干的荆秦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼力图,長吁一口氣:“原來是場噩夢啊……” “哼步绸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吃媒,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤瓤介,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后晓折,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惑朦,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兽泄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年漓概,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片病梢。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胃珍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜓陌,到底是詐尸還是另有隱情觅彰,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布钮热,位于F島的核電站填抬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏隧期。R本人自食惡果不足惜飒责,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一赘娄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宏蛉,春花似錦遣臼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嗅义,卻和暖如春屏歹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芥喇。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工西采, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人继控。 一個(gè)月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓械馆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親武通。 傳聞我的和親對象是個(gè)殘疾皇子霹崎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容