本系列是对李仁密老师的视频的学习记录 前端页面设计与实现部分不会在此展示,直接上后端部分。 项目源码和教程可以点击上面链接进行学习
码云地址:项目源码
IDEA利用Spring初始化工具创建
依赖选择如下
更改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类型的错误对应跳转到相应页面
导入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);
}
}
直接将前端写好的页面按目录对应放入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:lirenmi@163.com</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/jquery@3.2/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/jquery.scrollto@2.1.2/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片段,实现美化。
关系抽象
实体类:
以Blog为纽带。
评论类的自关联关系: 一条(父)评论可以被人多次回复,一对多
属性设计
双环表明该属性为对象
JAP相关知识
代码如下(此处省略了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 = “分类名称不能为空”)是后端数据校验功能
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地址
}
}
登陆成功使用转发:转发能够保存一次会话的数据。 登录失败使用重定向:重定向会清空数据。
转发和重定向都是面向控制器路由的(即action路径),而非模板映射路径; 由于本节控制器中没有专用于登陆成功的控制器,因此此处没有使用转发,而是通过模板映射。
相关注意点
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可以) 查看原因
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"); //表单提交不能被拦截
}
}
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
public String post(@Valid Type type,BindingResult result) {
//如果result中存在校验错误,则返回到输入页面
if (result.hasErrors()) {
return "admin/types-input";
}
}
@Valid Type type表示对type进行校验,校验方式就是我们在该实体类中所标注的校验注解 BindingResult result 接收校验之后的结果
前端页面显示校验结果(message)
此外,通过BindingResult 还可以自定义错误校验,绕过注解校验 如:如果用户输入的名字重复了,可以通过result进行返回错误,显示方法跟上述第4步一致。
Type type = typeService.getTypeByName(type.getName());
if (type != null) {
result.rejectValue("name","nameError","不能添加重复的分类");
}
result.rejectValue(“校验字段名”,“自定义错误名”,“前端返回错误信息”);
作用机制流程 首先在实体类上标注校验 然后将用户输入的信息放入控制器准备的空实体 该实体会被传输到后台,后台进行校验,并返回校验结果
注意,@Valid 实体类和BindingResult必须挨着,不然无效
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的接口
Service—提供分页查询方法,使用findAll(Pageable)方法
Controller—接收Pageable对象,并利用Service中的分页查询方法查询page,保存到视图中
机制:
机制:
注意:分页结果是一个完整的po,分页查询条件是一个vo。因此前端进行翻页的时候,除了将page的页码信息(${page.number}+1)传递给控制器,还得将vo传递给控制器
Dao—提供继承JpaRepository和接口
Service—提供分页查询方法,加上复杂分页查询vo,使用findAll(Specification,Pageable)方法
Controller—接收Pageable对象,并利用Service中的分页查询方法查询page,保存到视图中
Predicate:动态查询条件的容器 Root:查询对象,可以从中获取到表的字段 CriteriaBuilder:设置条件表达式 CriteriaQuery:进行查询
控制器
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);
}
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方法
虽然提供了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";
}
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(’’)只能识别小写
注意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);
}
控制器
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);
关于我 用一个静态页面即可
@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模板取值
更改相关配置(比如端口号) 运行mavne命令
从target中拿到jar包,放到服务器上试试(提前设置好数据库)
完美运行。。。 就不演示了
$取保存在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
<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
本项目通过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: '请输入博客标题'
}]
}
}
}