使用Spring Boot搭建個人博客
簡介
後端使用Spring Boot搭建的一個博客系統,前端使用的是thymeleaf + bootstrap,集成了editormd的markdown編輯器。
本項目適合Spring初學者作爲練手項目,包含的主要技術點:
從開始到項目完成的全過程,篇幅可能較長
- spring data jpa的基本使用
- 數據綁定
- 攔截器
- spring boot 註解配置
地址
項目的思路以及前端代碼來自他的博客:
一個JavaWeb搭建的開源Blog系統,整合SSM框架
項目GitHub地址:
https://github.com/wchstrife/blog
功能展示
把工程導入本地後,在mysql中添加一個叫做blog的database,然後在配置文件中把數據庫的賬號密碼地址修改成自己的即可運行
主界面:
詳情
登錄
後臺管理
寫博客
下面我們的正式開始
一、項目搭建
我使用的是IDEA使用Maven構建一個Spring Boot工程,搭建過程請自行百度
建好工程之後手動添加一些目錄,下面展示一下詳細的目錄
項目目錄
這裏主要介紹一下resources目錄
static目錄是spring boot 默認的一個掃描的路徑,所以我們要把引用的資源放在這裏。
bootstrap是我們引進的一個樣式控制的組件
editormd是引入的支持MarkDown語法的編輯器
css是一些全局的樣式控制
jquery是bootstrap必要的
templates目錄下放的是前端的HTML頁面
admin是後臺的管理
common是所有頁面公用的部分
front是前臺的展示界面
pom.xml
展示一下我們項目完整的依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wchstrife</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>1.5.4.RELEASE</version>
</dependency>
<!--不嚴格檢查-->
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
<!--熱部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional><!-- optional=true,依賴不會傳遞,該項目依賴devtools;之後依賴myboot項目的項目如果想要使用devtools,需要重新引入 -->
</dependency>
<!--markdown-->
<dependency>
<groupId>org.tautua.markdownpapers</groupId>
<artifactId>markdownpapers-core</artifactId>
<version>1.4.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
實體映射
實體的映射是在entity包下
下面我們使用Spring data jpa 對我們項目需要的實體進行數據庫的映射
通過這種方式,我們可以避免直接操作數據庫,而是通過Spring data jpa來進行增刪改查的操作
這裏我們一共用到三張表:Aritcle Category User 分別對應博客,分類,用戶
主鍵生成策略:
這裏我採用的是UUID的生成方式,會生成一串字符串作爲主鍵
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(name = "id", columnDefinition = "varchar(64) binary")
private String id;
外鍵約束
在Article表中,我們使用了ManyToOne的外鍵約束
在Article中有一個Categoriy的引用,代表一個博客有對應的一個分類,而一個分類下應該有多個博客
@ManyToOne
private Category category;
數據訪問層dao
在dao包下封裝了一系列的對數據庫增刪改查的操作
Spring data jpa強大之處就是可以根據方法名進行解析。所以在dao層下的接口,大部分只有方法名和接收的參數。
如果需要自定義sql語句只需要加註解即可
這裏演示一個自定義的模糊查詢
@Query("from Article where title like %:title%")
public List<Article> findByTitleLike(@Param("title") String title);
controller和service
劃分這兩層體現了很重要的分層的思想,即每一層只針對一層提供方法,不去了解其他層的方法,這樣方便維護。
所以爲了體現這種分層的思想,所以針對數據庫的操作都放在service層進行封裝
在controller層主要負責url地址的分配,對外提供的接口,部分簡單的邏輯寫在這裏
二、front前臺展示
在前端我們使用了Thymeleaf前端模板,裏面有很多類似JSP標籤的寫法,進行對數據的遍歷操作
在後端Control裏面返回頁面的時候,使用Model向其中添加屬性,在前端頁面上可以通過${}
來獲取這個屬性值,並且用th:each
來遍歷
注意帶th的語法表示交給thyme leaf模板解析的語法
例如前臺index界面:
controller返回所有的博客列表
@RequestMapping("")
public String list(Model model){
List<Article> articles = articleService.list();
model.addAttribute("articles", articles);
return "front/index";
}
在前臺使用ty的語法進行遍歷顯示
<div th:each="articles:${articles}">
<h3 th:text="${articles.title}"></h3>
<span class="summary" th:text="${articles.summary}"></span><br/><br/>
</div>
所以在前臺展示只有三個頁面,分別是列表顯示所有博客,按類型顯示博客,某個博客的全文
所以對應在controller裏面只需要從數據庫篩選全部的博客、某個類型的博客、取出某個博客(通過ID)的全文在頁面上展示即可
管理員界面
在管理員界面要實現的功能比較多,最重要的是對博客的增刪改
同時這裏有一個登錄的功能,只有在User表中有對應的賬號密碼才能登錄,所以這裏需要一層登陸攔截,這個稍後介紹。
引入Markdown組件
在編輯博客的時候我們支持使用markdown語法,我在網上找了一款叫做editormd的開源項目,放到static目錄下
在我們write.html用如下的語法引入編輯器
<script th:src="@{/jquery-3.2.1.min.js}"></script>
<script th:src="@{/editormd/editormd.js}"></script>
<script th:src="@{/bootstrap/js/bootstrap.js}"></script>
<script type="text/javascript" th:inline="javascript">
// 調用編輯器
var testEditor;
$(function() {
testEditor = editormd("test-editormd", {
width : "1000px",
height : 640,
syncScrolling : "single",
path : [[@{/editormd/lib/}]]
});
});
</script>
<script th:inline="javascript">
function selectCategory(obj) {
var name = $(obj).attr("name");
var displayName = $(obj).attr("abbr");
console.log(name + " " + displayName);
$("#categoryBtn").html(displayName);
$("#cateoryInput").val(name);
}
</script>
在需要編輯器的地方輸入一個textarea即可
<div id="test-editormd">
<textarea style="display:none;" name="content" th:field="*{content}" th:text="${target.content}"></textarea>
</div>
深坑:
如果在js中要使用thymeleaf的語法,比如@{} ${}這種語法,一定要加上這句話th:inline="javascript
這樣來使用該值[[@{/editormd/lib/}]]
write 操作
在寫文章按鈕上綁定好要提交的action,在controller裏面對這個action進行處理,這裏我們重點是要返回一個new出來的Article對象,因爲要對對象進行數據的綁定,所以如果不傳這個參數的話會報錯
@RequestMapping("/write")
public String write(Model model){
List<Category> categories = categoryService.list();
model.addAttribute("categories", categories);
model.addAttribute("article", new Article());
return "admin/write";
}
在write.html中引入相關的編輯器組件以後,通過th:object綁定到Article對象上,然後Spring Boot會自動的幫我們把表單中的數據組合成一個ArtIicle對象,是不是很方便
<form method="post" th:action="@{/admin/save}" th:object="${article}">
<input name="category" id="cateoryInput" type="hidden" th:field="*{category.name}"/>
<input type="text" class="form-contorl" palceholder="標題" name="title" th:field="*{title}"/>
<textarea style="display:none;" name="content" th:field="*{content}"></textarea>
</form>
save操作
前面html的表達提交之後,提交到save這個action中,在這裏我們對提交的數據進行一個簡單的處理,然後調用service裏面封裝的dao層的save方法即可
這裏主要是對博客的日期,簡介進行一個處理
@RequestMapping(value = "/save", method = RequestMethod.POST)
public String save(Article article){
//設置種類
String name = article.getCategory().getName();
Category category = categoryService.fingdByName(name);
article.setCategory(category);
//設置摘要,取前40個字
if(article.getContent().length() > 40){
article.setSummary(article.getContent().substring(0, 40));
}else {
article.setSummary(article.getContent().substring(0, article.getContent().length()));
}
article.setDate(sdf.format(new Date()));
articleService.save(article);
return "redirect:/admin";
}
update操作
更新博客其實和寫博客是一個道理,不過在更新的時候需要把id傳給controller,然後根據id找到這個文章
把這個博客的內容、標題渲染在update.html中
然後在表單提交的字段中加一個隱藏表單,把博客的id傳進去
調用save方法即可完成更新(根據id進行save,所以這時候會執行更新操作)
@RequestMapping("/update/{id}")
public String update(@PathVariable("id") String id, Model model){
Article article = articleService.getById(id);
model.addAttribute("target", article);
List<Category> categories = categoryService.list();
model.addAttribute("categories", categories);
model.addAttribute("article", new Article());
return "admin/update";
}
登陸攔截
對於後臺的增刪改操作,我們只對管理員開放,雖然我們增加了一個登錄界面,但是別人還是可以通過直接輸入對應url進行訪問,所以我們要在這裏增加一層登陸攔截,讓沒有登錄的人不允許訪問到我們後天的界面
在登錄處理登錄的doLogin方法中,我們在登錄成功之後在cookie中加一個標誌
Cookie cookie = new Cookie(WebSecurityConfig.SESSION_KEY, user.toString());
response.addCookie(cookie);
在aspect包中建立一個攔截器
在WebSecurityConfig類中繼承WebMvcConfigurerAdapter
重寫addInterceptors
方法,在裏面配置要攔截的路徑
在裏面建一個內部類
SecurityInterceptor繼承HandlerInterceptorAdapter
重寫preHandle方法,表明在方法執行前執行攔截的動作
我們在這裏對cookie的內容進行判斷,如果有登錄成功的標誌,就進入後臺管理界面,否則跳轉到登錄界面
注意:使用session的方法是不可以的,因爲我們在登錄的controller當中使用的是重定向(redirect),所以會導致session裏面的值取不到