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

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

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

demo

@Transactional(rollbackFor = Exception.class)
public interface QuizRepository extends JpaRepository {
    @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 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 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 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 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 kafkaConsumer;

        private int id;

        private String name;

        public ConsumerThread(Map 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 records = kafkaConsumer.poll(1000);
                    for (ConsumerRecord 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包通过注解使用。

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

你可能感兴趣的:(向SpringBoot2.0迁移的爬坑指南)