SpringBoot开发小而美的博客——学习笔记
学习链接: SpringBoot开发一个小而美的个人博客。
- 技术组合:
- 后端:SpringBoot + JPA + thymeleaf模板
- 数据库:MySQL
- 前端UI:Semantic UI框架
- 工作与环境:
- IDEA
- Maven3
- JDK8
- Axure RP 8
- 课程内容模块
- 需求分析和功能规划
- 页面设计与开发
- 技术框架搭建
- 后端管理功能实现
- 前端管理功能实现
- 你能学到什么
- 基于SpringBoot完整全栈式的开发套路
- Semantic UI框架的使用
- 一套博客系统的源代码与设计
1 需求与功能
1.1 用户故事
用户故事是敏捷开发的一种方法,从用户的角度描述需求,用户故事模板:
- 作为一个(某个角色)使用者,我可以做(某个功能)事情,如此可以有某个(商业价值)的好处。
- 角色、功能、商业价值
举例:
- 作为一个招聘网站注册用户,我想查看最近三天发布的招聘信息,以便于了解最新的招聘信息。
确定角色:
- 普通访客、管理员(我)
访客实现的功能:
管理员实现的功能:
1.2 功能规划
2 页面设计与开发
2.1 页面设计
使用Axure RP 8进行原型设计,软件下载安装可自行百度。根据上面的功能规划图可以总结出需要设计的页面。
- 前端展示页面:首页、详情页、分类、标签、归档、关于我
- 后端管理页面:模板页
2.2 页面开发
Semantic是快速构建前端页面的框架。我们使用WebStorm开发。把下面的css放在head标签中,第二行js放在body标签最底部。
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
在https://www.jsdelivr.com/里找到JQuery的CDN导入。
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2/dist/jquery.min.js"></script>
2.3 插件集成
背景图片资源
编辑器Markdown,
内容排版typo.css,
动画animate.css,
代码高亮prism或者官网,
滚动侦测waypoints,
平滑滚动jquery.scrollTo,
目录生成Tocbot,
二维码生成qrcode.js。
3 框架搭建
3.1 构建与配置
1、引入SpringBoot模块:
- web
- Thymeleaf
- IPA
- MySQL
- Aspects(在SpringBoot2.x自动导入了)
- Devtools
2、application-dev.yml配置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/bolg?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
logging:
level:
root: info
com.zkp: debug
配合logback.xml
<?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>3</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>
application/yml
spring:
profiles:
active: dev
3.2 异常处理
1、定义错误页面
- 404
- 500
- error
从后台获取信息在static下完成不了,因此在template下定义错误页面,新建error文件夹,下面创建404.html和500.html,如果出现异常,Spring约定会在error文件夹下使用对应的html页面,比如404对应404.html。
2、全局处理异常
新建包handler,创建类ControllerExceptionHandler,
@ControllerAdvice
public class ControllerExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e) throws Exception {
logger.error("Request URL: {}, Exception: {}", request.getRequestURL(), e.toString());
e.printStackTrace();
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.html中加上下列代码,作用是在源代码中打印错误信息,这样如果出错,在前端就能看到哪里出错,不用跑到服务器去看。
<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>
新建exception,创建NotFoundException
@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);
}
}
3.3 日志处理
采用SpringBoot的AOP进行日志处理,记录日志内容有:
- 请求url
- 访问者ip
- 调用方法classMethod
- 参数args
- 返回内容
编写BlogAspect如下:
@Aspect
@Component
public class LogAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* com.zkp.web.*.*(..))")
public void log(){
}
@Before("log()")
public void doBrfore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
String ip = request.getRemoteAddr();
String classMethod = joinPoint.getSignature() + " ";
Object[] args = joinPoint.getArgs();
RequestLog requestLog = new RequestLog(url, ip, classMethod, args);
//记录
logger.info("Request:" + requestLog);
}
@After("log()")
public void doAfter() {
// logger.info("------after------");
}
@AfterReturning(returning = "result",pointcut = "log()")
public void doAfterReturn(Object result) {
//记录
logger.info("-----Result------:" + result);
}
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 "{" +
"url='" + url + '\'' +
", ip='" + ip + '\'' +
", classMethod='" + classMethod + '\'' +
", args=" + Arrays.toString(args) +
'}';
}
}
}
效果如下:
3.4 页面处理
1、静态页面导入project
这里如果目录结构没有对上,需要移动html文件位置,记得勾选search for references,会自动修改引用位置。
导入之后再设置路径进行访问,结果报错500,解决方法是clean之后重新编译。F12控制台可以看到有些访问不到
需要使用th:href=“@{}”,注意这里默认到stati里面找。如果页面很多,每一个都需要这样处理比较麻烦,在thymeleaf中的fragment可以解决这个问题。
2、thymeleaf布局
- 定义fragment
新建_fregments.html,编写head标签
<head th:fragment="head(title)">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:replace="${title}">首页</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/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>
- 使用fragment布局
在其他html页面的head中这样修改
<head th:replace="_fragments::head(~{::title})">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<link rel="stylesheet" href="../static/css/me.css">
</head>
<!--/*/ <th:block th:replace="_fragments::script">/*/-->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<!--/*/ </th:block>/*/-->
这样两端都可以用
3、优化错误页面
加入head和footer,并加入错误信息提示:
<div class="m-container-small m-padded-tb-massive">
<div class="ui error message m-padded-tb-huge">
<div class="ui container">
<h2>404</h2>
<p>对不起,您访问的资源不存在</p>
</div>
</div>
</div>
4 设计与规范
4.1 实体设计
可以先建表,也可以面向对象的思维,先设计实体,再用Spring Boot的JPA设计数据库表结构。
实体类:
- 博客Blog
- 博客分类Type
- 博客标签Tag
- 博客评论Comment
- 用户User
关系:例如,类型和博客之间是一对多的关系。
评论和回复的关系:
Blog类:包含一组标签,一组评论,一个用户。
评论类:
用户类:
以创建Blog为例,需要加上这些注解:JPA使用@Entity表示有数据库生成的能力,@Table可以指定数据库表的名字,@Id是主键,@GeneratedValue默认生成策略。
@Entity
@Table(name = "t_blog")
public class Blog {
@Id
@GeneratedValue
private Long id;
}
如果是创建日期类型,需要加上注解:
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
创建完实体类后,处理实体类之间的关系,例如一个博客有多个标签。在Blog类中加入注解@ManyToOne,在Type中假如注解@OneToMany。多对多同理用@ManyToMany。关系被维护方指定mappedBy。
@ManyToOne
private Type type;
@OneToMany(mappedBy = "type")
private List<Blog> blogs = new ArrayList<>();
@ManyToMany
private List<Tag> tags = new ArrayList<>();
级联新增@ManyToMany(cascade = {CascadeType.PERSIST})
。
启动之后,可以看到自动生成数据库表:
4.2 应用分层
终端显示层即template。
4.3 命名约定
Serverce和Da层方法命名约定:
- 获取单个用get
- 获取多个用list
- 获取统计值用count
- 插入用insert
- 删除用delete
- 修改用update
5 后台管理
5.1 登录
登录需要的步骤如下:
1、构建登录页面和后台管理首页
登录后台:
2、UserService和UserRepository
继承JpaRepository,User表示操作的对象,Long是主键类型。只要方法名符合规范,就能直接封装好sql语句。
public interface UserRepository extends JpaRepository<User,Long> {
User findByUsernameAndPassword(String username, String password);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User checkUser(String username, String password) {
User user = userRepository.findByUsernameAndPassword(username,password);
return user;
}
}
3、LoginController实现登录
@Controller
@RequestMapping("/admin")
public class LoginController {
@Autowired
private UserService userService;
@GetMapping
public String loginPage(){
return "admin/login";
}
@PostMapping("/login")
public 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.setAttribute("user",user);
return "admin/index";
}
else{
attributes.addFlashAttribute("message","用户名或密码错误");
return "redirect:/admin";
}
}
@GetMapping("/logout")
public String logout(HttpSession session){
session.removeAttribute("user");
return "redirect:/admin";
}
}
4、MD5加密
创建util.MD5Utils类
public class MD5Utils {
public static String code(String str){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[] byteDigest = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < byteDigest.length; offset++){
i = byteDigest[offset];
if (i<0){
i+=256;
}
if (i<16){
buf.append("0");
}
buf.append(Integer.toHexString(i));
}
return buf.toString();
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
}
5、登录拦截器
创建interceptor.LoginInterceptor类
public class LoginInterceptor 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;
}
}
编写config配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin")
.excludePathPatterns("/admin/login");
}
}
5.2 分类管理
1、分类管理页面
2、分类列表页面
在Type实体类上通过注解方式进行校验,非空校验,业务上的校验
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@NotBlank(message = "分类名称不能为空")
private String name;
下面的注释,html静态页面不会显示,但是thymeleaf渲染时会显示。
<!--/*/
<div class="ui negative message" th:if="${#fields.hasErrors('name')}">
<i class="close icon"></i>
<div class="header">验证失败</div>
<p th:errors="*{name}">提交信息不符合规则</p>
</div>
/*/-->
3、分类新增、修改、删除
可以使用@Transactional进行事务管理
5.3 标签管理
这里和5.2类似。
5.4 博客管理
1、博客分页查询
需要动态sql,dao层接口继承JpaSpecificationExecutor<>。Service层编写如下:
@Override
public Page<Blog> listBlog(Pageable pageable, Blog blog) {
return blogRepository.findAll(new Specification<Blog>() {
@Override
public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>();
if(blog.getTitle()!=null&&blog.getTitle()!=""){
predicates.add(cb.like(root.<String>get("title"),"%" + blog.getTitle() + "%"));
}
if(blog.getType().getId()!=null){
predicates.add(cb.equal(root.<Type>get("type").get("id"),blog.getType().getId()));
}
if(blog.isRecommend()){
predicates.add(cb.equal(root.<Boolean>get("recommend"),blog.isRecommend()));
}
cq.where(predicates.toArray(new Predicate[predicates.size()]));
return null;
}
},pageable);
}
可以只刷新片段blogList,结合ajax和return "admin/blogs::blogList";
,
在html下会显示,但是thymeleaf渲染下不会显示。
<!--/*-->
<div class="item" data-value="2">开发者手册</div>
<!--/*-->
2、博客新增
修改之后出现错误,Data truncation: Data too long for column 'content' at row 1
,这是因为content用的String类型,映射到数据库中变成varchar(255),长度不够存储因此报错。
解决方法:
- 修改content的数据库中的类型为longtext
-
先清除表或者字段,再使用下面注解
@Basic(fetch = FetchType.LAZY)
@Lob
private String content;
3、博客修改
4、博客删除
5、搜索页面
6 前端展示
6.1 首页展示
1、博客列表
2、top分类
3、top标签
4、最新博客推荐
5、博客详情
Markdown转换HTML
处理中间的内容部分,应该放html内容。
Markdown转换HTML插件:
commonmark-java https://github.com/atlassian/commonmark-java
pom中引用commonmark插件。
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.10.0</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-heading-anchor</artifactId>
<version>0.10.0</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>0.10.0</version>
</dependency>
使用th:utext不转义。
li标签字体过小,可以调节typo.css。
在javacript中动态改变二维码的地址。
<script th:inline="javascript">
//域名
var serurl = "127.0.0.1:8080"
var url = /*[[@{/blog/{id}(id=${blog.id}})}]]*/"";
var qrcode = new QRCode("qrcode", {
text: serurl + url,
width: 110,
height: 110,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
</script>
评论功能:
1、评论信息提交与回复
2、评论信息列表展示功能
3、管理员回复评论
6.2 分类页
展示效果如下:
6.3 标签页
操作类似分类页。
6.4 归档页
所谓归档就是按照年份把发布的博客进行展示。
6.5 关于我
最后在resources下新建messages.properties文件。
可以全球化,新建两个文件messages_en_US.properties,和messages_zh_CN.properties。
如果放在i18n文件夹下,需要在yml文件中配置路径。
spring:
message:
basename: i18n/messages
部署到服务器
参考另外一篇博客.