向SpringBoot2.0迁移的爬坑指南

公司的项目需要从SpringMVC迁移到SpringBoot2.0,本人用了三天的时间才基本完成迁移,今天就来大体的做一下总结

HikariCP

SpringBoot2.0将HikariCP替换原来的Tomcat作为默认的数据库连接池(众心所向)。

下面就说一下在配置中我们需要做的变化

原来我们在配置读写分离的数据库,是这样配置的

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

如果升级后还保持原有配置会出现错误

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

当然既然是使用了读写分离的数据库,光做这些是不够的,需要进行手动配置

    @Bean
    // 设置为首选的数据源
    @Primary
    // 读取配置
    @ConfigurationProperties(prefix="spring.datasource.readwrite")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

也许有的朋友还不知道配置文件是如何读取到配置类的我们就简单说一下

  • 配置文件中写一个name
  • 在需要匹配的类中有一个name属性
  • 这样就会一一对应进行读取

可能上面说的有点抽象,下面通过一个实例来进行进一步的解释

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;

}

可以发现我们先配置一个前缀,让配置类找到props,然后通过属性与配置的一一对应进行匹配,现在明白了如何配置,我们就来看一下HikariConfig

private String driverClassName;
private String jdbcUrl;

我们可以从源码中看到这两个属性,这也就是我们要设置jdbc-url的原因

故事到这里只是刚刚开始,请大家耐心去看

Gradle

springboot2默认需要4.0以上的gradle了,所以我们修改一下gradle-wrapper.properties

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

还有一个重要的地方,gradle的依赖管理进行了升级,在gradle中加入一个插件即可

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

在打包时的命令也发生了变化,可以使用gradle bootjar或者gradle bootwar来进行打包,然后gradle bootrun运行

当然,要补充一点,在boot2.0迁移的官方文档中说,推荐我们加入

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

只要将其作为依赖添加到项目中,它不仅会分析应用程序的环境并在启动时打印诊断信息,而且还会在运行时为项目临时迁移属性

ps:boot2的报错真的有点少,我遇到了多次什么报错信息都没有Hikari就自动关闭的情况

ORM

由于我们的项目还是使用的Hibernate,所以起初想着平滑迁移,便没有改变,但是发现在Hibernate5.2.1以上已经不推荐Criteria,这代表着正在逐渐向JPA标准化进行过度,所以下面给出两种替换方式

  • JPA

demo

// 传入Pageable对象和quizId,返回一个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) {
        // 创建构造器
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        // 设置语句查询对应的实体     
        CriteriaQuery<QuizEntity> criteria = builder.createQuery(QuizEntity.class);
        // 设置from的来源
        Root<QuizEntity> root = criteria.from(QuizEntity.class);
        // 设置查询的条件
        criteria.where(builder.ge(root.get("status"), 0));
        // 设置排序的属性
        criteria.orderBy(builder.asc(root.get("createTime")));
        TypedQuery<QuizEntity> query = entityManager.createQuery(criteria);
        // 获取总数据量
        long total = query.getResultList().size();
        // 设置第几页,和每页的数据
        query.setFirstResult((page - 1) * size);
        query.setMaxResults(size);
        List<QuizEntity> resultList = query.getResultList();
        return new Pair<>(total, resultList);
    }
}

可以发现一种更加方便快捷,一种更加灵活,大家可以自行选型,但当我使用JPA时,遇到了问题。

在我使用UPDATE时发生了error,最后发现需要进行事务和标注

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相信大家基本都用过,就是可以通过注解来生成构造函数,getset方法等的包,而在boot2中引入最新版时,遇到了一些问题

通过查看官方文档,发现了下面这句话

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在最新版本中默认lombok.addJavaxGeneratedAnnotation为false

这导致了通过http请求获取数据进行转化时的失败,需要我们手动配置一下,所以我选择了降级到1.16.18省去配置的麻烦

Redis

如果使用的client为Jedis,那恭喜你,你又需要做转变了,因为boot2.0中默认为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')

但是,引入包后一直发生错误,又是Hikari自动停止,后来仔细观察了包的依赖关系(使用./gradlew dependencyInsight --dependency cassandra-driver-core可以分析)

发现在mapping中包含了core,于是去掉core,果然项目跑起来了- -,很激动。

Kafka

最后再来说一下kafka在boot中的基本使用,首先介绍最简单的一种单线程消费模式

我们只需要创建两个类

@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
// 生产者key的序列化方式
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
// 生产者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
// 消费者key的反序列化方式
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
// 消费者value的反序列化方式
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

下面来介绍一下Kafka如何使用多线程来进行消费

@Slf4j
@Component
public class ConsumerGroup {

    private ExecutorService executor;

    // 注入消费者
    @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) {
        // 配置参数
        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);
        }
        // 设置一个消费者开关,传入值大于等于1时,才开启消费者
        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 {

        /**
         * 每个线程私有的KafkaConsumer实例
          */
        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) {
                     // 循环轮询消息
                    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);
                        // 使用手动提交,当消费成功后才进行消费
                        kafkaConsumer.commitAsync();
                    }
                }
            } catch (Exception e) {
                log.warn("process message failure!", e);
            } finally {
                // 报错时关闭消费者
                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)) {
                // 消费逻辑
        }
        long endTime = System.currentTimeMillis();
        log.info("SubmitConsumer.time=" + (endTime - startTime));
    }
}

最后,当调试时不要忘记在application。properties中设置

debug=true

打开debug可以看到更清晰的调试信息。

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

顺便附上本人的两个开源项目地址:

  1. 基于token验证的用户中心:https://github.com/stalary/UserCenter
  2. 轻量级的java消息队列LightMQ:https://github.com/stalary/lightMQ
    支持点对点和订阅发布模式,内部基于ArrayBlockingQueue简单实现,客户端轮询拉取数据,可直接maven引入jar包通过注解使用。

最激动人心的不是站在高处时的耀眼,而是无人问津时的默默付出

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容