使用SpringBoot 框架搭建个人博客代码附在文末
首先在官网 https://start.spring.io/ 下载一个模板到本地,使用Spring Boot 2.4.8版, java 8,下载jar包,同时导入Spring Boot DevTools(热部署)、Lombok(快捷注释)、 Spring Web(网络项目需要用) 三个依赖;下次使用时,解压然后改文件名就可以了
我们创建的时候没有安装mysql 和 mybatis 依赖,我们要安装一个新的插件,来方便添加新的依赖,
首先进入 Settings 然后点击 plugins,在这里市场里(Marketplace) 中 搜索到 EditStarters 然后安 装,安装好后重启IDEA; 如果 Marketplace 刷新不出来,重启一下IDEA就好;此时我们进入pom.xml 中,在这个文件中右键,点击Genrate
--> Edit Starters
;如果没有报错,就点击OK,如果出现报 错,说找不到Spring Boot,就重新在右上角的Maven中重新引入一下加载(build)一下项目环境;然后 搜索 mysql 和 mybatis 两个依赖,然后重新加载maven
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文件下,这部分后面举例时详细说。
(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语句
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>
(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>Documenttitle>
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>Documenttitle>
head>
<body>
<h1>注册失败h1>
<a href="regin.html">点击添加注册页面a>
body>
html>
注意:如果注册完成遇到报错,报错内容为数据库无法连接,原因可能是因为复制粘贴过去的代码格式有些问题,把用户名和密码这部分删掉照着手敲上去。
同样,我们现在已经有了用户的模板类,所以在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类型的对象
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 页面,这里可以随便先写一个静态页面代替,说明登陆成功即可
现在要拿到个人的文章信息了,所以即需要用户信息,也需要文章信息,所以这里会用到联表查询。此处呢,我们反过来,先把前端页面写好,根据前端页面去写后端操作
(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>
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">
div>
div>
body>
<script src="js/jquery.min.js">script>
<script src="js/mytools.js">script>
<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 += "\n" +
" " + alist[i].title + "\n" +
" " +
alist[i].createtime.substring(0, alist[i].createtime.indexOf("T"))
+ "\n" +
" \n" +
alist[i].content +
" \n" +
" + alist[i].id + "&acount=" +
alist.length
+ "\" class=\"detail2\">查看全文 >>\n" +
" + alist[i].id + ")\" class=\"detail2\">删除\n" +
" ";
}
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的正则表达式{或