01-Springboot博客項目

本系列是對李仁密老師的視頻的學習記錄
前端頁面設計與實現部分不會在此展示,直接上後端部分。
項目源碼和教程可以點擊上面鏈接進行學習

碼雲地址:項目源碼


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="'&lt;!--'" 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="'--&gt;'" 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相關知識

  1. @Entity 標註爲實體,實現對象到表的映射

  2. @Table(name = “t_blog”) 設置表名

  3. @Id
    @GeneratedValue 設置id,自增

  4. @Column(columnDefinition=“text”) 指定列屬性

  5. @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類型。
  6. mapperBy
    1>只有OneToOne,OneToMany,ManyToMany上纔有mappedBy屬性,ManyToOne不存在該屬性;
    2>mappedBy標籤一定是定義在被擁有方的,他指向擁有方;
    3>mappedBy的含義,應該理解爲,擁有方能夠自動維護跟被擁有方的關係
    4>mappedBy跟joinColumn/JoinTable總是處於互斥的一方,mappedBy這方定義JoinColumn/JoinTable總是失效的,不會建立對應的字段或者表。

  7. @ManyToMany(cascade = {CascadeType.PERSIST})
    private List<Tag> tags = new ArrayList<>();
    上述兩行在Blog中,設置級聯新增。當新增文章的同時新增了標籤,則該標籤也會被增加到標籤表中

  8. 級聯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

  1. 實體類中增加校驗註解(以name上面校驗爲例)
    @NotBlank(message = “不能爲空”)是後端數據校驗功能
    String name;

  2. A控制器中放入一個空的實體
    model.addAttribute(“type”,new Type());

  3. B控制器中進行校驗(這裏只保留了校驗錯誤發生時所需的代碼)

public String post(@Valid Type type,BindingResult result) {
//如果result中存在校驗錯誤,則返回到輸入頁面
	if (result.hasErrors()) {
	    return "admin/types-input";
	}
}

@Valid Type type表示對type進行校驗,校驗方式就是我們在該實體類中所標註的校驗註解
BindingResult result 接收校驗之後的結果

  1. 前端頁面顯示校驗結果(message)
    • 前端頁面的form標籤上聲明這個實體
      <form action="#" method="post"  th:object="${type}">
      
    • 在該form中準備輸入被校驗的值(name)上聲明校驗
      <input type="text" name="name" placeholder="分類名稱" th:value="*{name}" >
      
    • 顯示校驗結果
      <!--/*/
      <div th:if="${#fields.hasErrors('name')}"  >
      	<div class="header">驗證失敗</div>
      	<p th:errors="*{name}">提交信息不符合規則</p>
      </div>
      /*/-->
      
      fields.hasErrors(‘name’) 中的name是指被校驗註解標註的字段名
      *{name}中的星號可以理解爲object

此外,通過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";
    }
}


簡單分頁查詢

  1. Dao—提供繼承JpaRepository的接口
    public interface BlogRepository extends JpaRepository<Blog,Long> {
    }
    
  2. 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);
    }
    
  3. 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傳遞給控制器

  1. Dao—提供繼承JpaRepository和JpaSpecificationExecutor接口
    public interface BlogRepository extends JpaRepository<Blog,Long>, 	JpaSpecificationExecutor<Blog> {
    }
    
  2. Service—提供分頁查詢方法,加上覆雜分頁查詢vo,使用findAll(Specification,Pageable)方法
    Page<Blog> listBlog(Pageable pageable,BlogQuery blog){
    	blogRepository.findAll(pageable);
    };
    
  3. 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="'&lt;!--'" 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="'--&gt;'" 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: '請輸入博客標題'
          }]
        }
    }
}
發佈了78 篇原創文章 · 獲贊 15 · 訪問量 5229
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章