演示地址:个人博客
个人博客功能:
技术组合:
- 后端:Spring Boot + JPA + thymeleaf模板
- 数据库:MySQL
- 前端UI:Semantic UI框架
工具与环境:
- IDEA
- Maven 3
- JDK 8
- Axure RP 8
内容模块:
- 需求分析与功能规划
- 页面设计与开发
- 技术框架搭建
- 后端管理功能实现
- 前端管理功能实现
用户故事是敏捷框架中的一种开发方法。可以帮助开发者转换视角,以用户的角度更好的把握需求,从而实现具有商业价值的功能。
用户故事最好是用户团队编写
用户故事模板:
- As a (role of user), I want (some feature) so that (some business value).
- 作为一个(某个角色) 使用者,我可以做(某个功能) 事情,如此可以有(某个商业价值) 的好处
关键点:角色、功能、商业价值
举例:
- 作为一个招聘网站注册用户,我想查看最近3天发布的招聘信息,以便于了解最新的招聘信息。
- 作为公司,可以张贴新工作。
个人博客系统的用户故事:
角色:普通访客,管理员(我)
- 访客,可以分页查看所有的博客
- 访客,可以快速查看博客数最多的6个分类
- 访客,可以查看所有的分类
- 访客,可以查看某个分类下的博客列表
- 访客,可以快速查看标记博客最多的10个标签
- 访客,可以查看所有的标签
- 访客,可以查看某个标签下的博客列表
- 访客,可以根据年度时间线查看博客列表
- 访客,可以快速查看最新的推荐博客
- 访客,可以用关键字全局搜索博客
- 访客,可以查看单个博客内容
- 访客,可以对博客内容进行评论
- 访客,可以赞赏博客内容
- 访客,可以微信扫码阅读博客内容
- 访客,可以在首页扫描公众号二维码关注我
- 我,可以用户名和密码登录后台管理
- 我,可以管理博客
- 我,可以发布新博客
- 我,可以对博客进行分类
- 我,可以对博客打标签
- 我,可以修改博客
- 我,可以删除博客
- 我,可以根据标题,分类,标签查询博客
- 我,可以管理博客分类
- 我,可以新增一个分类
- 我,可以修改一个分类
- 我,可以删除一个分类
- 我,可以根据分类名称查询分类
- 我,可以管理标签
- 我,可以新增一个标签
- 我,可以修改一个标签
- 我,可以删除一个标签
- 我,可以根据名称查询标签
页面规划:
前端展示:首页、详情页、分类、标签、归档、关于我
后台管理:模板页
1、引入Spring Boot模块:
- web
- Thymeleaf
- JPA
- MySQL
- Aspects
- DevTools
2、application.yml配置
-
使用 thymeleaf 3
pom.xml:
<springboot-thymeleaf.version>3.0.2.RELEASE</springboot-thymeleaf.version>
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
application.xml:
spring:
thymeleaf:
mode: HTML
- 数据库连接配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
jpa:
hibernate:
ddl-auto: update
properties:
format_sql: true
show-sql: true
-
日志配置
application.yml:
logging:
level:
root: info
com.zfans: debug
file:
path: log/blog-dev.log
logback-spring.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>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时,按日期切分日志,切分后目录:
blog.2017-08-01.0 8MB
blog.2017-08-01.1 10MB
blog.2017-08-02.0 5MB
blog.2017-08-03.0 5MB
......
-->
- 生产环境与开发环境配置
- application-dev.yml
- application-pro.yml
1、定义错误页面
- 404
- 500
- error
2、全局处理异常
统一处理异常:
@ControllerAdvice
public class ControllerExceptionHandler {
LoggerFactory loggerFactory;
SubstituteLogger substLogger;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 异常处理
* 标识方法可以做异常处理
*/
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHander(HttpServletRequest request, Exception e) throws Exception {
logger.error("Request URL : {} , Exception : {}", request.getRequestURL(), e.getMessage());
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;
}
// private final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
//
// /**
// * 异常处理
// *
// * @param request
// * @param e
// * @return
// */
// @ExceptionHandler({Exception.class})
// public ModelAndView handleException(HttpServletRequest request, Exception e) throws Exception {
//
// logger.error("Request URL : {} , Exception : {}", request.getRequestURL(), e);
//
// if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
// throw e;
// }
// ModelAndView mav = new ModelAndView();
// mav.addObject("url", request.getRequestURL());
// mav.addObject("exception", e);
// mav.setViewName("error/error");
//
// return mav;
// }
}
错误页面异常信息显示处理:
<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>
3、资源找不到异常
@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);
}
}
1、记录日志内容
- 请求 url
- 访问者 ip
- 调用方法 classMethod
- 参数 args
- 返回内容
2、记录日志类:
@Aspect
@Component
public class LogAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* com.zfans.web.*.*(..))")
public void log() {
}
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
String ip = request.getRemoteAddr();
String classMethod = joinPoint.getSignature().getDeclaringTypeName() + '.' + joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
RequestLog requestLog = new RequestLog(url, ip, classMethod, args);
logger.info("Request : {}", requestLog);
}
@After("log()")
public void doAfter() {
// logger.info("---------doAfter----------");
}
@AfterReturning(returning = "result", pointcut = "log()")
public void doAfterReturn(Object result) {
logger.info("Result : {}", result);
}
private class RequestLog {
private final String url;
private final String ip;
private final String classMethod;
private final 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) +
'}';
}
}
}
1、静态页面导入project
2、thymeleaf布局
- 定义fragment
- 使用fragment布局
3、错误页面美化
4、设计与规范
实体类:
- 博客 Blog
- 博客分类 Type
- 博客标签 Tag
- 博客评论 Comment
- 用户 User
实体关系:
评论类自关联关系:
Blog类:
Type类:
Tag类:
Comment类:
User类:
Service/DAO层命名约定:
- 获取单个对象的方法用get做前缀。
- 获取多个对象的方法用list做前缀。
- 获取统计值的方法用count做前缀。
- 插入的方法用save(推荐)或insert做前缀。
- 删除的方法用remove(推荐)或delete做前缀。
- 修改的方法用update做前缀。
1、构建登录页面和后台管理首页
2、UserService和UserRepository
3、LoginController实现登录
4、MD5加密
5、登录拦截器
1、分类管理页面
2、分类列表分页
{
"content":[
{"id":123,"title":"blog122","content":"this is blog content"},
{"id":122,"title":"blog121","content":"this is blog content"},
{"id":121,"title":"blog120","content":"this is blog content"},
{"id":120,"title":"blog119","content":"this is blog content"},
{"id":119,"title":"blog118","content":"this is blog content"},
{"id":118,"title":"blog117","content":"this is blog content"},
{"id":117,"title":"blog116","content":"this is blog content"},
{"id":116,"title":"blog115","content":"this is blog content"},
{"id":115,"title":"blog114","content":"this is blog content"},
{"id":114,"title":"blog113","content":"this is blog content"},
{"id":113,"title":"blog112","content":"this is blog content"},
{"id":112,"title":"blog111","content":"this is blog content"},
{"id":111,"title":"blog110","content":"this is blog content"},
{"id":110,"title":"blog109","content":"this is blog content"},
{"id":109,"title":"blog108","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
}
3、分类新增、修改、删除
1、博客分页查询
2、博客新增
3、博客修改
4、博客删除
1、博客列表
2、top分类
3、top标签
4、最新博客推荐
5、博客详情
1、Markdown 转换 HTML
- commonmark-java https://github.com/atlassian/commonmark-java
- pom.xml引用commonmark和扩展插件
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.15.2</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>0.15.2</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-heading-anchor</artifactId>
<version>0.15.2</version>
</dependency>
2、评论功能
- 评论信息提交与回复功能
- 评论信息列表展示功能
- 管理员回复评论功能