1)使用Spring Boot的DevTools實作熱部署
在項目建立之初我們就已經在pom檔案中引入了
spring-boot-devtools
這個Spring Boot提供的熱部署插件,但是可能會遇到
熱部署不生效的問題,如果你使用的是Intellij Idea開發工具,參見《使用Spring Boot Devtools實作熱部署》。
2)建立資料表的實體類
雖說我們要做的是對于部落格的全文檢索,但是在使用者沒有給定搜尋條件之前,我們還是應該使用Mysql通過主鍵索引的方式查詢出部落格資料(當然,簡單起見這裡沒有實作分頁查詢功能),Mysql的主鍵查詢是很快的,無需使用ES;當使用者輸入查詢條件時,表示需要進行全文檢索時我們才使用ES。
Mysql自不用說,ES也是需要實體類來承載資料的,他們的字段結構都是相同的,不同的隻是标注在字段上的注解,下面來快速建立他們的實體類:
2.1)建立部落格文章Mysql實體類
@Data
@Table(name = "t_article") // 指定實體類對應的資料表
@Entity
public class MysqlArticle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 使用資料表定義的增長政策(自增)
private Integer id; // 文章ID
private String author; // 文章作者
private String title; // 文章标題
// String類型預設映射的是varchar類型,content字段使用到了mediumtext,需要顯式指定一下
@Column(name = "content", columnDefinition = "mediumtext")
private String content; // 文章正文内容
private Date createTime; // 文章建立時間
private Date updateTime; // 文章更新時間
}
2.2)建立部落格文章ElasticSearch實體類
@Data
// 指定實體類對應ES的索引名稱為blog,類型type是文檔類型,使用伺服器遠端配置
// 為避免每次重新開機項目都将ES中的資料删除後再同步,createIndex指定為false
@Document(indexName = "blog", type = "_doc",
useServerConfiguration = true, createIndex = false)
public class ElasticArticle {
@Id // org.springframework.data.annotation.Id
private Integer id; // 文章ID
// 指定字段對應的ES類型是Text,analyzer指定分詞器為ik_max_word
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String author; // 文章作者
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title; // 文章标題
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String content; // 文章正文内容
// 指定字段對應ES中的類型是Date,使用自定義的日期格式化,pattern指定格式化
// 規則是“日期時間”或“日期”或“時間毫秒”
@Field(type = FieldType.Date, format = DateFormat.custom,
pattern = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private Date createTime; // 文章建立時間
@Field(type = FieldType.Date, format = DateFormat.custom,
pattern = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private Date updateTime; // 文章更新時間
}
3)編寫Mysql和ES的資料操作接口
由于使用了
Spring-Data-JPA
,這項工作将十分簡單,隻需要自建接口繼承自對應的
JpaRepository
和
ElasticsearchRepository
接口即可。
建立Mysql資料通路接口
public interface MysqlArticleRepository
extends JpaRepository<MysqlArticle, Integer> {
}
建立ES資料通路接口
public interface ElasticArticleRepository
extends ElasticsearchRepository<ElasticArticle, Integer> {
}
4)測試從ES中檢索資料
為了保證ES已成功內建到項目中,我們需要測試一下能否從ES中擷取到資料。在工程的測試目錄下編寫一個ES測試類擷取并周遊ES中所有文章的标題:
@SpringBootTest
public class ElasticTest {
@Autowired
private ElasticArticleRepository elasticArticleRepository;
@Test
void testElasticSearch() {
elasticArticleRepository.findAll().iterator()
.forEachRemaining(elasticArticle -> System.out.println(elasticArticle.getTitle()));
}
}
此時直接啟動測試會報錯
NoNodeAvailableException[None of the configured nodes are available
,我們之前啟動ES隻需要一條簡單的啟動指令,就能使用HTTP的形式在浏覽器中成功通路到ES。但是在項目中用Java代碼的形式使用的是TCP的形式,我們需要對ES的配置檔案做一些簡單的修改:進入到ElasticSearch的目錄下的config,找到名為
elasticsearch.yml
的配置檔案,将其中
cluster.name
和
network.host
的注釋取消(删除"#"),再将
network.host
一項的值改為
127.0.0.1
。将ES程序停止後再次啟動ES,然後到logstash的bin目錄下執行指令
./logstash -f ../config/mysql.conf
啟動logstash将資料同步到ES中,最後執行測試用例:
5)編寫後端控制器
在確定能夠正常連接配接ES後,我們直接編寫最主要的後端控制器
SearchController
(因為業務簡單我們省略Service層的編寫):
@RestController
@RequestMapping("/article")
public class SearchController {
private final MysqlArticleRepository mysqlRepository;
private final ElasticArticleRepository elasticRepository;
public SearchController(MysqlArticleRepository mysqlRepository,
ElasticArticleRepository elasticRepository) {
this.mysqlRepository = mysqlRepository;
this.elasticRepository = elasticRepository;
}
@GetMapping
public List<MysqlArticle> queryAllArticles() {
return mysqlRepository.findAll();
}
@PostMapping
// @IgnoreAdvice
public List<ElasticArticle> handleSearchRequest(
@RequestBody RequestParam requestParam) {
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 構造ES查詢條件
String keyword = requestParam.getKeyword();
queryBuilder.should(QueryBuilders.matchPhraseQuery("title", keyword))
.should(QueryBuilders.matchPhraseQuery("content", keyword));
StopWatch watch = new StopWatch();
watch.start(); // 開始計時
Page<ElasticArticle> articles = (Page<ElasticArticle>)
elasticRepository.search(queryBuilder); // 檢索資料
watch.stop(); // 結束計時
System.out.println(String.format("資料檢索耗時:%s ms",
watch.getTotalTimeMillis()));
return articles.getContent();
}
@GetMapping("{id:d+}")
public Object getArticleDetails(@PathVariable("id") Integer id) {
return elasticRepository.findById(id).orElseGet(ElasticArticle::new);
}
}
同時為了讓項目啟動時會通路首頁
index.html
,它是我們的主要頁面,負責展示文章資料和提供文章檢索的入口。我們需要一個控制器
IndexController
:
@Controller
public class IndexController {
private final MysqlArticleRepository mysqlRepository;
public IndexController(MysqlArticleRepository mysqlRepository) {
this.mysqlRepository = mysqlRepository;
}
@GetMapping("/")
public String toIndexPage() {
mysqlRepository.findAll();
return "index.html";
}
}
6)統一響應的處理
一般我們的後端響應都應該有一個統一的格式,是以這裡我再做一下統一響應的處理。考慮到可能不是所有的後端傳回都需要統一格式,我們提供一個注解
@IgnoreAdvice
來決定是否需要傳回統一格式,它可以标注在類和方法上:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface IgnoreAdvice {
}
既然需要統一響應,我們就要有對應格式的Java類來包裝傳回結果:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseData<T> implements Serializable {
private Integer code; // 請求響應碼
private String message; // 請求響應消息
private T result; // 響應資料
public ResponseData(T result) {
this.code = 0;
this.message = "請求處理成功";
this.result = result;
}
public ResponseData(Integer code, String message) {
this.code = code;
this.message = message;
}
}
在Spring MVC架構中提供了對傳回結果進行(增強)處理的支援,我們隻需要實作
ResponseBodyAdvice
接口即可對其他
@RestController
控制器方法的傳回結果進行攔截修改:
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(
MethodParameter methodParameter,
Class<? extends HttpMessageConverter<?>> aClass) {
// 方法類上存在IgnoreAdvice注解,不需要增強處理
if (methodParameter.getDeclaringClass().isAnnotationPresent(
IgnoreAdvice.class)) {
return false;
}
// 如果方法上存在IgnoreAdvice注解,則不需要增強處理
return !Objects.requireNonNull(methodParameter.getMethod())
.isAnnotationPresent(IgnoreAdvice.class
);
}
@Override
public Object beforeBodyWrite(
Object o, MethodParameter methodParameter,
MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
if (o == null) {
return new ResponseData<>(0, "請求結果為空");
} else if (o instanceof ResponseData) {
return o;
} else { return new ResponseData<>(o); } // 包裝結果
}
}
7)編寫前端頁面
在編寫頁面之前我們先通過浏覽器看看後端響應是否跟我們想的一樣。啟動項目通路id為1的文章(注意這裡其實是從ElasticSearch中讀取資料的),可以看到資料正常傳回,通用響應也工作了:
上面我們并沒有驗證ElasticSearch的搜尋接口的功能,因為這需要使用到post請求,我們這裡也不再使用Postman驗證了,而是編寫一個前端頁面來測試;在編寫之前需要介紹一下幾個必要的類庫和Bootstrap前端架構:
以上的資源都可以百度“靜态資源庫”查找并引用,如果想要下載下傳到本地,可以打開對應連結将内容複制到建立檔案中;下面先編寫一個名為index.html的頁面展示文章清單和提供查詢入口:
<!DOCTYPE html>
<html xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Spring Boot + ElasticSearch部落格檢索系統</title>
<link rel="stylesheet" href="styles/bootstrap.min.css" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >
<link rel="stylesheet" href="styles/bootstrap-grid.min.css" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >
</head>
<body>
<div class="container">
<div class="row" style="margin-top: 20px;">
<div class="col-12"><h2>Spring Boot部落格檢索系統</h2></div>
</div>
<div class="row" style="margin-top: 20px;" id="article-view">
<div class="col-12">
<form class="form-inline">
<div class="form-group mb-2">
<label for="search"></label>
<input type="text" class="form-control" id="search" placeholder="請輸入要檢索的關鍵字" v-model="keyword">
</div>
<button type="button" class="btn btn-primary mb-2" style="" v-on:click="elasticSearch">開始檢索</button>
</form>
<div class="row">
<div class="col-sm-6" v-for="(article, index) in articles" style="margin-top: 20px;">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{article.title}}</h5>
<p>{{article.author}}釋出于{{article.createTime}}</p>
<a :href="'details.html?id=' + article.id" target="_blank" rel="external nofollow" >文章詳情</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="scripts/axios.min.js"></script>
<script src="scripts/vue.min.js"></script>
<script type="text/javascript">
const app = new Vue({
el: '#article-view',
data: {
articles: [],
keyword: ''
},
methods: {
getArticles: function () {
let vue = this;
axios.get("http://localhost:8080/article").then(function (response) {
vue.articles = response.data.result;
})
},
elasticSearch: function () {
let vue = this;
let param = {"keyword": vue.keyword};
axios.post("http://localhost:8080/article", param).then(function (response) {
vue.articles = response.data.result;
})
}
},
created: function () { this.getArticles(); }
});
</script>
</body>
</html>
啟動項目通路
localhost:8080
,自動跳轉到首頁:
接下來在搜尋框中輸入“Kibana”然後點選搜尋:
可以看到,搜尋到的文章标題中并不包含關鍵字“kibana”,因為我們設定的檢索條件是檢索文章标題和内容,隻要标題或内容包含了對應的關鍵字就表示比對到了。要看我們的文章内容究竟有沒有“kibana”關鍵字,我們需要再編寫一個文章詳情頁details.html,同樣使用vue.js完成資料的擷取和頁面渲染:
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>文章詳情</title>
<link rel="stylesheet" href="styles/bootstrap.min.css" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >
<link rel="stylesheet" href="styles/bootstrap-grid.min.css" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >
</head>
<body>
<div class="container">
<div class="row">
<div class="row" id="article-view">
<div class="col-12">
<h1 id="title">{{title}}</h1>
<span>作者:{{author}} | 釋出時間:{{createTime}}</span>
<div class="col-9" v-html="content"></div>
</div>
</div>
</div>
</div>
<script src="scripts/axios.min.js"></script>
<script src="scripts/marked.min.js"></script>
<script src="scripts/vue.min.js"></script>
<script type="text/javascript">
function getArticleId(id) {
let query = window.location.search.substring(1);
let vars = query.split("&");
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split("=");
if (pair[0] === id) return pair[1];
}
return false;
}
const app = new Vue({
el: '#article-view',
data: {
title: '',
author: '',
createTime: '',
content: ''
},
methods: {
getArticle: function () {
let vue = this;
let id = getArticleId("id");
axios.get('http://localhost:8080/article/' + id).then(function (response) {
let article = response.data.result;
vue.title = article.title;
vue.author = article.author;
vue.createTime = article.createTime;
vue.content = marked(article.content);
})
}
},
created: function () { this.getArticle(); }
})
</script>
</body>
</html>
點選“檢視文章詳情”進入詳情頁,可以發現文章内容确實存在檢索的關鍵字:
至此一個簡單的部落格文章檢索系統就完成啦~~~