使用SpringBoot 框架搭建个人博客代码附在文末

1、下载环境

  首先在官网 https://start.spring.io/ 下载一个模板到本地,使用Spring Boot 2.4.8版, java 8,下载jar包,同时导入Spring Boot DevTools(热部署)、Lombok(快捷注释)、 Spring Web(网络项目需要用) 三个依赖;下次使用时,解压然后改文件名就可以了

2、安装依赖

  我们创建的时候没有安装mysql 和 mybatis 依赖,我们要安装一个新的插件,来方便添加新的依赖,
首先进入 Settings 然后点击 plugins,在这里市场里(Marketplace) 中 搜索到 EditStarters 然后安 装,安装好后重启IDEA; 如果 Marketplace 刷新不出来,重启一下IDEA就好;此时我们进入pom.xml 中,在这个文件中右键,点击Genrate --> Edit Starters ;如果没有报错,就点击OK,如果出现报 错,说找不到Spring Boot,就重新在右上角的Maven中重新引入一下加载(build)一下项目环境;然后 搜索 mysql 和 mybatis 两个依赖,然后重新加载maven

3、配置aplication.properties文件

debug = false
## 日志保存路径
#logging.file.path = logs/
#logging.logback.rollingpolicy.max-file-size=10MB

# 设置日志等级
logging.level.root = info

# 设置端口号
server.port=8080

# 数据库连接地址
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&useSSL=true
spring.datasource.name=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# mybatis mapper 路径
mybatis.mapper-locations=classpath:mapper/**Mapper.xml

# 开启mybatis的sql执行日志
logging.level.com.example.demo.mapper=debug

  其中 mybatis mapper 路径 的作用是:配置 **Mapper.xml 文件,该配置文件应该放在 resources 下新建一个mapper文件,这个文件的作用是 拿到对数据库操作的具体方法的映射,这样我们在java文件夹下写具体操作方法时,只需要写一个 该类方法的 接口, 这个接口的具体实现部分则是放在对应类的Mapper.xml文件下,这部分后面举例时详细说。

  • 开启mybatis的sql执行日志 的作用是:令 com.example.demo.mapper 文件夹下的程序与形式以debug的等级去打印日志

4、将所有网页的静态页面准备充分,稍后将根据以下顺序实现功能

  • (0)index.html:导航页面
  • (1)login.html:登陆页面;
  • (2)regin.html:注册页面;reg_err.html:注册失败页面;reg_succerss.html:注册成功页面
  • (3)myblog.list.html:我的文章页面
  • (4)blog_content.html:文章内容页面*
  • (5)blog_edit:添加文章页面

5、一些要用到的工具类

(1)模板类与数据库
  根据在使用servlet做项目的经验,可以知道,登录就需要用户的信息,所以在我们需要一个model模板类,在这个类下存放用户的所有信息(User.java)或是存放文章的所有信息(ArticleInfo.java);其次对应的,如何映射到前端的url,并在后端操作,这是controller类需要解决的问题,针对用户相关的操作就写在controller类下的·在UserController.java类中列举各种具体的操作方法,同样对于文章的具体操作方法就写在controller类下的ArticleInfoController.java下。
  当前我们要实现的是用户的登录功能,首先构建数据库,数据库的模板如下,将下述sql代码导入mysql中执行到数据库中:

-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8;

-- 使用数据数据
use mycnblog;

-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
);

-- 创建文章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
);

-- 文章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正文',1);

  有了我们本地有了这个数据库后,接下来要在model模板类根据数据库中的userinfo表和articlinfo表创建User.java和ArticleInfo.java两个模板类,这两个模板类中的成员变量名必须和数据库中的属性名相同,方便后续进行沟通
  User用户信息模板类

package com.example.demo.model;
import lombok.Data;
import java.util.List;
@Data // 加上这个注释,我们就不用写 private 的 get 和 set 方法了
public class User {
    private int id;
    private String username;
    private String password;
    private String photo;
    private List<ArticleInfo> alist; // 要知道当前用户有多少文章,则需要这么定义文章表的内容在这里
}

  ArticleInfo文章内容模板类

package com.example.demo.model;
import lombok.Data;
import java.util.Date;
@Data
public class ArticleInfo {
    private int id;
    private String title;
    private String content;
    private Date createtime;
    private Date updatetime;
    private int uid;
    private int rcount;
    private String state;
    private User user;
}

(2)一些相关的配置文件(config.java 类)
①:AppFinal.java —— 这里写项目中不会变更的常量 即 final修饰的变量

package com.example.demo.config;
// 保存全局常量
public class AppFinal {
// 用户登录后 session 的key名称
public static final String USERINFO_SESSIONKEY = "userinfo";
// 图片存放的路径
public static final String IMAGE_PATH = "/upload/";
}

②:AppConfig.java —— 在这个类中,我们配置url的使用规则与拦截规则(拦截器具体则需要在config类下另外定义)

package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 在这个类中,我们配置url的使用规范,并且可以通过配置拦截器索要拦截的地址
* */
@Configuration  // 代表配置文件的注释
public class AppConfig implements WebMvcConfigurer {// 要实现自定义url和连接规则,继承 WebMvcConfigurer 也是不能忘的
    // 自定义url前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api" , c -> true);
        // 这里使用了λ表达式的方式,这里的意思是,之后我们在网页输入所有的页面的url时都要加上api
        // c -> true 的意思就是 configurer 对象为true,当为false就表示不添加
    }
    
    // 自定义拦截器规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截所有的接口
                .excludePathPatterns("/api/user/login") // 不拦截登录接口
                .excludePathPatterns("/api/user/reg") // 不拦截注册接口
                .excludePathPatterns("/login.html") // 不拦截登录页面
                .excludePathPatterns("/regin.html") // 不拦截注册页面
                .excludePathPatterns("/**/**.html") // 不拦截注册页面
                .excludePathPatterns("/**/*.css")
                .excludePathPatterns("/**/*.js")
                .excludePathPatterns("/**/*.jpg")
                .excludePathPatterns("/reg_success.html")
                .excludePathPatterns("/reg_err.html")
                .excludePathPatterns("/**/*.png");
    }
}

③:LoginInterceptor.java: —— 登录拦截器,当用户没有登陆的情况下(网页端没有有效Session信息时),是不能访问到其他任何页面的

package com.example.demo.config;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor { // HandlerInterceptor 经常用来实现拦截功能,所以我们要继承它
    // 自定义拦截方法,返回结果为 boolean值
    // 为 true表示可以访问后端接口,返回 false 表示无权访问后端接口
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断Session 是否有值
        HttpSession session = request.getSession(false); // false 表示不创建新的session
        if(session != null &&
            session.getAttribute(AppFinal.USERINFO_SESSIONKEY) != null){
            // 说明用户已登陆了
            return true;
        }
        return false;
    }
}

④:ErrorAdvice.java —— 自定义异常,将输出异常打包成想要的格式输出(这里打包成json字符串的格式,就可以将错误返回给前端)

package com.example.demo.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@ControllerAdvice // 规范出现异常时的返回数据格式
public class ErrorAdvice {
    @ExceptionHandler(RuntimeException.class) // 自定义运行时异常的格式
    @ResponseBody
    public Object err2(Exception e){
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", "-2");
        map.put("data", "");
        map.put("msg", e.getMessage());
        return map;
    }
    @ExceptionHandler(NullPointerException.class) // 自定义空指针异常的输出格式
    @ResponseBody
    public Object err3(Exception e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", "-3");
        map.put("data", "");
        map.put("msg", "空指针异常");
        return map;
    }
    @ExceptionHandler
    @ResponseBody
    public Object err(Exception e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", "-1");
        map.put("data", "");
        map.put("msg", e.getMessage());
        return map;
    }
}

⑤:MyResponseBodyAdvice —— 打包返回给前端的数据(这个在项目使用中近乎是模板一样的)

package com.example.demo.config;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice // 增强返回格式的的类, 要继承 ResponseBodyAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
    // 必须都要重写 ResponseBodyAdvice 的两个方法
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object o,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class aClass,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 0);  // 返回给前端的标识
        map.put("data", o);     // o 对象就是 数据对象部分
        map.put("msg","");
        return map;
    }
}

(3)在Spring中,要通过通过连接数据库对数据库进行增删查改是不用像servlet项目那样使用JDBC,这里使用的是MyBatis,在一开始的aplication.properties文件中,我们已经定义了本机数据库的相关信息,在那里面有一个 mybatis mapper 路径 的属性,这里就需要使用Spring中的Mapper映射的方法去修改数据库。
  具体逻辑为: 以注册为例,UserController 中的 regin 方法的路由地址为"/reg",则在与controller同级的文件夹下需要有一个新的mapper类,在mapper类下有各个具体操作的子类接口,这里的话就是UserMapper接口,在UserMapper下只写方法但不写具体实现,通过在resources文件夹下创建一个新的文件夹UserMapper.xml,这个文件就是用来配置具体的数据库操作逻辑,它是和UserMapper.java这个接口互通的,在UserMapper.java定义接口方法,到Mapper.xml文件中去具体执行。
  在UserMapper.java接口类中写映射的接口

package com.example.demo.mapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper // 用来映射 mapper的配置文件
public interface UserMapper {
    // 添加用户(注册功能)
    public int addUser(User user);
}

  去UserMapper.xml中写具体修改数据库的SQL语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">

    <insert id="addUser" parameterType="com.example.demo.model.User"
            useGeneratedKeys="true" keyProperty="id" keyColumn="id"><!-->parameterType 时定义返回的参数类型,这里返回的就是用户类的样子<!-->
            <!-->useGeneratedKeys 为true标识开启自动生成主键 后面两个参数指的是 参数名称 和 数据表中的属性名称<!-->
            insert into userinfo(username, password,photo)
            values (
                #{username},#{password},#{photo}
        )
    </insert>
</mapper>

5、实现后端的注册功能

  (1)先搞定后端部分
   接下来我们注册用户需要先创建User.java,这里并没有用到所有的数据库中userinfo的属性,我们注册用户只用到这些即可
  紧接着需要controller 类 作为中间商 来传递 这个url地址,创建UserController.java

package com.example.demo.controller;
import com.example.demo.config.AppFinal;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@Controller // 作为Spring项目,需要将当前类使用Controller注释将其托管给Sprting,若不是使用此注释,当Sping运行起来时,就不会运行该类
@RequestMapping("/user") // 作用是用来映射url的地址
@Slf4j  // lombok中用来打印日志 的注释
public class UserController {
    // 从application.properties 配置文件中拿到 myimgpath 参数的值
    @Value("${myimgpath}")
    private String imgpath;
    @Resource // 拿到 usermapper 的映射对象,这样才能去根据mapper修改数据库
    private UserMapper userMapper;
    // 实现注册功能
    @RequestMapping("/reg") // 第一步:用来建立连接的 url 绝对不能忘
    public String regin(String username, String password,
                        @RequestParam MultipartFile file) throws IOException { // 这一行的参数是用来读取用户上传上来的头像图片的
        // 第二步:todo:非空效验
        // 第三步:动态获取当前项目的路径
        //      这是因为当项目布置到服务器上是,路径肯定和本机的路径是不一样的,我们需要将图片保存在当前项目规定的头像图片路径下
        String path = imgpath;
        path += AppFinal.IMAGE_PATH;
        log.info("path:" + path);  // 通过日志输出图片文件存储路径
        // 第四步:拿到我们传过来图片的文件类型名(文件后缀) 并生成 文件名
            // 取文件类型
        String fileType = file.getOriginalFilename(); // 这步得到了完整的文件名
        fileType = fileType.substring(fileType.lastIndexOf("."));// 取.后面的,比如img.jpg 这里取到的就是.jpg
            // 取生成全局唯一的文件名
        String fileName = UUID.randomUUID().toString() + fileType;
            // 将文件名拼接在一起,保存在服务器
        file.transferTo(new File(path + fileName));
        // 第五步:将用户信息存储到服务器的数据库中
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        user.setPhoto(AppFinal.IMAGE_PATH + fileName); // 设置头像的地址
        int result = userMapper.addUser(user);
        if(result > 0){
            // 操作成功
            return "redirect:/reg_success.html"; // 重定向到注册成功页面
        }else {
            return "redirect:/reg_err.html";
        }
    }
}

(2)解决前端部分 —— 注册页面前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <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="css/conmmon.css">
    <link rel="stylesheet" href="css/login.css">
</head>
<body>
<form id="form1" action="/api/user/reg" method="post" enctype="multipart/form-data">
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="blog_list.html">主页</a>
        <a href="blog_edit.html">写博客</a>
        <a href="login.html">登陆</a>
        <!-- <a href="#">注销</a> -->
    </div>
    <!-- 版心 -->
    <div class="login-container">
        <!-- 中间的登陆框 -->
        <div class="login-dialog">
            <h3>登陆</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text" id="username" name="username">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" id="password" name="password">
            </div>
            <div class="row">
                <span>确认密码</span>
                <input type="password" id="password2" name="password2">
            </div>
            <div class="row">
                <span>头像</span>
                <input type="file" id="file" name="file">
            </div>
            <div class="row">
                <button id="submit" onclick="mysub()">提交</button>
            </div>
        </div>
    </div>
</form>
</body>
<script src="js/jquery.min.js"></script>
<script>
    function mysub() {
        // todo:非空效验
        // 效验确认密码和密码是否一致
        var password1 = jQuery("#password").val().trim();
        var password2 = jQuery("#password2").val().trim();
        if (password1 != "" && password2 != "" &&
            password1 != password2) {
            alert("两次输入的密码不一致请重新输入!");
            return false;
        }
        // 提交表单(jquery 内置方法)
        jQuery("#form1").submit();
    }
</script>
</html>

  注册成功提示页面

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>注册成功</h1>
<a href="login.html">点击添加到登录页面</a>
</body>
</html>

  注册失败提示页面

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>注册失败</h1>
<a href="regin.html">点击添加注册页面</a>
</body>
</html>

  注意:如果注册完成遇到报错,报错内容为数据库无法连接,原因可能是因为复制粘贴过去的代码格式有些问题,把用户名和密码这部分删掉照着手敲上去。

6、实现登录功能

  同样,我们现在已经有了用户的模板类,所以在UserController.java中写url的沟通前后端传递方法,然后在UserMapper.java中新建查询方法的映射接口,然后在UserMapper.xml中写查询数据库的方法即可
  (1)首先是要在UserController.java中定义url和查询数据库并将数据返回给前端验证的方法(这里只贴登录功能的部分)

@RequestMapping("/login")
@ResponseBody // 有了这个注释,返回给前端的就是JSON字符串
public Object login(User user, HttpServletRequest request){
   // 根据 前端传给的用户名和 密码 去查询
   User user2 = userMapper.getUserByNameAndPassword(user.getUsername(), user.getPassword());
   if(user2 == null){
       user2 = user;
   }else {
       // 登陆成功 添加session信息
       HttpSession session = request.getSession();
       // 存储session信息
       session.setAttribute(AppFinal.USERINFO_SESSIONKEY, user2);
   }
   return user2;
}

  (2)在userMapper.java添加映射接口 —— 登录功能(getUserByNameAndPassword)

package com.example.demo.mapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper // 用来映射 mapper的配置文件
public interface UserMapper {
    // 添加用户(注册功能)
    public int addUser(User user);
    // 根据 前端传来的 用户名和密码 去后端服务器查询用户信息验证登录(登录功能)
    public User getUserByNameAndPassword(@Param("name") String username, String password); // 这个Param("name") 是将username传递到后端时以name替代username
}

  (3)在UserMapper.xml写具体的查询数据库实现 —— 登录功能(id=“getUserByNameAndPassword”)
需要注意的是,在这里我们返回的类型不像注册那样直接返回的是一个User模板类的对象,而是通过将类的属性名和数据库的列明通过映射关系,返回为一个map类型的对象

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="com.example.demo.model.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="photo" property="photo"/>
    </resultMap>
    <insert id="addUser" parameterType="com.example.demo.model.User"
            useGeneratedKeys="true" keyProperty="id" keyColumn="id">
        insert into userinfo(username,password,photo)
        values(
            #{username},#{password},#{photo}
        )
    </insert>
    <select id="getUserByNameAndPassword" parameterType="java.lang.String"
            resultMap="BaseResultMap"><!--> 这里创建了一个map格式,用来映射类的属性名和数据库的列名,我们将它定义在xml文件的最开始 <!-->
        select * from userinfo where username=#{name} and password=#{password}
    </select>
</mapper>

  (4)当我们登陆成功欧会跳转到 mybolg_list.html 页面,这里可以随便先写一个静态页面代替,说明登陆成功即可

7、个人文章列表显示功能

  现在要拿到个人的文章信息了,所以即需要用户信息,也需要文章信息,所以这里会用到联表查询。此处呢,我们反过来,先把前端页面写好,根据前端页面去写后端操作
  (1)先将前端页面放在这里,然后根据前端页面中定义的url地址,去写Controller

<!DOCTYPE html>
<html lang="en">
<head>
    <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="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_list.css">
</head>
<body>
<!-- 导航栏 -->
<div class="nav">
    <img src="img/logo2.jpg" alt="">
    <span class="title">我的博客系统</span>
    <!-- 用来占据中间位置 -->
    <span class="spacer"></span>
    <a href="blog_list.html">主页</a>
    <a href="blog_edit.html">写博客</a>
    <!-- <a href="#">注销</a> -->
</div>
<!-- 版心 -->
<div class="container">
    <!-- 左侧个人信息 -->
    <div class="container-left">
        <div class="card">
            <img id="photoimg" src="" class="avtar" alt="">
            <h3 id="username"></h3>
            <a href="http:www.github.com">github 地址</a>
            <div class="counter">
                <span>文章</span>
            </div>
            <div class="counter">
                <span id="acount"></span>
            </div>
        </div>
    </div>
    <!-- 右侧内容详情 -->
    <div class="container-right" id="cdiv">
        <!-- 这里通过 id="cdiv" 来拿到后端传递给前端的内容 -->
    </div>
</div>
</body>
<script src="js/jquery.min.js"></script>
<script src="js/mytools.js"></script>  <!-- 这里将获取url参数放在了js代码中 -->
<script>
    // 得到当前用户id
    var uid = getParamValue("uid");
    if (uid != null) {
        jQuery.getJSON("/api/user/getalist", {"uid": uid}, function (data) {
            if (data != null && data.status == 0) {
                // 用户信息
                var userinfo = data.data;
                // 文章列表
                var alist = userinfo.alist;
                // 设置用户名
                jQuery("#username").html(userinfo.username);
                // 设置头像
                jQuery("#photoimg").attr("src", userinfo.photo);
                // 设置文章格式
                jQuery("#acount").text(alist.length);
                var contentHtml = "";
                // 设置文章列表
                for (var i = 0; i < alist.length; i++) {
                    contentHtml += "<div class=\"blog\">\n" +
                        "            <div class=\"title\">" + alist[i].title + "</div>\n" +
                        "            <div class=\"date\">" +
                        alist[i].createtime.substring(0, alist[i].createtime.indexOf("T"))
                        + "</div>\n" +
                        "            <div class=\"desc\">\n" +
                        alist[i].content +
                        "            </div>\n" +
                        "            <a href=\"blog_content.html?id=" + alist[i].id + "&acount=" +
                        alist.length
                        + "\" class=\"detail2\">查看全文 >></a>\n" +
                        "            <a href=\"javascript:mydel(" + alist[i].id + ")\" class=\"detail2\">删除</a>\n" +
                        "        </div>";
                }
                jQuery("#cdiv").html(contentHtml);
                // alert("用户:" + userinfo.username + " ,发布文章数:" + alist.length);
            }
        });
    }
    // 删除事件
    function mydel(id) {
        if (confirm("确认是否删除")) {
            // 访问后端接口进行文章删除
            jQuery.getJSON("/api/art/del", {"id": id}, function (data) {
                if (data != null && data.status == 0 &&
                    data.data == 1) {
                    // 删除成功
                    alert("恭喜:删除数据成功!");
                    // 刷新页面
                    location.href = location.href;
                } else {
                    alert("抱歉:操作失败请重试!");
                }
            });
        }
    }
</script>
</html>

  (2)先分析第一个jQuery —— jQuery.getJSON("/api/user/getalist"-----在个方法里我们因为要将前端显示的文章节截取显示,但是由于我们保存的文章是markdown格式的所以在前端会显示的并非纯文本,所以需要加一个 HtmlText 类来帮助我们完成删除markdown格式的符

package com.example.demo.tool;
import java.util.regex.Pattern;
public class HtmlText {
    public static String Html2Text(String inputString) {
        String htmlStr = inputString;
        String textStr = "";
        java.util.regex.Pattern p_script;
        java.util.regex.Matcher m_script;
        java.util.regex.Pattern p_style;
        java.util.regex.Matcher m_style;
        java.util.regex.Pattern p_html;
        java.util.regex.Matcher m_html;
        java.util.regex.Pattern p_html1;
        java.util.regex.Matcher m_html1;
        try {
            String regEx_script = "<[\\s]*?script[^>]*?>[\\s\\S]*?<[\\s]*?\\/[\\s]*?script[\\s]*?>"; //定义script的正则表达式{或<script[^>]*?>[\\s\\S]*?<\\/script> }
            String regEx_style = "<[\\s]*?style[^>]*?>[\\s\\S]*?<[\\s]*?\\/[\\s]*?style[\\s]*?>"; //定义style的正则表达式{或<style[^>]*?>[\\s\\S]*?<\\/style> }
            String regEx_html = "<[^>]+>"; //定义HTML标签的正则表达式
            String regEx_html1 = "<[^>]+";
            p_script = Pattern.compile(regEx_script, Pattern.CASE_INSENSITIVE);
            m_script = p_script.matcher(htmlStr);
            htmlStr = m_script.replaceAll(""); //过滤script标签

            p_style = Pattern.compile(regEx_style, Pattern.CASE_INSENSITIVE);
            m_style = p_style.matcher(htmlStr);
            htmlStr = m_style.replaceAll(""); //过滤style标签

            p_html = Pattern.compile(regEx_html, Pattern.CASE_INSENSITIVE);
            m_html = p_html.matcher(htmlStr);
            htmlStr = m_html.replaceAll(""); //过滤html标签

            p_html1 = Pattern.compile(regEx_html1, Pattern.CASE_INSENSITIVE);
            m_html1 = p_html1.matcher(htmlStr);

            htmlStr = m_html1.replaceAll(""); //过滤html标签
            textStr = htmlStr;
        } catch (Exception e) {
            System.err.println("Html2Text: " + e.getMessage());
        }
        return textStr;//返回文本字符串
    }
}

  这里传给后端的url地址为 /user/getalist,所以这里就要在UserController类下创建一个路由为("/getlist")

@RequestMapping("/getalist")
@ResponseBody
public Object getFullUser(int uid){
   User user = userMapper.getFullUser(uid);
   // 因为这里展示的是所有文章的大致信息,所以要对文章的正文进行一部分删减
   List<ArticleInfo> list = user.getAlist();
   for (ArticleInfo item : list){
       // 去除html 标签
       String desc = HtmlText.Html2Text(item.getContent());
       // 截取字符串
       if(desc.length() > 64){
           desc = desc.substring(0, 64) + "...";
       }
       item.setContent(desc);
   }
   user.setAlist(list);
   return user;
}

   写完之后运行发现返回为空值,仔细检查后发现原来是UserMapper.xml配置的问题,这里既然用到了联表查询,就要在中有如下添加,这里通过构建链表查询,可以看到collection 中的返回resultMap 位置映射的时ArticleInfoMapper中的BaseResultMap,所以需要在mapper的文件夹下在建一个xml配置文件,名字叫做ArticleInfoMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="com.example.demo.model.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="photo" property="photo"/>
        <collection property="alist" columnPrefix="a_"
                    resultMap="com.example.demo.mapper.ArticleInfoMapper.BaseResultMap">
        </collection>
    </resultMap>
。。。。。
。。。。。
</mapper>

   建立新的ArticleInfoMapper.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ArticleInfoMapper">
    <resultMap id="BaseResultMap" type="com.example.demo.model.ArticleInfo">
        <id property="id" column="id"></id>
        <result property="title" column="title"></result>
        <result property="content" column="content"></result>
        <result property="createtime" column="createtime"></result>
        <result property="updatetime" column="updatetime"></result>
        <result property="uid" column="uid"></result>
        <result property="rcount" column="rcount"></result>
        <result property="state" column="state"></result>
        <association property="user"
                     columnPrefix="u_"
                     resultMap="com.example.demo.mapper.UserMapper.BaseResultMap">
        </association>
    </resultMap>
</mapper>

  完成这两步代码的补充之后,联表查询才能起到作用,将信息返回给前端

8、个人文章删除功能

  在第7步的前端页面里我们看到了已经写好的删除文章功能,那里写的交互路由为jQuery.getJSON("/api/art/del",
  所以此时需要再建立一个Controller类,名为ArticleInfoController.java,通过在这个类中去写删除文章的方法

package com.example.demo.controller;
import com.example.demo.mapper.ArticleInfoMapper;
import com.example.demo.model.ArticleInfo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController // ResponseBody + Controller 的合体写法
@RequestMapping("/art")
public class ArticleInfoController {
    @Resource // 引入映射关系接口
    private ArticleInfoMapper articleInfoMapper;
    // 根据文章id删除文章
    @RequestMapping("/del")
    public int delById(@RequestParam int id){
        return articleInfoMapper.delById(id);
    }
}

  然后去mapper接口类下创建ArticleInfoController.java的接口类

package com.example.demo.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleInfoMapper {
    // 根据文章id删除文章
    public int delById(int id);
}

  最后去ArticleInfoController.xml配置文件下去写具体的删除sql语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ArticleInfoMapper">
    <resultMap id="BaseResultMap" type="com.example.demo.model.ArticleInfo">
        <id property="id" column="id"></id>
        <result property="title" column="title"></result>
        <result property="content" column="content"></result>
        <result property="createtime" column="createtime"></result>
        <result property="updatetime" column="updatetime"></result>
        <result property="uid" column="uid"></result>
        <result property="rcount" column="rcount"></result>
        <result property="state" column="state"></result>
        <association property="user"
                     columnPrefix="u_"
                     resultMap="com.example.demo.mapper.UserMapper.BaseResultMap">
        </association>
    </resultMap>

    <delete id="delById" parameterType="java.lang.Integer">
        delete from articleinfo where id=#{id}
    </delete>
</mapper>

9、查看文章详情

&  通过点击个人列表的查看全文可以跳转到文章的详情页面,从mybolg_list.html代码中有一句是跳转到文章详情页面 blog_content.html,如下

<!DOCTYPE html>
<html lang="en">
<head>
    <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="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_content.css">
</head>
<body>
<!-- 导航栏 -->
<div class="nav">
    <img src="img/logo2.jpg" alt="">
    <span class="title">我的博客系统</span>
    <!-- 用来占据中间位置 -->
    <span class="spacer"></span>
    <a href="blog_list.html">主页</a>
    <a href="blog_edit.html">写博客</a>
    <a href="login.html">登陆</a>
</div>
<!-- 版心 -->
<div class="container">
    <!-- 左侧个人信息 -->
    <div class="container-left">
        <div class="card">
            <img id="phtotimg" src="" class="avtar" alt="">
            <h3 id="name"></h3>
            <a href="http:www.github.com">github 地址</a>
            <div class="counter">
                <span>文章</span>
            </div>
            <div class="counter">
                <span id="acount">0</span>
            </div>
        </div>
    </div>
    <!-- 右侧内容详情 -->
    <div class="container-right">
        <div class="blog-content">
            <!-- 博客标题 -->
            <h3 id="title"></h3>
            <!-- 博客时间 -->
            <div id="createtime" class="date"></div>
            <!-- 博客正文 -->
            <div id="content">
            </div>
        </div>
    </div>
</div>
</body>
<script src="js/jquery.min.js"></script>
<script src="js/mytools.js"></script>
<script>
    // 1.得到当前页面的文章id
    var id = getParamValue("id");
    if (id != null && id > 0) {
        // 2.请求后端接口查询文章和用户信息
        jQuery.getJSON(
            "/api/art/detail",
            {"id": id},
            function (data) {
                if (data != null && data.status == 0 &&
                    data.data.id > 0) {
                    // 文章已经正常查询到了
                    // 文章和用户信息的动态赋值
                    jQuery("#phtotimg").attr("src", data.data.user.photo);
                    jQuery("#name").html(data.data.user.username);
                    // 从url中取出文章发布的数量
                    jQuery("#acount").text(getParamValue("acount"));
                    // 设置文章标题
                    jQuery("#title").html(data.data.title);
                    // 设置文章发布时间
                    var ctime = data.data.createtime; // 2021-07-13T..
                    ctime = ctime.substring(0, ctime.indexOf("T"));
                    jQuery("#createtime").html(ctime);
                    // 设置文章的正文信息
                    jQuery("#content").html(data.data.content);
                } else {
                    alert("抱歉操作失败,请重试!");
                }
            }
        );
    }
</script>
</html>

  其中jQuery.getJSON("/api/art/detail", 所以要在ArticleInfoController.java中添加路由

// 查询文章详情
@RequestMapping("/detail")
public ArticleInfo detail(@RequestParam int id) {
    return articleInfoMapper.detail(id);
}

  然后去配置ArticleInfoMapper.java接口和ArticleInfoMapper.xml具体sql语句

package com.example.demo.mapper;
import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleInfoMapper {
    // 根据文章id删除文章
    public int delById(int id);
    // 根据文章id查看文章详情
    public ArticleInfo detail(int id);
}
<select id="detail" resultType="com.example.demo.model.ArticleInfo">
    select a.*,u.username u_username,u.photo u_photo from articleinfo a
    left join userinfo u on a.uid=u.id where a.id=#{id}
</select>

10、添加文章

  同样先把前端页面放出来

<!DOCTYPE html>
<html lang="en">
<head>
    <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="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_edit.css">
    <!-- 引入 editor.md 的依赖 -->
    <link rel="stylesheet" href="editor.md/css/editormd.min.css"/>
    <script src="js/jquery.min.js"></script>
    <script src="editor.md/editormd.js"></script>
</head>
<body>
<!-- 导航栏 -->
<div class="nav">
    <img src="img/logo2.jpg" alt="">
    <span class="title">我的博客系统</span>
    <!-- 用来占据中间位置 -->
    <span class="spacer"></span>
    <a href="blog_list.html">主页</a>
    <a href="blog_edit.html">写博客</a>
    <a href="login.html">登陆</a>
    <!-- <a href="#">注销</a> -->
</div>
<!-- 编辑框容器 -->
<div class="blog-edit-container">
    <!-- 标题编辑区 -->
    <div class="title">
        <input id="title" name="title" type="text" placeholder="在这里写下文章标题">
        <button onclick="mysub()">发布文章</button>
    </div>
    <!-- 创建编辑器标签 -->
    <div id="editorDiv"></div>
</div>
<script>
    // 初始化编辑器
    var editor = editormd("editorDiv", {
        // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉.
        width: "100%",
        // 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
        height: "calc(100% - 50px)",
        // 编辑器中的初始内容
        markdown: "# 在这里写下一篇博客",
        // 指定 editor.md 依赖的插件路径
        path: "editor.md/lib/",
        saveHTMLToTextarea: true //
    });
    // 提交
    function mysub() {
        // 标题
        var title = jQuery("#title").val();
        // 正文
        var content = editor.getHTML();
        // todo:非空效验
        // 将当前页面的内容提交给后端
        jQuery.getJSON("/api/art/add",
            {
                "title": title,
                "content": content
            },
            function (data) {
                if (data != null && data.data != null &&
                    data.data.id >= 0) {
                    // 成功添加文章
                    alert("恭喜文章添加成功");
                    if (confirm("是否继续添加文章?")) {
                        // 需要刷新页面
                        location.href = location.href;
                    } else {
                        location.href = "myblog_list.html?uid=" + data.data.uid;
                    }
                } else {
                    alert("抱歉操作失败,请重试!");
                }
            });
    }
</script>
</body>
</html>

  同样,看他jQuery传的参数:jQuery.getJSON("/api/art/add",可知,要在ArticleInfoController.java下创建一个路由为("/add")的方法

// 添加文章
@RequestMapping("/add")
public ArticleInfo add(@RequestParam String title,
                       @RequestParam String content,
                       HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    // 添加文章前先验证登录的用户信息,登陆了才能添加
    Object u = null;
    if (session == null ||
            (u = session.getAttribute(AppFinal.USERINFO_SESSIONKEY)) == null) {
        return null;
    }
    User user = (User) u;
    ArticleInfo articleInfo = new ArticleInfo();
    articleInfo.setTitle(title);
    articleInfo.setContent(content);
    articleInfo.setUid(user.getId());
    int result = articleInfoMapper.add(articleInfo);
    return articleInfo;
}

  然后添加ArticleInfoMapper.java接口,再去ArticleInfoMapper.xml中写sql语句

package com.example.demo.mapper;
import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleInfoMapper {
    // 根据文章id删除文章
    public int delById(int id);
    // 根据文章id查看文章详情
    public ArticleInfo detail(int id);
    // 添加文章
    public int add(ArticleInfo articleInfo);
}
<insert id="add">
    insert into articleinfo(title,content,uid) values(
        #{title},#{content},#{uid}
    )
</insert>

  代码链接附在下面,配置好环境依赖后,代码就可以使用了,记得修改本地数据库的username、password和数据库的名称