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="'&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>

在这里插入图片描述
新建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

部署到服务器

参考另外一篇博客.

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值