公司的項(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)目地址:
- 基于token驗(yàn)證的用戶中心:https://github.com/stalary/UserCenter
- 輕量級的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í)的默默付出