SpringBoot开发的个人博客笔记 半个月(2020-11-05-2020-11-20)
一:需求分析
角色 商业 商业价值
1:角色
访客和管理员(我)
访客:具有查询.留言.赞赏的权限...
管理员:具有发布.编辑文章.分类.删除博客...的权限
二:页面的开发与设计
用纸和笔。‘
ps等软件设计页面
1.前端页面
首页:
详细页:
分类:
标签:
归档:
关于我:
2:后台管理
模板页面
三:构建框架的环境
1:在yml中配置连接所有的数据库和多种配置的环境开发和日常的日志
#开发环境
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/myblog
username: root
password: 123456
mybatis:
type-aliases-package: com.sx.entity
mapper-locations: classpath:mapper/*.xml
#开启驼峰命名法
configuration:
map-underscore-to-camel-case: true
# 日志的级别
logging:
level:
root: info
com.sx: debug
file: log/blog-dev.log
server:
port: 8080
#生产环境
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/myblog
username: root
password: 123456
mybatis:
type-aliases-package: com.sx.entity
mapper-locations: classpath:mapper/*.xml
#开启驼峰命名法
configuration:
map-underscore-to-camel-case: true
# 日志的级别
logging:
level:
root: warn
com.sx: info
file: log/blog-pro.log
server:
port: 8081
最大的权限application.yml
spring:
thymeleaf:
mode: HTML
#选择你想选择的开发环境
profiles:
active: por
配置日志的信息
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!--包含Spring boot对logback日志的默认配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<!--重写了Spring Boot框架 org/springframework/boot/logging/logback/file-appender.xml 配置-->
<appender name="TIME_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i</fileNamePattern>
<!--保留历史日志一个月的时间-->
<maxHistory>30</maxHistory>
<!--
Spring Boot默认情况下,日志文件10M时,会切分日志文件,这样设置日志文件会在100M时切分日志
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>
<!--日志输出到哪里呢-->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="TIME_FILE" />
</root>
</configuration>
<!--
1、继承Spring boot logback设置(可以在appliaction.yml或者application.properties设置logging.*属性)
2、重写了默认配置,设置日志文件大小在100MB时,按日期切分日志,切分后目录:
my.2017-08-01.0 80MB
my.2017-08-01.1 10MB
my.2017-08-02.0 56MB
my.2017-08-03.0 53MB
......
-->
2:配置全局的异常处理
1:配置拦截异常处理的类:
package com.sx.config.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
/**
* @author SX
* @version 1.0
* @date 2020/11/6 15:54
*/
@ControllerAdvice //拦截所有的Controller的注解
public class ControllerExceptionHandler {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler //标识这个方法是做异常处理的
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e) throws Exception {
//记录错误信息到日志中
logger.error("Request URL : {},Exception : {}", request.getRequestURL(), e);
//分析状态 如果不为空就交给springboot来处理
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}
ModelAndView mv = new ModelAndView();
mv.addObject("url", request.getRequestURL());
mv.addObject("exception", e);
mv.setViewName("error/error");
return mv;
}
}
配置error的页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>错误</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>
3设置 自定义的异常类
package com.sx.config.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* @author SX
* @version 1.0
* @date 2020/11/6 16:21
* 自定义异常
*/
//返回的状态
@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);
}
}
package com.sx.controller;
import com.sx.config.exception.NotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author SX
* @version 1.0
* @date 2020/11/6 15:48
*/
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
String blog = null;
if (blog == null) {
throw new NotFoundException("博客不存在");
}
return "index";
}
}
3:日志的处理
3.1 写一个日志的类用aop记录(ip,url,method,参数)
package com.sx.config.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
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 org.springframework.web.servlet.mvc.condition.RequestConditionHolder;
import sun.misc.Request;
import javax.servlet.ServletRegistration;
import javax.servlet.ServletRequestAttributeEvent;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* @author SX
* @version 1.0
* @date 2020/11/6 16:43
* 配置日志切面
*/
@Aspect
@Component
public class LogAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* com.sx.controller.*.*(..))")
public void log() {
}
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
String ip = request.getRemoteAddr();
String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
Object[] arges = joinPoint.getArgs();
RequestLog requestLog = new RequestLog(url, ip, classMethod, arges);
logger.info("Request : {}", requestLog);
}
@After("log()")
public void doAfter() {
logger.info("-------doBefore----------");
}
/**
* 返回的内容
*
* @param result
*/
@AfterReturning(returning = "result", pointcut = "log()")
public void doAfterReturn(Object result) {
logger.info("Result : {}", result);
}
/**
* 获取到指定的信息用aop
* 记录url、ip、classMethod、args
*/
private class RequestLog {
private String url;
private String ip;
private String classMethod;
private Object[] args;
public RequestLog(String url, String ip, String classMethod, Object[] args) {
this.url = url;
this.ip = ip;
this.classMethod = classMethod;
this.args = args;
}
@Override
public String toString() {
return "RequestLog{" +
"url='" + url + '\'' +
", ip='" + ip + '\'' +
", classMethod='" + classMethod + '\'' +
", args=" + Arrays.toString(args) +
'}';
}
}
}
4 thymeleaf对公共部分进行处理
4.1关建语法:
新建common.html 这个页面存放公共的资源 关键词 fragment
th:fragment="footer"
在需要用的公共资源的时候用 关键词 replace
th:replace="common :: footer"
5:设计与规范
实体类
- 博客 Blog
- 博客分类 Type
- 博客标签 Tag
- 博客评论 Comment
一个对象有多个回复,将具有被回复性质的作为parentComment
//一对多
List<Comment> replyComments=new ArrayList<>();
//多对一
private Comment parentComment;
- 用户 User
5.1 命名约定
Service/DAO层命名约定:
- 获取单个对象的方法用get做前缀。
- 获取多个对象的方法用list做前缀。
- 获取统计值的方法用count做前缀。
- 插入的方法用save(推荐)或insert做前缀。
- 删除的方法用remove(推荐)或delete做前缀。
- 修改的方法用update做前缀。
四、后台管理功能实现
写一个配置类
package com.sx.config.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author SX
* @version 1.0
* @date 2020/11/7 15:55
* 过滤请求
* 配置类
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/admin/**")
.excludePathPatterns("/admin/login").excludePathPatterns("/admin");
}
}
期间遇见的问题:
1:WebMvcConfigurerAdapter过时的替换方法
Spring 5.0后,WebMvcConfigurerAdapter被废弃,取代的方法有两种:
①implements WebMvcConfigurer(官方推荐)
②extends WebMvcConfigurationSupport
使用第一种方法是实现了一个接口,可以任意实现里面的方法,不会影响到Spring Boot自身的@EnableAutoConfiguration,而使用第二种方法相当于覆盖了@EnableAutoConfiguration里的所有方法,每个方法都需要重写,比如,若不实现方法addResourceHandlers(),则会导致静态资源无法访问,实现的方法如下:
package com.sx.config.interceptor;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author SX
* @version 1.0
* @date 2020/11/7 15:55
*/
@Configurable
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/admin/**")
.excludePathPatterns("/admin/login").excludePathPatterns("/admin");
}
}
1:博客的业务
crud + 模糊查询
创建一个接受模糊查询dto
package com.sx.queryvo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author SX
* @version 1.0
* @date 2020/11/10 0:56
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SearchBlog {
private String title;
private Long typeId;
}
创建一个博客的信息的dto
package com.sx.queryvo;
import com.sx.entity.Type;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author SX
* @version 1.0
* @date 2020/11/8 10:29
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BlogQuery {
private Long id;
private String title;
private Date updateTime;
private Boolean recommend;
private Boolean published;
private Long typeId;
private Type type;
}
删除文章的分类
/**
* 删除 分类 逻辑判断是否该id
* 分类是否有文章有的话 就不让他删除
* @param id
* @param attributes
* 重定向 携带数据
* @return
*/
@GetMapping("/types/{id}/delete")
public String delete(@PathVariable Long id,RedirectAttributes attributes) {
typeService.deleteType(id);
attributes.addFlashAttribute("message", "删除成功");
return "redirect:/admin/types";
}
-
RedirectAttributes 重定向 携带数据 RedirectAttributes是SpringMVC3.1版本之后出来的一个新功能,专门用于重定向之后还能带参数跳转的的工具类。
2: 模糊查询实现 局部刷新(thymeleaf)
前端ajax
function loaddata() {
$("#table-container").load(/*[[@{/admin/blogs/search}]]*/"/admin/blogs/search",{
title : $("[name='title']").val(),
typeId : $("[name='typeId']").val(),
page : $("[name='page']").val()
});
}
后端返回
admin/blogs :: blogList
文章的模糊查询动态sql:
<select id="listSearchBlog" resultMap="IndexBlogMap" parameterType="com.sx.queryvo.SearchBlog">
select b.id, b.title, b.recommend, b.published, b.update_time, t.id, t.name
from myblog.t_blog b,
myblog.t_type t
<where>
<if test="1==1">
b.type_id=t.id
</if>
<if test="typeId != null">
and type_id=#{typeId,jdbcType=BIGINT}
</if>
<if test="title != null">
and title like concat('%',#{title,jdbcType=VARCHAR},'%')
</if>
</where>
</select>
五:博客前端页面的显示:
1.首页的业务分析:
- 最新文章:
创建一个dto用来接受最新文章的数据
package com.sx.queryvo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author SX
* @version 1.0
* @date 2020/11/10 17:56
* 最新博客信息返回的pojo
* 显示博客信息外,还需要显示分类、作者等信息,所以还需要定义分类名称和用户名、用户头像属性
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LatestBlog {
//博客信息
private Long id;
private String title;
private String firstPicture;
private Integer views;
private Integer commentCount;
private Date updateTime;
private String description;
//分类名称
private String typeName;
//用户名
private String nickname;
//用户头像
private String avatar;
}
<select id="listLatestBlog" resultType="com.sx.queryvo.LatestBlog">
select b.id,
b.title,
b.first_picture,
b.views,
b.comment_count,
b.update_time,
b.description,
t.`name` as typeName,
u.nickname,
u.avatar
from myblog.t_blog b,
myblog.t_user u,
myblog.t_type t
where b.type_id = t.id
and b.user_id = u.id
</select>
- 推荐文章:
创建一个dto用来接受推荐文章的数据
package com.sx.queryvo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author SX
* @version 1.0
* @date 2020/11/10 17:59
* 接受推荐博客的pojo
* 推荐只要显示博客标题、首图信息
* boolean类型的变量recommend 是否被推荐
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RecommendedBlog {
private Long id;
private String title;
private String firstPicture;
private boolean recommend;
}
<select id="listRecommendedBlog" resultType="com.sx.queryvo.RecommendedBlog">
select b.id, b.title, b.first_picture, b.recommend
from myblog.t_blog b
where b.recommend = true
</select>
-
博客的信息的统计
文章总数
访问总数
评论总数
留言总数
-
模糊查询 查询 文章
2:博客详情页显示
业务分析
当点进去一篇博客的时候
访问量+1
博客信息数据更新
3:评论
- 创建entity
package com.sx.entity;
import com.sx.queryvo.DetailedBlog;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author SX
* @version 1.0
* @date 2020/11/7 14:21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
private Long id;
private String nickname;
private String email;
private String content;
//头像
private String avatar;
private Date createTime;
private Long blogId;
//父节点的id
private Long parentCommentId;
//父节点的名字
private String parentNickname;
//回复评论
private List<Comment> replyComments = new ArrayList<>();
private Comment parentComment;
private boolean adminComment;
private DetailedBlog blog;
}
注意:
查询 所有评论的业务 逻辑
parentCommentId 当为-1时是父节点
然后根据id 循环 查询 出子节点
放到 replyComments集合中
@Override
public List<Comment> listCommentByBlogId(Long BlogId) {
List<Comment> comments = iCommentMapper.findByBlogIdParentIdNull(BlogId, Long.parseLong("-1"));
if (!comments.isEmpty()) {
for (int i = 0; i < comments.size(); i++) {
Long id = comments.get(i).getId();
String Parentnickname1 = comments.get(i).getNickname();
List<Comment> childComments = iCommentMapper.findByBlogIdParentIdNotNull(BlogId, id);
//查询出子评论
findTwoComment(BlogId, childComments, Parentnickname1);
comments.get(i).setReplyComments(tempReplys);
tempReplys = new ArrayList<>();
}
}
return comments;
}
public void findTwoComment(Long BlogId, List<Comment> comments, String parentnickname1) {
if (!comments.isEmpty()) {
for (int i = 0; i < comments.size(); i++) {
String parentnickname = comments.get(i).getNickname();
comments.get(i).setParentNickname(parentnickname1);
tempReplys.add(comments.get(i));
Long childId = comments.get(i).getId();
//查询二级评论
ReplyComment(BlogId, childId, parentnickname);
}
}
}
//查看回复
public void ReplyComment(Long blogId, Long childId, String parentnickname1) {
List<Comment> replyComment = iCommentMapper.findByBlogIdChildIdNotNull(blogId, childId);
if (!replyComment.isEmpty()) {
for (int i = 0; i < replyComment.size(); i++) {
Long replyId = replyComment.get(i).getId();
String parentnickname = replyComment.get(i).getNickname();
replyComment.get(i).setParentNickname(parentnickname1);
tempReplys.add(replyComment.get(i));
ReplyComment(blogId, replyId, parentnickname);
}
}
}
4:前端分类显示
/**
* 前端分类的显示
*
* @return
*/
@GetMapping("/types/{id}")
public String queryType(@RequestParam(defaultValue = "1", value = "pageNum") Integer pageNum, @PathVariable Long id, Model model) {
List<Type> types = iTypeService.listAllType();
//id为-1表示从首页导航栏点击进入分类页面
if (id == -1) {
id = types.get(0).getId();
}
model.addAttribute("types", types);
List<LatestBlog> blogs = iBlogService.listGetBlogByTypeId(id);
PageHelper.startPage(pageNum, 10000);
PageInfo<LatestBlog> pageInfo = new PageInfo<>(blogs);
model.addAttribute("pageInfo", pageInfo);
model.addAttribute("activeTypeId", id);
return "types";
}
BUG
前端 页面 进行重构 大改造
级联删除 在 JPA中可以 级联删除
但是在 Mybatis中需要自己掌管业务逻辑
标签删除
博客删除
分类删除
业务是否都得放到service中呢?
mysql:的免安装问题
接受数据中文,插入到数据库就乱码
解决方法:
修改 安装文件中的 my.ini
[mysqld]
#安装目录
basedir=S:\environment\mysql-5.7.19-winx64
datadir=S:\environment\mysql-5.7.19-winx64\data\
port=3306
#skip-grant-tables
#basedir表示mysql安装路径
#datadir表示mysql数据文件存储路径
#port表示mysql端口
#skip-grant-tables表示忽略密码
character-set-server=utf8
# 设置mysql客户端默认字符
character-set-server=utf8
普通for循环
@Override
public List<Comment> listCommentByBlogId(Long BlogId) {
List<Comment> comments = iCommentMapper.findByBlogIdParentIdNull(BlogId, Long.parseLong("-1"));
if (!comments.isEmpty()) {
for (int i = 0; i < comments.size(); i++) {
Long id = comments.get(i).getId();
String Parentnickname1 = comments.get(i).getNickname();
List<Comment> childComments = iCommentMapper.findByBlogIdParentIdNotNull(BlogId, id);
//查询出子评论
findTwoComment(BlogId, childComments, Parentnickname1);
comments.get(i).setReplyComments(tempReplys);
tempReplys = new ArrayList<>();
}
}
return comments;
}
public void findTwoComment(Long BlogId, List<Comment> comments, String parentnickname1) {
if (!comments.isEmpty()) {
for (int i = 0; i < comments.size(); i++) {
String parentnickname = comments.get(i).getNickname();
comments.get(i).setParentNickname(parentnickname1);
tempReplys.add(comments.get(i));
Long childId = comments.get(i).getId();
//查询二级评论
ReplyComment(BlogId, childId, parentnickname);
}
}
}
//查看回复
public void ReplyComment(Long blogId, Long childId, String parentnickname1) {
List<Comment> replyComment = iCommentMapper.findByBlogIdChildIdNotNull(blogId, childId);
if (!replyComment.isEmpty()) {
for (int i = 0; i < replyComment.size(); i++) {
Long replyId = replyComment.get(i).getId();
String parentnickname = replyComment.get(i).getNickname();
replyComment.get(i).setParentNickname(parentnickname1);
tempReplys.add(replyComment.get(i));
ReplyComment(blogId, replyId, parentnickname);
}
}
}
增强for循环
@Override
public List<Comment> listCommentByBlogId(Long blogId) {
//查询出父节点
List<Comment> comments = commentDao.findByBlogIdParentIdNull(blogId, Long.parseLong("-1"));
for (Comment comment : comments) {
Long id = comment.getId();
String parentNickname1 = comment.getNickname();
List<Comment> childComments = commentDao.findByBlogIdParentIdNotNull(blogId, id);
// 查询出子评论
combineChildren(blogId, childComments, parentNickname1);
comment.setReplyComments(tempReplys);
tempReplys = new ArrayList<>();
}
return comments;
}
private void combineChildren(Long blogId, List<Comment> childComments, String parentNickname1) {
// 判断是否有一级子评论
if (childComments.size() > 0) {
// 循环找出子评论的id
for (Comment childComment : childComments) {
String parentNickname = childComment.getNickname();
childComment.setParentNickname(parentNickname1);
tempReplys.add(childComment);
Long childId = childComment.getId();
// 查询出子二级评论
recursively(blogId, childId, parentNickname);
}
}
}
private void recursively(Long blogId, Long childId, String parentNickname1) {
// 根据子一级评论的id找到子二级评论
List<Comment> replayComments = commentDao.findByBlogIdAndReplayId(blogId, childId);
if (replayComments.size() > 0) {
for (Comment replayComment : replayComments) {
String parentNickname = replayComment.getNickname();
replayComment.setParentNickname(parentNickname1);
Long replayId = replayComment.getId();
tempReplys.add(replayComment);
recursively(blogId, replayId, parentNickname);
}
}
}
测试粗线
评论