本系列是對李仁密老師的視頻的學習記錄
前端頁面設計與實現部分不會在此展示,直接上後端部分。
項目源碼和教程可以點擊上面鏈接進行學習
碼雲地址:項目源碼
項目搭建
1. 創建項目
IDEA利用Spring初始化工具創建
依賴選擇如下
2. 配置項目
更改thymeleaf版本
pom文件中
<properties>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
</properties>
更改thymeleaf解析模式 重要!
thymeleaf對html的檢查非常嚴格,容易出現無法解析的情況,而且不會告訴你具體是哪裏無法解析,這就很頭疼。不如降低檢查水平。
導入依賴
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
application.yml中更改模式
thymeleaf:
cache: false #關閉緩存方便調試
mode: LEGACYHTML5 #更改解析檢查模式
網上說改爲thymeleaf3之後就能降低解析難度,但是我發現有些情況還是會出錯,不如改成LEGACYHTML5 模式
連接數據庫
spring:
datasource:
url: jdbc:mysql://localhost:3306/blog?serverTimezone=UTC
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
配置日誌
logging:
level:
root: info
com.ddw: debug #指定com.ddw下進行debug級別的記錄
file:
name: log/blog.log #指定日誌存放目錄
異常處理
創建全局異常配置類
@ControllerAdvice // 作爲一個控制層的切面處理
public class GlobalException{
@ExceptionHandler(value = Exception.class) // 所有的異常都是Exception子類
public ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) {
ModelAndView mav = new ModelAndView();
mav.addObject("url", request.getRequestURL());
mav.addObject("exception", e);
mav.setViewName("error/5xx");
return mav;
}
}
要點
1.@ControllerAdvice 聲明切面類
2.@ExceptionHandler 聲明處理方法以及處理類型
3.setViewName(“error/5xx”); 返回到對應頁面
編寫5xx.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>5xx</title>
</head>
<body>
<h1>系統出現未知錯誤</h1>
<div>
<div th:utext="'<!--'" th:remove="tag"></div>
<div th:utext="'Failed Request URL : ' + ${url}" th:remove="tag"> </div>
<div th:utext="'Exception message : ' + ${exception.message}" th:remove="tag"></div>
<ul th:remove="tag">
<li th:each="st : ${exception.stackTrace}" th:remove="tag"><span th:utext="${st}" th:remove="tag"></span></li>
</ul>
<div th:utext="'-->'" th:remove="tag"></div>
</div>
</body>
</html>
編寫4xx.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>4xx</title>
</head>
<body>
<h1>頁面不存在</h1>
</body>
</html>
創建目錄如下,springboot會自動將4xx和5xx類型的錯誤對應跳轉到相應頁面
3. 定製日誌
導入AOP
<!--Aop依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
編寫日誌切面
package com.ddw.blog.aspect;
import org.apache.tomcat.util.http.fileupload.RequestContext;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Enumeration;
@Aspect
@Component
public class Log {
private final Logger logger = LoggerFactory.getLogger(Log.class);
@Pointcut("execution(* com.ddw.blog.controller..*.*(..))")
public void weblog(){}
@Before("weblog()")
public void doBefore(JoinPoint joinPoint){
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
logger.info("---------------request----------------");
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_Method : "+request.getMethod());
logger.info("IP : "+request.getRemoteAddr());
logger.info("Class_Method : "+joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
Enumeration<String> enu = request.getParameterNames();
while (enu.hasMoreElements()) {
String name = enu.nextElement();
logger.info("name:" + name + "value" + request.getParameter(name));
}
}
@AfterReturning(returning = "ret",pointcut = "weblog()")
public void doAfterReturning(Object ret){
logger.info("---------------response----------------");
logger.info("ResponseData : " +ret);
}
}
4. 關聯靜態頁面
直接將前端寫好的頁面按目錄對應放入springboot項目中之後,也會出現靜態資源無法找到的情況
這是因爲使用了thymeleaf模板。
只需要將無法找到的靜態資源用thymeleaf語法引入即可。
(也可以使用warjar引入方式)
但是,幾乎所有本地外部引用的資源都找不到,如果一個一個增加thymeleaf引入會非常麻煩。
因此,可以使用fragments替換。
編寫fragments,包含“大家共用的片段”
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head th:fragment="head(title)">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:replace="${title}">title</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css">
<link rel="stylesheet" href="../static/css/typo.css" th:href="@{/css/typo.css}">
<link rel="stylesheet" href="../static/css/animate.css" th:href="@{css/animate.css}">
<link rel="stylesheet" href="../static/lib/prism/prism.css" th:href="@{/lib/prism/prism.css}">
<link rel="stylesheet" href="../static/lib/tocbot/tocbot.css" th:href="@{/lib/tocbot/tocbot.css}">
<link rel="stylesheet" href="../static/css/me.css" th:href="@{/css/me.css}">
</head>
無論是thymeleaf的普通th語法替換,還是fragments替換,都能夠保持原有html,不需要對前端給的靜態頁面進行刪減,只需要增加一些thymeleaf語法實現動態數據替換,因此,thymeleaf也能實現前後端分離開發。
然後在其他head頁面中的head標籤內增加引用即可,不需要一一更改原有html引用。
<head th:replace="_fragments :: head(~{::title})">
<!--這裏是不同頁面head-->
</head>
th:replace=“fragments文件名 :: 替換fragment名(~{::替換標籤內容名})”
通過參數title,更改不同頁面的title
因此,所有公共部分,都做成fragment引入。
導航欄
通過參數n,更改不同頁面的選中狀態,th:classappend="${n==1} ? ‘active’",如果n==1,則增加一個class名“active”使之高亮。
<!--導航-->
<nav th:fragment="menu(n)" class="ui inverted attached segment m-padded-tb-mini m-shadow-small" >
<div class="ui container">
<div class="ui inverted secondary stackable menu">
<h2 class="ui teal header item">Blog</h2>
<a href="#" class="m-item item m-mobile-hide " th:classappend="${n==1} ? 'active'"><i class="mini home icon"></i>首頁</a>
<a href="#" class="m-item item m-mobile-hide" th:classappend="${n==2} ? 'active'"><i class="mini idea icon"></i>分類</a>
<a href="#" class="m-item item m-mobile-hide" th:classappend="${n==3} ? 'active'"><i class="mini tags icon"></i>標籤</a>
<a href="#" class="m-item item m-mobile-hide" th:classappend="${n==4} ? 'active'"><i class="mini clone icon"></i>歸檔</a>
<a href="#" class="m-item item m-mobile-hide" th:classappend="${n==5} ? 'active'"><i class="mini info icon"></i>關於我</a>
<div class="right m-item item m-mobile-hide">
<div class="ui icon inverted transparent input m-margin-tb-tiny">
<input type="text" placeholder="Search....">
<i class="search link icon"></i>
</div>
</div>
</div>
</div>
<a href="#" class="ui menu toggle black icon button m-right-top m-mobile-show">
<i class="sidebar icon"></i>
</a>
</nav>
底部
<!--底部footer-->
<footer th:fragment="footer" class="ui inverted vertical segment m-padded-tb-massive">
<div class="ui center aligned container">
<div class="ui inverted divided stackable grid">
<div class="three wide column">
<div class="ui inverted link list">
<div class="item">
<img src="../static/images/wechat.jpg" th:src="@{/images/wechat.jpg}" class="ui rounded image" alt="" style="width: 110px">
</div>
</div>
</div>
<div class="three wide column">
<h4 class="ui inverted header m-text-thin m-text-spaced " >最新博客</h4>
<div class="ui inverted link list">
<a href="#" class="item m-text-thin">用戶故事(User Story)</a>
<a href="#" class="item m-text-thin">用戶故事(User Story)</a>
<a href="#" class="item m-text-thin">用戶故事(User Story)</a>
</div>
</div>
<div class="three wide column">
<h4 class="ui inverted header m-text-thin m-text-spaced ">聯繫我</h4>
<div class="ui inverted link list">
<a href="#" class="item m-text-thin">Email:[email protected]</a>
<a href="#" class="item m-text-thin">QQ:865729312</a>
</div>
</div>
<div class="seven wide column">
<h4 class="ui inverted header m-text-thin m-text-spaced ">Blog</h4>
<p class="m-text-thin m-text-spaced m-opacity-mini">這是我的個人博客、會分享關於編程、寫作、思考相關的任何內容,希望可以給來到這兒的人有所幫助...</p>
</div>
</div>
<div class="ui inverted section divider"></div>
<p class="m-text-thin m-text-spaced m-opacity-tiny">Copyright © 2016 - 2017 Lirenmi Designed by Lirenmi</p>
</div>
</footer>
script
其實這個還是有些非公共部分,不過這裏用到的資源不是很多,統一導入影響不大,而且方便
<!--可以將所有的script放進一個div中,方便使用fragment功能。不過更推薦使用th自帶的bolck-->
<th:block th:fragment="script">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/[email protected]/jquery.scrollTo.min.js"></script>
<script src="../static/lib/prism/prism.js" th:src="@{/lib/prism/prism.js}"></script>
<script src="../static/lib/tocbot/tocbot.min.js" th:src="@{/lib/tocbot/tocbot.min.js}"></script>
<script src="../static/lib/qrcode/qrcode.min.js" th:src="@{/lib/qrcode/qrcode.min.js}"></script>
<script src="../static/lib/waypoints/jquery.waypoints.min.js" th:src="@{/lib/waypoints/jquery.waypoints.min.js}"></script>
</th:block>
注意,在原生html中,script使用bolck包裹起來的時候,最好使用特殊方法將其註釋掉,這樣不影響原生html代碼,也能使th代碼生效
<!--/*/<th:block th:replace="">/*/-->
只要在註釋內的標籤前後加上/*/即可
同理,給之前的錯誤頁面,也引入head和footer片段,實現美化。
5. 實體設計
關係抽象
實體類:
- 博客 Blog
- 博客分類 Type
- 博客標籤 Tag
- 博客評論 Comment
- 用戶 User
以Blog爲紐帶。
- type–blog,一對多(一種類型有多篇文章)
- tag–blog,多對多(多個標籤對應多篇文章)
- User–blog,一對多(一個用戶寫了多篇文章)
- Comment–bolg,多對一,(一篇文章被多人評論)
評論類的自關聯關係:
一條(父)評論可以被人多次回覆,一對多
屬性設計
雙環表明該屬性爲對象
6. 創建實體,生成數據庫表
JAP相關知識
-
@Entity 標註爲實體,實現對象到表的映射
-
@Table(name = “t_blog”) 設置表名
-
@Id
@GeneratedValue 設置id,自增 -
@Column(columnDefinition=“text”) 指定列屬性
-
@Temporal(TemporalType.TIMESTAMP)
Temporal註解用於幫java Data類型進行格式化,因爲java Data是util下的類- 第一種:@Temporal(TemporalType.DATE)——>實體類會封裝成日期“yyyy-MM-dd”的 Date類型。
- 第二種:@Temporal(TemporalType.TIME)——>實體類會封裝成時間“hh-MM-ss”的 Date類型。
- 第三種:@Temporal(TemporalType.TIMESTAMP)——>實體類會封裝成完整的時間“yyyy-MM-dd hh:MM:ss”的 Date類型。
-
mapperBy
1>只有OneToOne,OneToMany,ManyToMany上纔有mappedBy屬性,ManyToOne不存在該屬性;
2>mappedBy標籤一定是定義在被擁有方的,他指向擁有方;
3>mappedBy的含義,應該理解爲,擁有方能夠自動維護跟被擁有方的關係
4>mappedBy跟joinColumn/JoinTable總是處於互斥的一方,mappedBy這方定義JoinColumn/JoinTable總是失效的,不會建立對應的字段或者表。 -
@ManyToMany(cascade = {CascadeType.PERSIST})
private List<Tag> tags = new ArrayList<>();
上述兩行在Blog中,設置級聯新增。當新增文章的同時新增了標籤,則該標籤也會被增加到標籤表中 -
級聯CascadeType所有狀態
-
ALL
級聯所有實體狀態轉換 -
PERSIST
級聯實體持久化操作。 -
MERGE
級聯實體合併操作。 -
REMOVE
級聯實體刪除操作。 -
REFRESH
級聯實體刷新操作。 -
DETACH
級聯實體分離操作。
-
代碼如下(此處省略了set、get、空構造以及tostring方法)
@Entity
@Table(name = "t_blog")
public class Blog {
@Id
@GeneratedValue
private Long id;
private String title;
@Basic(fetch = FetchType.LAZY)
@Lob
private String content;
private String firstPicture;
private String flag;
private Integer views;
private boolean appreciation;
private boolean shareStatement;
private boolean commentabled;
private boolean published;
private boolean recommend;
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
@Temporal(TemporalType.TIMESTAMP)
private Date updateTime;
@ManyToOne
private Type type;
@ManyToMany(cascade = {CascadeType.PERSIST})
private List<Tag> tags = new ArrayList<>();
@ManyToOne
private User user;
@OneToMany(mappedBy = "blog")
private List<Comment> comments = new ArrayList<>();
@Basic(fetch = FetchType.LAZY)
@Lob
這兩個註解會將屬性映射爲LongText字段。太大所以進行懶加載
@Entity
@Table(name = "t_comment")
public class Comment {
@Id
@GeneratedValue
private Long id;
private String nickname;
private String email;
private String content;
private String avatar;
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
@ManyToOne
private Blog blog;
@OneToMany(mappedBy = "parentComment")
private List<Comment> replyComments = new ArrayList<>();
@ManyToOne
private Comment parentComment;
@Entity
@Table(name = "t_tag")
public class Tag {
@Id
@GeneratedValue
private Long id;
@NotBlank(message = "標籤名稱不能爲空")
private String name;
@ManyToMany(mappedBy = "tags")
private List<Blog> blogs = new ArrayList<>();
@Entity
@Table(name = "t_type")
public class Type {
@Id
@GeneratedValue
private Long id;
@NotBlank(message = "分類名稱不能爲空")
private String name;
@OneToMany(mappedBy = "type") //type是被擁有端,因此聲明mappedBy,對應字段是擁有端Blog中的外鍵名
private List<Blog> blogs = new ArrayList<>();
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue
private Long id;
private String nickname;
private String username;
private String password;
private String email;
private String avatar;
private Integer type;
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
@Temporal(TemporalType.TIMESTAMP)
private Date updateTime;
@OneToMany(mappedBy = "user")
private List<Blog> blogs = new ArrayList<>();
注意評論自關聯的部分,同一個實體中
//當前實體作爲parentComment時,包含多個子類對象,mappedBy 寫在子類對象上
@OneToMany(mappedBy = "parentComment")
private List<Comment> replyComments = new ArrayList<>();
//當前實體作爲replyComments時,多個replyComments對應一個父類對象
@ManyToOne
private Comment parentComment; //
備註,我沒看懂,我感覺這兩個註解應該交換一下(mapperdBy的位置不換)
運行項目,生成數據表
hibernate_sequence用於記錄表的主鍵
t_blog_tags是多對多關係的中間表
其他都是意料之中的表
擁有mapperBy的一方被稱爲“被擁有方”,該方不會生成xxx_id字段,而是擁有方纔會生成xxx_id字段
如t_blog表中有user_id,但是t_user表中沒有blog_id
多對多關係也不會在各表中生成xxx_id,而是生成中間表
@NotBlank(message = “分類名稱不能爲空”)是後端數據校驗功能
7. admin後臺
7.1 登陸管理頁面
dao層
public interface UserRepository extends JpaRepository<User,Long> {
User findByUsernameAndPassword(String username,String password);
}
JpaRepository<操作對象,主鍵類型>
裏面的查詢方法符合jpa命名規範即可。
service層
接口
public interface UserService {
public User checkUser(String username,String password);
}
實現
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserRepository userRepository;
@Override
public User checkUser(String username, String password) {
return userRepository.findByUsernameAndPassword(username,password);
}
}
控制器
@Controller
@RequestMapping("/admin")
public class LoginController {
@Autowired
UserServiceImpl userService;
@GetMapping
String loginPage(){
return "admin/login";
}
@PostMapping("/login")
String login(@RequestParam String username,
@RequestParam String password,
HttpSession session,
RedirectAttributes attributes){
User user = userService.checkUser(username,password);
if (user!=null){
user.setPassword(null); //保存session,進行安全處理
session.setAttribute("user",user);
return "admin/index";
}
attributes.addFlashAttribute("message", "用戶名或密碼錯誤");
return "redirect:/admin"; //重定向到admin地址
}
@GetMapping("logout")
String loginout(HttpSession session){
session.removeAttribute("user");
return "redirect:/admin"; //重定向到admin地址
}
}
登陸成功使用轉發:轉發能夠保存一次會話的數據。
登錄失敗使用重定向:重定向會清空數據。
補充筆記(重點)
1. 控制器之間的數據交互
- (1)Request.getServletContext() 單例,一個應用在運行期間共享一個servletContext
- (2)通過轉發傳遞request (瀏覽器url不變,只顯示轉發前那個請求,請求一次)
- ----return “forward:/admin” (轉發到不同控制器補全相對路徑即可)
- (3)通過重定向
- ----return “redirect:/admin”
2. 轉發和重定向的區別
- (1)涉及到數據操作(數據提交,增刪改)時,使用重定向。若使用轉發,頁面重載時會重新加載數據操作。轉發僅用於後臺邏輯操作,保證頁面不變
- (2)轉發之後,瀏覽器中URL仍然指向開始頁面,此時如果重載當前頁面,開始頁面將會被重新調用。
- (3)轉發爲同一個請求,重定向爲新的請求
- ①forword:直接到目標頁面,本頁面的所有響應都無效
- ②include:順序進行響應,進入include的頁面執行完後再返回本頁面繼續響應
轉發和重定向都是面向控制器路由的(即action路徑),而非模板映射路徑;
由於本節控制器中沒有專用於登陸成功的控制器,因此此處沒有使用轉發,而是通過模板映射。
3. 前後端的數據交互
- (1)控制器的參數對應表單提交的參數即可自動實現注入;若爲model,也能實現自動注入
- ①使用總結:控制器中形參的類型,在表單中直接提交形參類型的屬性即可。比如:
- 1)形參爲User,表單直接提交User中的username、
- 2)形參爲UserExt,表單直接提交UserExt中的map(infos[‘key’])、list(userList[0])
- ②若用axios傳輸,則只有js使用data傳輸數據(在uri中),控制器能用對應參數自動注入。否則只能用@RequestBody Map接收
- ①使用總結:控制器中形參的類型,在表單中直接提交形參類型的屬性即可。比如:
- (2)@RequestParam:接收請求頭(少量數據)
- ①Content-Type爲application/x-www-form-urlencoded編碼的內容,或者其他類型的請求
- (3)@RequestBody:大量數據,json,xml等非application/x-www-form-urlencodeed
- ①作用在形參列表上,用於將前臺發送過來固定格式的數據【xml 格式或者 json等】封裝爲對應的 JavaBean 對象,封裝時使用到的一個對象是系統默認配置的 HttpMessageConverter進行解析,然後封裝到形參上
- ②由於RequestBody實際從HttpEntity中獲取數據,而Get請求沒有HttpEntity,因此不適用。
- ③並非驗證是否爲請求體,而是接收application/合適的請求
- ④(model接收)
- 1)可以在model中的屬性上增加@JsonAlias實現別名
- 2)在model屬性上增加@JsonProperty實現唯一標準名(與前端提交的相比較)
- ⑤如果前端傳遞的不是json,又需要將其封裝爲model,使用反射或者
- (4)@Controller@ResponseBody / @RestController
- ①被此註解修飾的方法的return會把數據直接發送到請求體中,而不會被解析爲路徑(常用於發送json數據)
- ②如果返回的是字符串,則前端直接收到html原生代碼串
相關注意點
request.getAttribute 在一個request中傳遞對象
request.getSession().getAttribute 在一個session中傳遞對象
public String userLogin(@RequestParam("username")String username,@RequestParam("password")String password){
等價於
public String userLogin(HttpServletRequest request){
String username = request.getParameter("username");
String password = request.getParameter("password");
@RestController註解下不能訪問靜態資源(如圖標)
千萬千萬!!不要將HttpServletRequest傳遞到任何異步方法中!比如axios傳遞參數到@requestParams中(改成request.getParam可以)
查看原因
MD5加密
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Utils {
/**
* MD5加密類
* @param str 要加密的字符串
* @return 加密後的字符串
*/
public static String code(String str){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[]byteDigest = md.digest();
int i;
StringBuilder buf = new StringBuilder();
for (byte b : byteDigest) {
i = b;
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
//32位加密
return buf.toString();
// 16位的加密
//return buf.toString().substring(8, 24);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
}
UserServiceImpl中
public User checkUser(String username, String password) {
return userRepository.findByUsernameAndPassword(username, MD5Utils.code(password));
}
登錄攔截器
攔截器
package com.ddw.blog.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AdminInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getSession().getAttribute("user")==null){
response.sendRedirect("/admin");
return false;
}
return true;
}
}
註冊攔截器
package com.ddw.blog.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AdminInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin") //防止無限循環重定向進入admin
.excludePathPatterns("/admin/login"); //表單提交不能被攔截
}
}
7.2 分類、標籤管理
dao層
tag
public interface TagRepository extends JpaRepository<Tag,Long> {
Tag findByName(String name);
}
type
public interface TypeRepository extends JpaRepository<Type,Long> {
Type findByName(String name);
}
service層
tag
接口
public interface TagService {
Tag saveTag(Tag tag);
Tag getTag(Long id);
Tag getTagByName(String name);
Page<Tag> listTag(Pageable pageable);
Tag updateTag(Long id, Tag tag);
void deleteTag(Long id);
}
實現
@Service
public class TagServiceImpl implements TagService {
@Autowired
TagRepository tagRepository;
@Transactional
@Override
public Tag saveTag(Tag tag) {
return tagRepository.save(tag);
}
@Override
public Tag getTag(Long id) {
return tagRepository.getOne(id);
}
@Override
public Tag getTagByName(String name) {
return tagRepository.findByName(name);
}
@Override
public Page<Tag> listTag(Pageable pageable) {
return tagRepository.findAll(pageable);
}
@Transactional
@Override
public Tag updateTag(Long id, Tag tag) {
Tag tmp = getTag(id);
if (tmp==null){
throw new NotFoundException("標籤不存在");
}
BeanUtils.copyProperties(tag,tmp);
return tagRepository.save(tmp);
}
@Transactional
@Override
public void deleteTag(Long id) {
tagRepository.deleteById(id);
}
}
getOne返回一個實體的引用,無結果會拋出異常;
findById返回一個Optional對象;
findOne返回一個Optional對象,可以實現動態查詢;
Optional代表一個可能存在也可能不存在的值。
Page<分頁實體> list(Pageable pageable); springboot會自動將數據封裝爲一頁
當前端(更改)傳輸page的屬性時,控制器會接收到,比如前端點擊上一頁時,設置(page=${page.number}-1),則前端也會根據更改後的頁碼進行分頁查詢(比如本項目中的tag和type分頁)
如果是複雜分頁,則不能通過前端更改page頁碼實現動態查詢(比如本項目中的bolgs頁面)
type
接口
public interface TypeService {
Type saveType(Type type);
Type getType(Type type);
Type getTypeByName(String name);
Page<Type> listType(Pageable pageable);
Type updateType(Long id,Type type);
void deleteType(Long id);
}
實現
把上面tag的實現類中的“Tag”換成“Type”即可
自定義一個未找到異常
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException{
public NotFoundException() {
}
public NotFoundException(String message) {
super(message);
}
public NotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
控制器
type
package com.ddw.blog.controller.admin;
import com.ddw.blog.model.Type;
import com.ddw.blog.service.TypeServiceImpl;
import com.sun.org.apache.xpath.internal.operations.Mod;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
@Controller
@RequestMapping("/admin")
public class TypeController {
/**
* 涉及到的操作有: 方式 路由 頁面 描述
* 1. 訪問所有類型的頁面 get /types admin/types 通過分頁查詢展示所有類型
* 2. 訪問新增類型的頁面 get /types/input admin/types-input 在/types頁面單擊“新增”跳轉到本頁面
* 3. 編輯標籤請求 get /tags/{id}/input admin/tags-input 在/types頁面單擊“編輯”跳轉到本頁面,並進行數據回顯
* 4. 更新(添加)標籤請求 post /tags{id} admin/tags 在頁面獲得id,通過id進行更新
* 5. 刪除標籤請求 delete /tags/{id} admin/tags
*/
@Autowired
TypeServiceImpl typeService;
@GetMapping("/types")
public String types(@PageableDefault(size = 3,sort = {"id"},direction = Sort.Direction.DESC)
Pageable pageable, Model model){
// 按3條一頁的形式分寫,排序方式按id降序
// springboot會根據前端的參數封裝好pageable
model.addAttribute("page",typeService.listType(pageable));
return "admin/types";
}
@GetMapping("types/input")
public String input(Model model){
//這裏的model添加type,是爲了讓前端頁面能夠拿到一個type對象,然後進行數據校驗
model.addAttribute("type",new Type());
return "admin/types-input";
}
@GetMapping("/types/{id}/input")
public String editInput(@PathVariable Long id, Model model){
//數據回顯
model.addAttribute("type",typeService.getType(id));
return "admin/types-input";
}
@PostMapping("/types")
public String post(@Valid Type type, BindingResult result, RedirectAttributes attributes){
//進行重複校驗
Type tmp = typeService.getTypeByName(type.getName());
if (tmp!=null){
//這句話會將nameError加入到result中,因此,下面result.hasErrors()爲true,這裏不用return,
result.rejectValue("name","nameError","重複添加!");
}
//校驗,結合實體註解校驗
if(result.hasErrors()){
return "admin/types-input";
}
Type tmp2 = typeService.saveType(type);
if (tmp2==null){
attributes.addFlashAttribute("message","新增失敗");
}else{
attributes.addFlashAttribute("message","新增成功");
}
return "redirect:/admin/types";
}
@PostMapping("/types/{id}")
public String editPost(@Valid Type type, BindingResult result, @PathVariable Long id,RedirectAttributes attributes){
Type tmp = typeService.getTypeByName(type.getName());
if(tmp!=null){
result.rejectValue("name","nameError","重複添加!");
}
if(result.hasErrors()){
return "admin/types-input";
}
Type tmp2 = typeService.updateType(id,type);
if (tmp2==null){
attributes.addFlashAttribute("message","新增失敗");
}else{
attributes.addFlashAttribute("message","新增成功");
}
return "redirect:/admin/types";
}
@GetMapping("/types/{id}/delete")
public String delete(@PathVariable Long id,RedirectAttributes attributes){
typeService.deleteType(id);
attributes.addFlashAttribute("message","刪除成功");
return "redirect:/admin/types";
}
}
tag的跟這個幾乎完全一樣,就不貼了
注意
爲什麼要用重定向:admin/types中使用了分頁查詢,如果直接跳轉,會導致無法看到最新數據
JPA封裝的page數據格式
content中的內容是實體的屬性鍵值對,其他都是固定的
page
{
"content":[
{"id":123,"title":"blog122","content":"this is blog content"},
{"id":122,"title":"blog121","content":"this is blog content"},
],
"last":false, //是否是最後一頁
"totalPages":9,
"totalElements":123,
"size":15, //每頁數據條數
"number":0, //當前頁
"first":true,
"sort":[{
"direction":"DESC",
"property":"id",
"ignoreCase":false,
"nullHandling":"NATIVE",
"ascending":false
}],
"numberOfElements":15 //當前頁的數據有多少條
}
後端校驗
假設運行流程:
首頁單擊鏈接,通過A控制器,到達目標頁面
目標頁面輸入信息,提交請求到B控制器
實體類爲Type
-
實體類中增加校驗註解(以name上面校驗爲例)
@NotBlank(message = “不能爲空”)是後端數據校驗功能
String name; -
A控制器中放入一個空的實體
model.addAttribute(“type”,new Type()); -
B控制器中進行校驗(這裏只保留了校驗錯誤發生時所需的代碼)
public String post(@Valid Type type,BindingResult result) {
//如果result中存在校驗錯誤,則返回到輸入頁面
if (result.hasErrors()) {
return "admin/types-input";
}
}
@Valid Type type表示對type進行校驗,校驗方式就是我們在該實體類中所標註的校驗註解
BindingResult result 接收校驗之後的結果
- 前端頁面顯示校驗結果(message)
- 前端頁面的form標籤上聲明這個實體
<form action="#" method="post" th:object="${type}">
- 在該form中準備輸入被校驗的值(name)上聲明校驗
<input type="text" name="name" placeholder="分類名稱" th:value="*{name}" >
- 顯示校驗結果
fields.hasErrors(‘name’) 中的name是指被校驗註解標註的字段名<!--/*/ <div th:if="${#fields.hasErrors('name')}" > <div class="header">驗證失敗</div> <p th:errors="*{name}">提交信息不符合規則</p> </div> /*/-->
*{name}中的星號可以理解爲object
- 前端頁面的form標籤上聲明這個實體
此外,通過BindingResult 還可以自定義錯誤校驗,繞過註解校驗
如:如果用戶輸入的名字重複了,可以通過result進行返回錯誤,顯示方法跟上述第4步一致。
Type type = typeService.getTypeByName(type.getName());
if (type != null) {
result.rejectValue("name","nameError","不能添加重複的分類");
}
result.rejectValue(“校驗字段名”,“自定義錯誤名”,“前端返回錯誤信息”);
作用機制流程
首先在實體類上標註校驗
然後將用戶輸入的信息放入控制器準備的空實體
該實體會被傳輸到後臺,後臺進行校驗,並返回校驗結果
注意,@Valid 實體類和BindingResult必須挨着,不然無效
7.3 博客管理(含重要註釋)
Dao
public interface BlogRepository extends JpaRepository<Blog,Long>, JpaSpecificationExecutor<Blog> {
}
Service層
接口
public interface BlogService {
Blog saveBlog(Blog blog);
Blog getBlog(Long id);
Page<Blog> listBlog(Pageable pageable, BlogQuery blog); //這個用於複雜分頁查詢
Page<Blog> listBlog(Pageable pageable); //這個用於剛進入博客時展示所有博客
Blog updateBlog(Long id, Blog blog);
void deleteBlog(Long id);
}
實現
package com.ddw.blog.service;
import com.ddw.blog.dao.BlogRepository;
import com.ddw.blog.exception.NotFoundException;
import com.ddw.blog.po.Blog;
import com.ddw.blog.po.Type;
import com.ddw.blog.utils.MyBeanUtils;
import com.ddw.blog.vo.BlogQuery;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.xml.crypto.Data;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class BlogServiceImpl implements BlogService {
@Autowired
BlogRepository BlogRepository;
@Override
public Blog getBlog(Long id) {
return BlogRepository.getOne(id);
}
@Override
public Page<Blog> listBlog(Pageable pageable, BlogQuery blog) {
return BlogRepository.findAll(new Specification<Blog>() {
@Override
public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>(); //存放查詢條件
if(!"".equals(blog.getTitle()) && blog.getTitle()!=null) //如果標題不爲空
predicates.add(cb.like(root.<String>get("title"),"%"+blog.getTitle()+"%"));
if(blog.getTypeId()!=null) //如果標籤不爲空
predicates.add(cb.equal(root.<Type>get("type").get("id"),blog.getTypeId()));
if(blog.isRecommend())
predicates.add(cb.equal(root.<Boolean>get("recommend"),blog.isRecommend()));
cq.where(predicates.toArray(new Predicate[predicates.size()])); //cq.where必須傳一個數組
return null;
}
},pageable);
}
@Transactional
@Override
public Blog saveBlog(Blog Blog) {
//初始化文章,傳過來的文章並沒有對時間進行處理
Blog.setCreateTime(new Date());
Blog.setUpdateTime(new Date());
Blog.setViews(0);
//如果將更新和插入方法公用,會出現錯誤:A數據原來有abc字段,當更新時,更新了ab,如果傳過來的數據不包含c,那c會被置爲null
return BlogRepository.save(Blog);
}
@Override
public Page<Blog> listBlog(Pageable pageable) {
return BlogRepository.findAll(pageable);
}
@Transactional
@Override
public Blog updateBlog(Long id, Blog Blog) {
Blog tmp = getBlog(id);
if (tmp==null){
throw new NotFoundException("文章不存在");
}
//如果直接將前端傳來的blog copy 給數據庫查到的tmp,則blog中的null會覆蓋tmp原來有數據的字段
//因此,要忽略掉blog中屬性值爲空的字段
BeanUtils.copyProperties(Blog,tmp, MyBeanUtils.getNullPropertyNames(Blog));
tmp.setUpdateTime(new Date());
return BlogRepository.save(tmp);
}
@Transactional
@Override
public void deleteBlog(Long id) {
BlogRepository.deleteById(id);
}
}
控制器
package com.ddw.blog.controller.admin;
import com.ddw.blog.po.Blog;
import com.ddw.blog.po.User;
import com.ddw.blog.service.BlogService;
import com.ddw.blog.service.TagService;
import com.ddw.blog.service.TypeService;
import com.ddw.blog.vo.BlogQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/admin")
public class BlogController {
@Autowired
BlogService blogService;
@Autowired
TypeService typeService;
@Autowired
TagService tagService;
@GetMapping("/blogs") //進入文章管理頁面
public String blogs(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC)Pageable pageable,
Model model) {
model.addAttribute("page",blogService.listBlog(pageable));
model.addAttribute("types",typeService.listType());
return "/admin/blogs";
}
@PostMapping("/blogs/search") //單擊查詢
public String search(@PageableDefault(size = 2, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
BlogQuery blog, Model model) {
//直接翻頁的時候,調用的也是這個,此時BlogQuery爲空,直接食用pageable進行查詢
model.addAttribute("page", blogService.listBlog(pageable, blog));
return "/admin/blogs :: blogList"; //部分刷新
}
//公用方法,拿到所有的type和tag保存在模板引擎
//用於給用戶從所有type和tag中進行選擇
private void setTypeAndTag(Model model){
model.addAttribute("types",typeService.listType());
model.addAttribute("tags",tagService.listTag());
}
//編輯文章頁面
@GetMapping("/blogs/{id}/input")
public String editInput(@PathVariable Long id,Model model){
//數據回顯
setTypeAndTag(model);
Blog blog = blogService.getBlog(id);
//由於前端標籤選擇欄的多個tags形式爲“1,2,3”,因此需要額外給blog實體增加一個tagIds保存字符串
// 並提供一個方法將list<tag>轉化爲String tagIds
blog.init();
model.addAttribute("blog",blog);
return "/admin/blogs-input";
}
//進入新增頁面
@GetMapping("/blogs/input")
public String input(Model model){
setTypeAndTag(model);
//由於新增頁面和編輯頁面共用了一個頁面,因此爲了保證頁面解析正確,這裏加一個空對象
model.addAttribute("blog",new Blog());
return "admin/blogs-input";
}
//保存/發佈文章請求進入這裏
@PostMapping("/blogs")
public String post(Blog blog, RedirectAttributes attributes, HttpSession session){
//傳遞過來的blog只包含title,type,tagIds,圖片,content等;這裏進行初始化
//這句話是爲了設置博客的作者,如果不加也沒關係,不過數據庫中blog對應的user_id爲null
blog.setUser((User) session.getAttribute("user"));
blog.setType(typeService.getType(blog.getType().getId()));
blog.setTags(tagService.listTag(blog.getTagIds())); //按照前端傳過來的tag“1,2,3”查詢標籤
Blog b;
if (blog.getId()==null)
b = blogService.saveBlog(blog);
else
b = blogService.updateBlog(blog.getId(),blog);
if (b ==null){
attributes.addFlashAttribute("message","操作失敗");
}else {
attributes.addFlashAttribute("message","操作成功");
}
return "redirect:/admin/blogs";
}
@GetMapping("/blogs/{id}/delete")
public String delete(@PathVariable Long id, RedirectAttributes attributes){
blogService.deleteBlog(id);
attributes.addFlashAttribute("message","刪除成功");
return "redirect:/admin/blogs";
}
}
簡單分頁查詢
- Dao—提供繼承JpaRepository的接口
public interface BlogRepository extends JpaRepository<Blog,Long> { }
- Service—提供分頁查詢方法,使用findAll(Pageable)方法
public Page<Blog> listBlog(Pageable pageable, BlogQuery blog) { return BlogRepository.findAll(new Specification<Blog>() { @Override public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> cq, CriteriaBuilder cb) { List<Predicate> predicates = new ArrayList<>(); //存放查詢條件 if(!"".equals(blog.getTitle()) && blog.getTitle()!=null) //如果標題不爲空 predicates.add(cb.like(root.<String>get("title"),"%"+blog.getTitle()+"%")); if(blog.getTypeId()!=null) //如果標籤不爲空 predicates.add(cb.equal(root.<Type>get("type").get("id"),blog.getTypeId())); if(blog.isRecommend()) predicates.add(cb.equal(root.<Boolean>get("recommend"),blog.isRecommend())); cq.where(predicates.toArray(new Predicate[predicates.size()])); //cq.where必須傳一個數組 return null; } },pageable); }
- Controller—接收Pageable對象,並利用Service中的分頁查詢方法查詢page,保存到視圖中
@GetMapping("/tags") public String tags(@PageableDefault(size = 3,sort = {"id"},direction = Sort.Direction.DESC) Pageable pageable, Model model) { model.addAttribute("page",tagService.listTag(pageable)); return "admin/tags"; }
機制:
- 1.(第一次)前端訪問控制器,控制器初始化Pageable對象,初始化相應的size、sort等page信息
- 2.控制器中將Pageable中的信息傳遞給Service中的分頁查詢方法,查詢返回一個Page
- 3.控制器將該Page放入視圖中,傳遞到模板引擎,模板引擎渲染數據到視圖,返回給前端。
- 4.(第一次之後)前端進行翻頁(${page.number}+1),控制器利用前端傳遞過來的翻頁信息和控制器聲明的信息對Pageable對象進行初始化
- 5.重複2~3
複雜分頁查詢
機制:
- 1.(第一次)前端訪問控制器,控制器初始化Pageable對象,初始化相應的size、sort等page信息,初始化查詢vo,此時vo爲空,查詢結構爲空
- 2.前端進行條件搜索,搜索條件作爲vo發送給控制器,同時攜帶了Pageable信息
- 3.控制器中將Pageable中的信息和vo傳遞給Service中的分頁查詢方法,查詢返回一個Page
- 4.控制器將該Page放入視圖中,傳遞到模板引擎,模板引擎渲染數據到視圖,返回給前端。
注意:分頁結果是一個完整的po,分頁查詢條件是一個vo。因此前端進行翻頁的時候,除了將page的頁碼信息(${page.number}+1)傳遞給控制器,還得將vo傳遞給控制器
- Dao—提供繼承JpaRepository和JpaSpecificationExecutor接口
public interface BlogRepository extends JpaRepository<Blog,Long>, JpaSpecificationExecutor<Blog> { }
- Service—提供分頁查詢方法,加上覆雜分頁查詢vo,使用findAll(Specification,Pageable)方法
Page<Blog> listBlog(Pageable pageable,BlogQuery blog){ blogRepository.findAll(pageable); };
- Controller—接收Pageable對象,並利用Service中的分頁查詢方法查詢page,保存到視圖中
@PostMapping("/blogs/search") //單擊查詢 public String search(@PageableDefault(size = 8, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable, BlogQuery blog, Model model) { model.addAttribute("page", blogService.listBlog(pageable, blog)); return "/admin/blogs :: blogList"; //部分刷新 }
Predicate:動態查詢條件的容器
Root:查詢對象,可以從中獲取到表的字段
CriteriaBuilder:設置條件表達式
CriteriaQuery:進行查詢
8. 前端展示
8.1 首頁展示
控制器
package com.ddw.blog.controller;
import com.ddw.blog.po.Blog;
import com.ddw.blog.po.Tag;
import com.ddw.blog.po.Type;
import com.ddw.blog.service.BlogService;
import com.ddw.blog.service.TagService;
import com.ddw.blog.service.TypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@Controller
public class IndexController {
@Autowired
BlogService blogService;
@Autowired
TagService tagService;
@Autowired
TypeService typeService;
@GetMapping("/")
public String index(@PageableDefault(size = 8,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable,
Model model ){
//數據回顯
Page<Blog> blogs = blogService.listBlog(pageable);
model.addAttribute("page",blogs);
List<Type> types = typeService.listTypeTop(6);
model.addAttribute("types",types);
List<Tag> tags = tagService.listTagTop(10);
model.addAttribute("tags",tags);
//如果是templates下的文件夾下的html,則文件夾需要加/,如果直接是templates下的html,則不用加/
return "index";
}
@GetMapping("/blog/{id}")
public String blog(@PathVariable Long id,Model model){
Blog blog = blogService.getBlog(id);
model.addAttribute("blog",blog);
return "blog";
}
}
需要增加“按博客數量排序返回前n個type\tag對象”的方法。
這裏以返回type爲例。
Dao
public interface TypeRepository extends JpaRepository<Type,Long> {
Type findByName(String name);
//傳入pageable對象,通過自定義查詢,查找到所有的type,放入List<Type>中
@Query("select t from Type t")
List<Type> findTop(Pageable pageable);
}
service實現
視頻給的方法已經過期了,這個是查看文檔更改的方法
@Override
public List<Type> listTypeTop(Integer size) {
//按type中的<List>blogs.size 降序排序
Sort sort = Sort.by(Sort.Direction.DESC,"blogs.size");
//從0~size,按sort方法生成分頁
Pageable pageable = PageRequest.of(0,size,sort);
return TypeRepository.findTop(pageable);
}
8.2 全局search
Dao
public interface BlogRepository extends JpaRepository<Blog,Long>, JpaSpecificationExecutor<Blog> {
//從Blog中查詢,按blog的title或者content與參數1進行相似比較
@Query("select b from Blog b where b.title like ?1 or b.content like ?1")
Page<Blog> findByQuery(String query,Pageable pageable);
}
service
@Override
public Page<Blog> listBlog(String query, Pageable pageable) {
return BlogRepository.findByQuery(query,pageable);
}
控制器
//全局搜索
@PostMapping("/search")
public String search(@PageableDefault(size = 8,sort = {"updateTime"},direction = Sort.Direction.DESC)Pageable pageable,
@RequestParam String query,Model model){
//jpa的Query不會自動處理like查詢所需的百分號,這裏手動加上
model.addAttribute("page",blogService.listBlog("%"+query+"%",pageable));
model.addAttribute("query",query);
return "search";
}
搜索按鈕是一個i標籤,因此需要綁定submit方法
8.3 文章詳情頁
雖然提供了markdown文本編輯器,但是提交到數據的內容還是markdown文本,而實際展示頁面要有markdown的樣式的化是需要轉爲html文本的。
因此,這裏增加markdown轉html的功能
導入依賴
<!-- markdown轉html-->
<!--基本包-->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.10.0</version>
</dependency>
<!--處理head,使其生成id實現頁內跳轉和頁內目錄-->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-heading-anchor</artifactId>
<version>0.10.0</version>
</dependency>
<!--處理table-->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>0.10.0</version>
</dependency>
<!-- markdown轉html end-->
Utils
生成一個markdown轉html的Utils工具
package com.ddw.blog.utils;
import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.ext.heading.anchor.HeadingAnchorExtension;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.AttributeProvider;
import org.commonmark.renderer.html.AttributeProviderContext;
import org.commonmark.renderer.html.AttributeProviderFactory;
import org.commonmark.renderer.html.HtmlRenderer;
import java.util.*;
public class MarkdownUtils {
/**
* markdown格式轉換成HTML格式的基本語法
* @param markdown
* @return
*/
public static String markdownToHtml(String markdown) {
Parser parser = Parser.builder().build();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
}
/**
* 增加擴展[標題錨點,表格生成]
* Markdown轉換成HTML
* @param markdown
* @return
*/
public static String markdownToHtmlExtensions(String markdown) {
//h標題生成id
Set<Extension> headingAnchorExtensions = Collections.singleton(HeadingAnchorExtension.create());
//轉換table的HTML
List<Extension> tableExtension = Arrays.asList(TablesExtension.create());
Parser parser = Parser.builder()
.extensions(tableExtension)
.build();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder()
.extensions(headingAnchorExtensions)
.extensions(tableExtension)
.attributeProviderFactory(new AttributeProviderFactory() {
public AttributeProvider create(AttributeProviderContext context) {
return new CustomAttributeProvider();
}
})
.build();
return renderer.render(document);
}
/**
* 處理標籤的屬性
*/
static class CustomAttributeProvider implements AttributeProvider {
@Override
public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
//改變a標籤的target屬性爲_blank
if (node instanceof Link) {
attributes.put("target", "_blank");
}
if (node instanceof TableBlock) {
attributes.put("class", "ui celled table");
}
}
}
public static void main(String[] args) {
String table = "| hello | hi | 哈哈哈 |\n" +
"| ----- | ---- | ----- |\n" +
"| 斯維爾多 | 士大夫 | f啊 |\n" +
"| 阿什頓發 | 非固定杆 | 撒阿什頓發 |\n" +
"\n";
String a = "[imCoding 愛編程](http://www.lirenmi.cn)";
System.out.println(markdownToHtmlExtensions(a));
}
}
service
@Override
public Blog getAndConvert(Long id) {
Blog blog = BlogRepository.getOne(id);
if (blog==null)
throw new NotFoundException("該博客不存在");
//爲了避免將數據庫中的markdown文本也轉換成html,這裏用一個臨時的blog接收並轉換
Blog tmp = new Blog();
BeanUtils.copyProperties(blog,tmp);
tmp.setContent(MarkdownUtils.markdownToHtmlExtensions(tmp.getContent()));
return tmp;
}
控制器
@GetMapping("/blog/{id}")
public String blog(@PathVariable Long id,Model model){
//放入markdown被轉換爲html的blog
model.addAttribute("blog",blogService.getAndConvert(id));
return "blog";
}
8.4 詳情評論
controller
package com.ddw.blog.controller;
import com.ddw.blog.po.Comment;
import com.ddw.blog.po.User;
import com.ddw.blog.service.BlogService;
import com.ddw.blog.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpSession;
@Controller
public class CommentController {
/*
實現功能:
1.當用戶訪問blog/{id}時,載入時頁面獲取id,發起ajax請求到comments/{id},實現刷新評論區
2.comment-container加載時,發起ajax請求,查看是否時博主登陸,進行信息回顯(不知道爲什麼,這個沒生效)
3.用戶發佈評論之後,局部重定向,刷新評論區
*/
@Autowired
private CommentService commentService;
@Autowired
private BlogService blogService;
//這裏爲所有用戶配置一個頭像,讀取配置文件,寫死一個
@Value("${comment.avatar}")
private String avatar;
//刷新評論區
@GetMapping("/comments/{blogId}")
public String comments(@PathVariable Long blogId, Model model){
model.addAttribute("comments",commentService.listCommentByBlogId(blogId));
return "blog::commentList";
}
//用戶評論是進入這裏
@PostMapping("/comments")
public String post(Comment comment, HttpSession session){
Long blogId = comment.getBlog().getId();
comment.setBlog(blogService.getBlog(blogId));
// comment.setBlog(comment.getBlog());
User user = (User) session.getAttribute("user");
//如果當前是博主在訪問,那就設置博主訪問信息
if (user!=null){
comment.setAvatar(user.getAvatar());
comment.setAdminComment(true);
}else {
comment.setAvatar(avatar);
}
commentService.saveComment(comment);
return "redirect:/comments/"+blogId;
}
}
ServiceImpl
這個涉及到稍微複雜的邏輯,我改寫了視頻給的方法。
此外,視頻說要操作通過findByBlogIdAndParentCommentNull查出來的數據的拷貝,不然會影響數據庫的數據,這個邏輯可能是錯的。
findByBlogIdAndParentCommentNull查出來的數據已經放在內存當中了,對數據庫應該不會造成影響。
我猜測是不是緩存刷新會導致數據庫的數據被刷新?如果有人知道,敬請留言。
此外,作者多次使用BeanUtils的copy功能,操作數據備份,我回去檢查了下,有一些跟這裏的邏輯一樣,似乎也不需要。
package com.ddw.blog.service;
import com.ddw.blog.dao.CommentRepository;
import com.ddw.blog.po.Comment;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class CommentServiceImpl implements CommentService {
@Autowired
CommentRepository commentRepository;
@Override
public Comment saveComment(Comment comment) {
// 當前端傳來一個評論時,判斷它是否時頂級回覆,如果是頂級回覆,則設置parentComment爲null
// 否則通過它的parentCommentId查詢它的父級評論,初始化它的相關信息
Long parentCommentId = comment.getParentComment().getId();
if (parentCommentId!=-1)
comment.setParentComment(commentRepository.getOne(parentCommentId));
else
comment.setParentComment(null);
comment.setCreateTime(new Date());
return commentRepository.save(comment);
}
/**
* 結構閉包分析:
* 根節點評論A,擁有子節點評論B;子節點評論B,擁有子節點評論C。
*
* 結構層次:
* A(B1,B2,...Bn),B(C1,C2,...Cn)...
*
* 算法目標:
* A(B1,...Bn,C1,...Cn,D1,...Dn,...)
*
* 處理邏輯:
* 0. 創建子節點容器(存放迭代找出的所有子代的集合)
* 1. 拿到所有根節點As
* 2. 遍歷As,拿到A;通過A,拿到它的子節點Bs,
* 3. 遍歷Bs,拿到B,將B放入子節點容器;通過B,拿到它的子節點Cs
* 4. 遍歷Cs,拿到C,將C放入子節點容器;通過C,拿到它的子節點Ds
* 5. ......
* 6. 當Ns爲空時結束
* 7. 將As的所有A的子節點改成子節點容器,清空子節點容器
* 8. 返回As
*
* 上述算法可以通過遞歸實現
* 0. 創建子節點容器(存放迭代找出的所有子代的集合)
* 1. 拿到所有根節點As
* 2. 遍歷As,拿到A;通過A,拿到它的子節點Bs;
* 3. 遍歷Bs,拿到B,如果B不爲空,將B放入子節點容器中,並拿到他們的子節點Cs,
* 4. 遞歸調用第三步(此時傳入的參數Bs=Ns,N=(C,D,E...))
* 5. 將As的所有A的子節點改成子節點容器,清空子節點容器
* 6. 返回As
*/
//0. 創建子節點容器(存放迭代找出的所有子代的集合)
private List<Comment> tempReplys = new ArrayList<>();
//1. 拿到所有根節點As
@Override
public List<Comment> listCommentByBlogId(Long blogId) {
Sort sort = Sort.by("createTime");
//按創建時間,拿到頂級評論(ParentComment爲null的字段)
List<Comment> As = commentRepository.findByBlogIdAndParentCommentNull(blogId,sort);
return combineChildren(As);
}
// 3. 如果Bs.size > 0,遍歷Bs,拿到B,將B放入子節點容器中;通過B,拿到他的子節點Cs,
private void Dep(List<Comment> Bs){
if (Bs.size()>0){
for (Comment B : Bs){
tempReplys.add(B);
List<Comment> Cs = B.getReplyComments();
//4. 遞歸調用
Dep(Cs);
}
}
}
//2. 遍歷As-Copy,拿到A;通過A,拿到它的子節點Bs;
private List<Comment> combineChildren(List<Comment> AsCopy) {
//傳入的是頂級節點
for (Comment A : AsCopy) {
//通過A,拿到Bs
List<Comment> Bs = A.getReplyComments();
// 調用第3步方法
Dep(Bs);
//修改頂級節點的reply集合爲迭代處理後的集合
A.setReplyComments(tempReplys);
//清除臨時存放區
tempReplys = new ArrayList<>();
}
return AsCopy;
}
}
Dao
package com.ddw.blog.dao;
import com.ddw.blog.po.Comment;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CommentRepository extends JpaRepository<Comment,Long> {
//通過blogId找到comment,而且ParentComment爲Null的記錄,並按sort排序
List<Comment> findByBlogIdAndParentCommentNull(Long blogId, Sort sort);
}
注意
自定義data-commentnickname不能使用駝峯形式,因爲$(obj).data(’’)只能識別小寫
8.5 按分類/標籤展示
按分類展示
注意springboot的controller即便不同包,也不允許同名
控制器
package com.ddw.blog.controller;
import com.ddw.blog.po.Type;
import com.ddw.blog.service.BlogService;
import com.ddw.blog.service.TypeService;
import com.ddw.blog.vo.BlogQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@Controller
public class TypeShowController {
@Autowired
private TypeService typeService;
@Autowired
private BlogService blogService;
@GetMapping("/types/{id}")
public String types(@PageableDefault(size = 8, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
@PathVariable Long id, Model model) {
List<Type> types = typeService.listTypeTop(10000);
if (id == -1) {
//如果從首頁進來,則id=-1,默認展示type的第一個
id = types.get(0).getId();
}
//需求是通過id查詢blog的分頁,沒有單獨的方法,拿這個BlogQuery也可以
BlogQuery blogQuery = new BlogQuery();
blogQuery.setTypeId(id);
model.addAttribute("types", types);
model.addAttribute("page", blogService.listBlog(pageable, blogQuery));
model.addAttribute("activeTypeId", id);
return "types";
}
}
按標籤展示
控制器
@GetMapping("/tags/{id}")
public String tags(@PageableDefault(size = 8, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
@PathVariable Long id, Model model) {
List<Tag> tags = tagService.listTagTop(10000);
if (id == -1) {
id = tags.get(0).getId();
}
model.addAttribute("tags", tags);
model.addAttribute("page", blogService.listBlog(id,pageable));
model.addAttribute("activeTagId", id);
return "tags";
}
BlogServiceImpl
連接查詢分頁
@Override
public Page<Blog> listBlog(Long tagId, Pageable pageable) {
return BlogRepository.findAll(new Specification<Blog>() {
@Override
public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
//將<Blog>root 與 Blog.tags連接
Join join = root.join("tags");
//查詢root.id =tagId的部分
return cb.equal(join.get("id"),tagId);
}
},pageable);
}
8.6 博客歸檔
控制器
package com.ddw.blog.controller;
import com.ddw.blog.service.BlogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ArchiveShowController {
@Autowired
private BlogService blogService;
@GetMapping("/archives")
public String archives(Model model) {
model.addAttribute("archiveMap", blogService.archiveBlog());
model.addAttribute("blogCount", blogService.countBlog());
return "archives";
}
}
BlogServiceImpl
@Override
public Map<String, List<Blog>> archiveBlog() {
List<String> years = BlogRepository.findGroupYear();
Map<String, List<Blog>> map = new HashMap<>();
for (String year : years) {
map.put(year, BlogRepository.findByYear(year));
}
return map;
}
@Override
public Long countBlog() {
return BlogRepository.count();
}
Dao
//注意group by不能用別名
@Query("select function('date_format',b.updateTime,'%Y') as year from Blog b group by function('date_format',b.updateTime,'%Y') order by year desc ")
List<String> findGroupYear();
@Query("select b from Blog b where function('date_format',b.updateTime,'%Y') = ?1")
List<Blog> findByYear(String year);
8.7 關於我與功能完善
關於我
用一個靜態頁面即可
@Controller
public class AboutShowController {
@GetMapping("/about")
public String about() {
return "about";
}
}
功能完善
footer的最新文章列表
@GetMapping("/footer/newblog")
public String newblogs(Model model) {
model.addAttribute("newblogs", blogService.listRecommendBlogTop(3));
return "_fragments :: newblogList";
}
從配置文件中讀值渲染模板
messages.properties是全局配置(與en-zh互補)
html模板取值
9. 打包運行
更改相關配置(比如端口號)
運行mavne命令
從target中拿到jar包,放到服務器上試試(提前設置好數據庫)
完美運行。。。
就不演示了
10. 項目thymeleaf知識點
$取保存在model中的變量
#取配置文件中的值
錯誤信息在源代碼中展示,頁面不顯示
<div>
<div th:utext="'<!--'" th:remove="tag"></div>
<div th:utext="'Failed Request URL : ' + ${url}" th:remove="tag"> </div>
<div th:utext="'Exception message : ' + ${exception.message}" th:remove="tag"></div>
<ul th:remove="tag">
<li th:each="st : ${exception.stackTrace}" th:remove="tag"><span th:utext="${st}" th:remove="tag"></span></li>
</ul>
<div th:utext="'-->'" th:remove="tag"></div>
</div>
th:utext與th:text
-
th:text
- 1.可以對表達式或變量進行求值
- 2.用“+”符號可進行文本連接
- 3.當獲取後端傳來的參數時,若後端有標籤,則直接顯示html代碼(沒有解析功能)
-
th:utext
- 具有解析html字符串的功能
<head th:fragment="head(title)">
<title th:replace="${title}">title</title>
<link rel="stylesheet" href="../static/css/me.css" th:href="@{/css/me.css}">
</head>
<head th:fragment=“head(title)”>
聲明此處爲fragment對象,名字爲head,包含參數爲title
<title th:replace="${title}">title</title>
意思是將title標籤內的內容動態的更改爲傳參過來的值title
<head th:replace="_fragments :: head(~{::title})">
</head>
th:replace=“fragments文件名 :: 替換fragment對象名(~{::替換標籤名})”
head(~{::title}) —>將一個片段作爲參數傳入,然後作爲替換元素~{::title}表示片段的引用
<tbody>
<tr th:each="type,iterStat : ${page.content}">
<td th:text="${iterStat.count}">1</td>
<td th:text="${type.name}">xx</td>
<td>
<a href="#" th:href="@{/admin/types/{id}/input(id=${type.id})}" class="ui mini teal basic button">編輯</a>
<a href="#" th:href="@{/admin/types/{id}/delete(id=${type.id})}" class="ui mini red basic button">刪除</a>
</td>
</tr>
</tbody>
each=“type,iterStat:${page.content}”
意思式遍歷page.content放到type中,同時保存遍歷狀態iterStat
iterStat.count表示當前元素的序號
th:href 能夠動態替換地址,…{id}…(id=${type.id})表示將將後端傳過來的type.id放到id中
<div class="ui mini pagination menu" th:if="${page.totalPages}>1">
<a th:href="@{/admin/types(page=${page.number}-1)}" class=" item" th:unless="${page.first}">上一頁</a>
<a th:href="@{/admin/types(page=${page.number}+1)}" class=" item" th:unless="${page.last}">下一頁</a>
</div>
th:if 如果條件成立則當前標籤可見
th:unless 如果條件成立則當前標籤不可見
<form action="#" method="post" th:object="${type}" th:action="*{id}==null ? @{/admin/types} : @{/admin/types/{id}(id=*{id})} " class="ui form">
<input type="hidden" name="id" th:value="*{id}">
th:object 拿到後端傳遞的對象
*{id} 意思式 object.id
之所以放一個hidden input標籤,是爲了將當前id傳遞給控制器(也可以不用)
通過:如果id爲空,則選擇不同的提交路徑,實現代碼複用。
javascript中含有th代碼的時候,需要如下配置纔有效。
archiveMap是Map對象,在th模板中用item接收,則item包含key和value
11. 相關UI技術
selection
本項目通過selection給某些字段進行賦值
<div class="ui type selection dropdown">
<input type="hidden" name="typeId">
<i class="dropdown icon"></i>
<div class="default text">分類</div>
<div class="menu">
<div th:each="type : ${types}" class="item" data-value="" th:data-value="${type.id}" th:text="${type.name}">錯誤日誌</div>
<!--/*-->
<div class="item" data-value="2">開發者手冊</div>
<!--*/-->
</div>
</div>
此處會將data-value的值賦給input的value
如果這個input在form表單內,則提交表單後後臺能夠獲取到typeId。
或者通過ajax的形式獲取到該值進行請求
function loaddata() {
$("#table-container").load(/*[[@{/admin/blogs/search}]]*/"/admin/blogs/search",{
title : $("[name='title']").val(),
typeId : $("[name='typeId']").val(),
recommend : $("[name='recommend']").prop('checked'),
page : $("[name='page']").val()
});
}
下面這種方式是thymeleaf的註釋方式,這樣註釋之後模板引擎渲染後會刪除該行,如果打開原生頁面,則能看見
<!--/*-->
<div class="item" data-value="2">開發者手冊</div>
<!--*/-->
前端非空校驗
$('.form').form({
fields : {
title : {
identifier: 'title',
rules: [{
type : 'empty',
prompt: '請輸入博客標題'
}]
}
}
}