基于spring boot的博客开发

学习一下当前web开发中比较火的spring boot,直接就实战开发了,从开发中慢慢学习。

个人觉得这个课程是初步了解中最好的,详细,并且有解释,还有配套资源。
https://www.bilibili.com/video/BV1KJ411R7XL?spm_id_from=333.999.0.0&vd_source=b7500783c2d0db35836200a2d00d0489

写完了后,感觉还是学到不少东西,了解了一些springboot的知识,一个网站的搭建,如何接收前端传的数据,如何处理,如何从数据库中提取数据,如何返回数据给前端,但是更多还是一个初步的了解吧,更多的详细点肯定是需要系统的学习的。

写完了也上传到了github:https://github.com/The-Itach1/Springboot-blog

前端开发

由于个人不关注与前端,所以,直接用现成的。

后端开发

构建框架

先下载破解版,https://www.cnblogs.com/technicist/p/15229615.html
破解版有更多的东西。

创建项目,发现没有Spring Initializr,去搜索这个插件然后下载就行。

然后跟着视频操作就行,添加下面的模块。

  • web
  • Thymeleaf
  • JPA
  • MySQL
  • Aspects
  • DevTools

然后发现我的IDEA没有core,然后添加下面的模块

  • Thymeleaf
  • JPA
  • MySQL
  • Spring Web
  • Spring Boot DevTools
  • Lombok

配置Thymeleaf的版本,在pom.xml中添加

<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>

配置thymeleaf模块,mysql,jpa(方便数据操作的东西),以及使用navicat创建一个blog数据库,navicat破解版:https://www.cnblogs.com/hhaostudy/p/15898030.html

spring:
  thymeleaf:
    mode: HTML
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/myblog?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true #将数据库的一些信息可以输出在日志和控制台中。

配置日志,日志级别,日志路径,都是spring默认的。

logging:
  level:
    root: info
    com.The_Itach1: debug #给自己的包一个日志级别
  file:
    name: log/blog.log #日志位置,默认按10m划分

然后可以自己去设置日志的相关内容,resources文件夹下创建logback-spring.xml,就是可以重写springboot集成的logback相关的一些配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

<?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、重写了默认配置,设置日志文件大小在10MB时,按日期切分日志
-->

开发环境,dev,生成环境,pro,复制两个配置文件就可以了,主要是生成和开发需要的一些配置不一样。

最后需要注意的是springboot版本在2.0以上,应配置为driver‐class‐name: com.mysql.cj.jdbc.Driver,并且你的mysql得打开。

异常处理

配置页面

错误页面,404错误,500错误,error错误页面。

先创建一个index.html,放在templates下。

将错误页面放在resources/tempplates中,动态的,创建html文件就行。

然后需要测试下,在com.the_itach1下,新建web软件包,在里面定义一个控制器类,作用就是如果以get方式访问根路径,会跳转到index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

package com.the_itach1.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

//添加一个Controller代表这是控制器
@Controller
public class IndexController {
//GetMapping,以get方式访问根路径时就返回到index.html界面
@GetMapping("/")
public String index(){
//故意触发错误,会转到500.html
int i=9/0;
return "index.html";
}
}

然后发现了问题,就是访问不到resources目录下的文件,造成的原因,spring boot的版本高了,解决方法,降低版本或者,在yml配置中添加下面的语句。

spring:
  web:
    resources:
      static-locations: classpath:/templates/,classpath:/static/

然后运行开始测试。

全局异常处理

自定义错误异常处理,在代码出问题的时候,返回到自己的错误页面,并显示一些错误信息。

我们需要创建一个在com.The_Itach1文件夹下面新建hander包,创建ControllerExceptionHandler错误页面拦截器,通过定义这个类来拦截所有的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37


package com.the_itach1.handler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

//拦截异常处理
@ControllerAdvice
public class ControllerExceptionHandler {

private final Logger logger= LoggerFactory.getLogger(this.getClass());

//ModelAndView这个对象可以控制返回一个页面,和控制输出到前端的信息。
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHander(HttpServletRequest request,Exception e)
{
//控制台会输出url和异常
logger.error("Requst URL : {}, Exception : {}",request.getRequestURI(),e);

//创建一个ModelAndView对象
ModelAndView mv=new ModelAndView();
//添加url,和异常信息
mv.addObject("url",request.getRequestURI());
mv.addObject("excption",e);
//选择到那个页面展示
mv.setViewName("error/error.html");

return mv;
}
}

然后这样还不够,还想要在页面也能显示我们的详细错误,我们就可以直接在页面查看错误信息,就不用在服务器中查看日志了,这我们就必须修改对应的html代码了。

error.html,添加下面代码

1
2
3
4
5
6
7
8
9
10

<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>

然后发现好像不行,还不知道怎么回事

资源找不到异常

自己定义一个异常类,例如如果找不到想要的资源,跳到自己的异常类进行处理,然后响应。

MyNotFoundException类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

package com.the_itach1;

//定义一个自己的异常处理类,继承RuntimeException,来捕获异常

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

//为了跳到404页面,还需要一个返回的状态吗,也就是HttpStatus.NOT_FOUND,这样就能跳到404.html
@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyNotFoundException extends RuntimeException{
public MyNotFoundException() {
super();
}

public MyNotFoundException(String message) {
super(message);
}

public MyNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}


为了测试,然后在控制器,IndexController中抛出这个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@Controller
public class IndexController {
//GetMapping,以get方式访问根路径时就返回到index.html界面
@GetMapping("/")
public String index(){
//故意触发错误,会转到500.html
//int i=9/0;

String blog=null;
if(blog==null)
{
throw new MyNotFoundException("博客不存在");
}
return "index.html";
}
}

这时候实际上还不行,因为我们之前创建了一个错误页面拦截的ControllerExceptionHandler类,会将异常都拦截下来,所以我们需要进行一些判断处理了,对于这些带状态码的异常,我们将其抛给springboot自己处理。

在ControllerExceptionHandler中添加。

1
2
3
4
5

//判断是否存在带状态码的异常,这些异常就由springboot自己处理
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}

日志处理

要记录日志内容

使用spring的aop,具体说是什么切断数据流,创造切面。

要记录的日志

  • 请求 url
  • 访问者 ip
  • 调用方法 classMethod
  • 参数 args
  • 返回内容

日志类

创建一个软件包,创建日志类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

package com.the_itach1.aspect;

import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

//切面
@Aspect
@Component
public class LogAspect {

private final Logger logger= LoggerFactory.getLogger(this.getClass());

//切点,拦截那些类,拦截com.the_itach1.web软件包中的所有类和方法。
@Pointcut("execution(* com.the_itach1.web.*.*(..))")
public void log(){
}

//在所有请求开始前执行doBerfore
@Before("log()")
public void doBefore()
{
logger.info("-----------doBefore------------");
}

//请求执行后
@After("log()")
public void doAfter()
{
logger.info("-----------doAfter-------------");
}

//执行后的返回值
@AfterReturning(returning = "result",pointcut = "log()")
public void doAfterReturn(Object result)
{
logger.info("Result : {}" , result);
}
}

然后修改下web控制器,里面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

package com.the_itach1.web;

import com.the_itach1.MyNotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

//添加一个Controller代表这是控制器
@Controller
public class IndexController {
//GetMapping,以get方式访问指定路径时就返回到index.html界面
@GetMapping("/{id}/{name}")
public String index(@PathVariable Integer id ,@PathVariable String name){

System.out.println("---------index---------");
return "../index.html";
}
}

这里我是将return 改为了../index.html 运行,要不然找不到首页,会报错,可以看到这些日志打印的顺序。

接下来添加代码,记录我们想要的url,ip,方法,返回值等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

package com.the_itach1.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

//切面
@Aspect
@Component
public class LogAspect {

private final Logger logger= LoggerFactory.getLogger(this.getClass());

//切点,拦截那些类,拦截com.the_itach1.web软件包中的所有类和方法。
@Pointcut("execution(* com.the_itach1.web.*.*(..))")
public void log(){
}

//在所有请求开始前执行doBerfore
@Before("log()")
public void doBefore(JoinPoint joinPoint)
{
ServletRequestAttributes attributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request=attributes.getRequest();

//获取url
String url=request.getRequestURI();
//获取访问的ip地址
String ip=request.getRemoteAddr();
//获取方法名称
String classMethod= joinPoint.getSignature().getDeclaringTypeName()+ "."+ joinPoint.getSignature().getName();
//获取参数
Object[] args=joinPoint.getArgs();
//创建我们的Requestlog类,返回字符串
Requestlog requestlog=new Requestlog(url,ip,classMethod,args);

//在日志中打印出来
logger.info("Request : {}",requestlog);
//logger.info("-----------doBefore------------");
}

//请求执行后
@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 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 "Requestlog{" +
"url='" + url + '\'' +
", ip='" + ip + '\'' +
", classMethod='" + classMethod + '\'' +
", args=" + Arrays.toString(args) +
'}';
}
}


}

2022-07-06 17:57:42.848 INFO 6316 --- [nio-4000-exec-1] com.the_itach1.aspect.LogAspect : Request : Requestlog{url='/11/aaa', ip='0:0:0:0:0:0:0:1', classMethod='com.the_itach1.web.IndexController.index', args=[11, aaa]}
---------index---------
2022-07-06 17:57:42.856 INFO 6316 --- [nio-4000-exec-1] com.the_itach1.aspect.LogAspect : Result : ../index.html

页面处理

这部分主要是将之前编写好的前端页面,添加进这个项目中。

弄得满足格式后,直接复制进来。

然后运行后,访问index,发现图片和自己的css都加载不进来,在application.yml下添加下面语句,并且好像不需要去添加th:href,不知道是不是因为我将thymeleaf版本升高了的原因。

  mvc:
    static-path-pattern: /static/**

然后就ok了,但是之前的error.html还是因为thymeleaf的原因无法成功,虽然已经换了版本,但是还是不行,不行就算了。

实际类构建

这一部分就是创建一些类,博客类,评论类,标签类,文章类型类,用户类,我们需要明确这些类的成员,以及互相的关系(一对多,多对一,多对多),并且在数据库中生成相应的表结构,直接就在代码中体现了。

然后维护和被维护,实际上就是外键,比如说blog对于Type,blog是维护的一方,Type就是被维护的一方,t_blog就含有外键type_id。

下面是教程中的一张图,可以方便理解关系。

创建po软件包,在里面创建我们需要的类。

Blog类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

package com.the_itach1.po;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

//和数据库相关系
@Entity
@Table(name = "t_blog")
public class Blog {


//id 标题 内容 首图 标记 浏览次数 赞赏开启 版权开启 评论开启 是否发布 创建时间 更新时间
//主键
@Id
@GeneratedValue
private Long id;

private String title;
private String content;
private String firstPicture;
private String flag;
private Integer views;
private boolean appreciation;
private boolean shareStatement;
private boolean commentabled;
private boolean published;
private boolean recommend;

//数据库中保存的时间,就是当时写评论的时间
@Temporal(TemporalType.TIMESTAMP)
private Date creatTime;
//数据库中更新的时间,就是当时更新评论的时间
@Temporal(TemporalType.TIMESTAMP)
private Date updateTime;

//blog 对于 Type是 多对一 ,blog是维护关系
@ManyToOne
private Type type;
//blog 对于 tags 多对多 ,blog是维护关系,级联新增,新增一个博客时,同时也新增一个tag,并且保存到数据库
@ManyToMany(cascade = {CascadeType.PERSIST})
private List<Tag> tags= new ArrayList<>();
//blog 对于 User是 多对一 ,blog是维护关系
@ManyToOne
private User user;

//blog 对于 User是 一对多 ,blog是被维护关系
@OneToMany(mappedBy = "blog")
private List<Comment> comments=new ArrayList<>();

public Blog() {
}

public void setId(Long id) {
this.id = id;
}

public void setTitle(String title) {
this.title = title;
}

public void setContent(String content) {
this.content = content;
}

public void setFirstPicture(String firstPicture) {
this.firstPicture = firstPicture;
}

public void setFlag(String flag) {
this.flag = flag;
}

public void setViews(Integer views) {
this.views = views;
}

public void setAppreciation(boolean appreciation) {
this.appreciation = appreciation;
}

public void setShareStatement(boolean shareStatement) {
this.shareStatement = shareStatement;
}

public void setCommentabled(boolean commentabled) {
this.commentabled = commentabled;
}

public void setPublished(boolean published) {
this.published = published;
}

public void setRecommend(boolean recommend) {
this.recommend = recommend;
}

public void setCreatTime(Date creatTime) {
this.creatTime = creatTime;
}

public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}

public Long getId() {
return id;
}

public String getTitle() {
return title;
}

public String getContent() {
return content;
}

public String getFirstPicture() {
return firstPicture;
}

public String getFlag() {
return flag;
}

public Integer getViews() {
return views;
}

public boolean isAppreciation() {
return appreciation;
}

public boolean isShareStatement() {
return shareStatement;
}

public boolean isCommentabled() {
return commentabled;
}

public boolean isPublished() {
return published;
}

public boolean isRecommend() {
return recommend;
}

public Date getCreatTime() {
return creatTime;
}

public Date getUpdateTime() {
return updateTime;
}

public Type getType() {
return type;
}

public void setType(Type type) {
this.type = type;
}
public List<Tag> getTags() {
return tags;
}

public void setTags(List<Tag> tags) {
this.tags = tags;
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

public List<Comment> getComments() {
return comments;
}

public void setComments(List<Comment> comments) {
this.comments = comments;
}

@Override
public String toString() {
return "Blog{" +
"id=" + id +
", title='" + title + '\'' +
", content='" + content + '\'' +
", firstPicture='" + firstPicture + '\'' +
", flag='" + flag + '\'' +
", views=" + views +
", appreciation=" + appreciation +
", shareStatement=" + shareStatement +
", commentabled=" + commentabled +
", published=" + published +
", recommend=" + recommend +
", creatTime=" + creatTime +
", updateTime=" + updateTime +
'}';
}
}

Type类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

package com.the_itach1.po;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "t_type")
public class Type {

@Id
@GeneratedValue
private Long id;
private String name;

//Type对于blog是一对多,type是被维护关系
@OneToMany(mappedBy = "type")
private List<Blog> blogs=new ArrayList<>();

public Type() {
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Blog> getBlogs() {
return blogs;
}

public void setBlogs(List<Blog> blogs) {
this.blogs = blogs;
}

@Override
public String toString() {
return "Type{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

Tag类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

package com.the_itach1.po;


import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "t_tag")
public class Tag {

@Id
@GeneratedValue
private Long id;
private String name;

//Tag对于blog是多对多,Tag是被维护关系
@ManyToMany(mappedBy = "tags")
private List<Blog> Blogs=new ArrayList<>();

public Tag() {
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Blog> getBlogs() {
return Blogs;
}

public void setBlogs(List<Blog> blogs) {
Blogs = blogs;
}

@Override
public String toString() {
return "Tag{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

package com.the_itach1.po;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Entity
@Table(name = "t_user")
public class User {

//id 昵称 用户名 密码 邮箱
@Id
@GeneratedValue
private Long id;
private String nickname;
private String username;
private String password;
private String email;
private String avatar;
private Integer type;

//数据库中保存的时间,就是当时写评论的时间
@Temporal(TemporalType.TIMESTAMP)
private Date creatTime;
//数据库中更新的时间,就是当时更新评论的时间
@Temporal(TemporalType.TIMESTAMP)
private Date updateTime;

//User对于blog是一对多,User是被维护关系
@OneToMany(mappedBy = "user")
private List<Blog> blogs=new ArrayList<>();
public User() {
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getAvatar() {
return avatar;
}

public void setAvatar(String avatar) {
this.avatar = avatar;
}

public Integer getType() {
return type;
}

public void setType(Integer type) {
this.type = type;
}

public Date getCreatTime() {
return creatTime;
}

public void setCreatTime(Date creatTime) {
this.creatTime = creatTime;
}

public Date getUpdateTime() {
return updateTime;
}

public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}

public List<Blog> getBlogs() {
return blogs;
}

public void setBlogs(List<Blog> blogs) {
this.blogs = blogs;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", nickname='" + nickname + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", avatar='" + avatar + '\'' +
", type=" + type +
", creatTime=" + creatTime +
", updateTime=" + updateTime +
'}';
}
}

Comment类,包含了子评论和父评论之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

package com.the_itach1.po;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Entity
@Table(name = "t_comment")
public class Comment {

//id 昵称 邮箱 评论内容 头像图片链接 创建时间

@Id
@GeneratedValue
private Long id;
private String nickname;
private String email;
private String content;
private String avatar;

//数据库中保存的时间,就是当时写评论的时间
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;

//Comment对于Blog是多对一
@ManyToOne
private Blog blog;


//评论自己内部之间的关系

//子评论 父评论对于子评论是一对多
@OneToMany(mappedBy = "parentComment")
private List<Comment> replyComments= new ArrayList<>();;

//父评论 子评论对于父评论是多对一
@ManyToOne
private Comment parentComment;

public Comment() {
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getAvatar() {
return avatar;
}

public void setAvatar(String avatar) {
this.avatar = avatar;
}

public Date getCreateTime() {
return createTime;
}

public void setCreateTime(Date createTime) {
this.createTime = createTime;
}

public Blog getBlog() {
return blog;
}

public void setBlog(Blog blog) {
this.blog = blog;
}

@Override
public String toString() {
return "Comment{" +
"id=" + id +
", nickname='" + nickname + '\'' +
", email='" + email + '\'' +
", content='" + content + '\'' +
", avatar='" + avatar + '\'' +
", createTime=" + createTime +
'}';
}
}

然后运行,就可以在数据库中看到生成的一些数据表了。

后台登录-1

还是先弄一些前端的东西,登录界面,后台index。

然后创建service软件包,创建UserService接口,定义一个checkUser(),用来验证用户登录。

1
2
3
4
5
6
7
8
9

package com.the_itach1.service;

import com.the_itach1.po.User;

public interface UserService {

User checkUser(String username,String password);
}

然后创建UserServiceImpl去重载这个函数,接下来还会设计到从数据库中提取User的信息,所以需要用到jpa。

具体操作就是创建一个dao软件包,然后定义一个UserRepository接口,继承JpaRepository,然后定义个查询函数,就ok,具体如下。

1
2
3
4
5
6
7
8
9
10

package com.the_itach1.dao;

import com.the_itach1.po.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Long> {
//根据username和password来查询用户
User findByUsernameAndPassword(String username,String password);
}

创建个UserServiceImpl类,重写下checkUser函数,返回具体的user。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

package com.the_itach1.service;

import com.the_itach1.dao.UserRepository;
import com.the_itach1.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@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;
}
}

接下来编写登录的web控制器,先进入login.html,通过前端post传入的参数,然后采取不同的措施。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

package com.the_itach1.web;

import com.the_itach1.po.User;
import com.the_itach1.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpSession;


@Controller
@RequestMapping("/admin")
public class LoginControl {

@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) {
//设置session,并且转到后台index
session.setAttribute("user", user);
return "admin/index";
} else {
//添加错误信息
attributes.addFlashAttribute("message","用户名或密码错误");
//使用重定向,不要直接返回到login
return "redirect:/admin";
}
}

//注销
@GetMapping("logout")
public String logout(HttpSession session)
{
//删除session,并重定向到登陆界面
session.removeAttribute("user");

return "redirect:/admin";
}
}

我的前端和视频中教的不太一样,可能由于我thymeleaf版本更高的原因,不需要对th这些做其他处理,直接在原html弄就行,所以我就没有像视频那样弄_fragments,也不知道后面是不是必须要这个东西,到时候在说。

我这里的html对应的两个传参是,直接就可以用。

<form class="ui large form" method="post" action="#" th:action="@{/admin/login}">

<a href="#" th:href="@{/admin/logout}" class="item">注销</a>

最后再填一段js验证用户名和密码是否为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

<script>
$('.ui.form').form({
fields : {
username : {
identifier: 'username',
rules: [{
type : 'empty',
prompt: '请输入用户名'
}]
},
password : {
identifier: 'password',
rules: [{
type : 'empty',
prompt: '请输入密码'
}]
}
}
});
</script>

然后效果如下。

登录进去后

后台登录-2

这里主要是添加md5加密password,主要是为了一个安全性。

添加一个Md5加密的工具软件包,创建md5类,写一个md5加密的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

package com.the_itach1.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util {


/**
* MD5加密类
* @param str 要加密的字符串
* @return 加密后的字符串
*/
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));
}
//32位加密
return buf.toString();
// 16位的加密
//return buf.toString().substring(8, 24);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}

}

public static void main(String[] args) {
System.out.println(code("111111"));
}
}

然后去修改checkUser这个方法。

 User user = userRepository.findByUsernameAndPassword(username, Md5Util.code(password));

接下来添加拦截器,目的就是防止没有权限的人,仍然可以根据已知路径访问到不能访问的资源界面等。

添加了一个BlogController类,用来测试,现在是无论怎么样都可以访问到admin/blogs.html。

用springboot内置interceptor的来做拦截器。

创建一个interceptor软件包,创建一个登陆拦截器类,LoginInterceptor,其实现就是重写了HandlerInterceptor的preHandle方法,作为一个网,达到拦截没处于登陆状态的用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

package com.the_itach1.interceptor;


import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//相当于一张网,拦截权限不够的访问。
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;
}
}

拦截器设置好了,肯定还需要配置下,访问那些url需要经过拦截器,那些又不需要通过拦截器,通俗讲就是,我们要将网安装在那些位置。

添加一个WebConfig,web控制器配置类,这个类可以拦截经过web控制器的所有请求,然后添加拦截器,原理是重写了WebMvcConfigurer的addInterceptors方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

package com.the_itach1.interceptor;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

//代表配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {

//添加拦截器,并且配置web控制器,admin目录下除了admin还有admin/login,其他url都需要经过拦截器。
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin")
.excludePathPatterns("/admin/login");
}
}

这时候,我们访问根目录/,是可以访问的,访问除了/admin和/admin/login这两个url,/admin下其他的路径,都会经过我们设置的拦截器。

分类管理-1

同样先创建好前端页面,这里我直接复制。

同样需要同数据库进行操作,涉及到增删查改等,所以还是像User类一样差不多的操作,创建接口,重写函数。

创建接口,TypeService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

package com.the_itach1.service;

import com.the_itach1.po.Type;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface TypeService {

//保存标签
Type saveType(Type type);
//获取标签
Type getType(Long id);
//分页查询 链表的方式
Page<Type> listType(Pageable pageable);
//修改标签
Type updateType(Long id,Type type);
//删除标签
void deletType(Long id);

}

创建接口TypeRepository,继承数据库交互JpaRepository。

重写函数,TypeServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

package com.the_itach1.service;

import com.the_itach1.MyNotFoundException;
import com.the_itach1.dao.TypeRepository;
import com.the_itach1.po.Type;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@Service
public class TypeServiceImpl implements TypeService{

@Autowired
private TypeRepository typeRepository;

//将增删改查放在事务里面
@Transactional
@Override
public Type saveType(Type type) {
return typeRepository.save(type);
}

@Transactional
@Override
public Type getType(Long id) {
return typeRepository.findById(id).orElse(null);
}

@Transactional
@Override
public Page<Type> listType(Pageable pageable) {
return typeRepository.findAll(pageable);
}

@Transactional
@Override
public Type updateType(Long id, Type type) {
Type t=typeRepository.findById(id).orElse(null);
if(t==null)
{
throw new MyNotFoundException("不存在该类型");
}
BeanUtils.copyProperties(type,t);

return typeRepository.save(t);
}

@Transactional
@Override
public void deletType(Long id) {
typeRepository.deleteById(id);
}
}

和数据库的交互就写完了,现在写Type对应的web控制器,这里还比较复杂,因为涉及到和前端的交互,然后我发现,教程中的_fragments.html实际上是一个全局的设置,防止我们重复性的对导航和底部进行编辑,然后这里我也不打算去使用了,因为实际上并不是很多,大不了,多改点。

这里存在一个问题,如果采用课程中的RESTful传参方法,会导致url路径改变,这样我们访问我们的一些css资源或者jpg就不行了,所以这里我采用传统的?get传参,并且修改了url,使其可读性跟好一点。

最好是和前端html一起理解,并且我简单修改了下其判空的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

package com.the_itach1.web.admin;

import com.the_itach1.po.Type;
import com.the_itach1.service.TypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/admin")
public class TypeController {

@Autowired
private TypeService typeService;


//分页展示,每页3条,按id排序,向前台发送page消息。
@GetMapping("/types")
public String types(@PageableDefault(size = 3,sort = {"id"},direction = Sort.Direction.DESC)
Pageable pageable, Model model) {
model.addAttribute("page",typeService.listType(pageable));
return "admin/types";
}

//新建一个类型,传一个空的Type对象过去,让前端编辑
@GetMapping("/types/input-add")
public String addinput(Model model) {
model.addAttribute("type", new Type());
return "admin/types-input";
}

//根据要修改的类型的id,得到其相应类型,返回给前端

@GetMapping("/types/edit")
public String editInput(@RequestParam("id") Long id, Model model) {
model.addAttribute("type", typeService.getType(id));
return "admin/types-input";
}

//根据id来删除某个标签
@GetMapping("/types/delete")
public String delete(@RequestParam("id") Long id,RedirectAttributes attributes) {
typeService.deletType(id);
attributes.addFlashAttribute("message", "删除成功");
return "redirect:/admin/types";
}


//前端编辑好我们类型后,返回给后端,创建一个类型
@PostMapping("/types")
public String addpost(Type type, BindingResult result, RedirectAttributes attributes) {

//判断数据是否重复和不为空,并在前端生成相应提示。
Type type1 = typeService.getTypeByName(type.getName());
if (type1 != null) {
result.rejectValue("name","nameError","不能添加重复的分类");
}
if (type.getName().isEmpty()) {
result.rejectValue("name","nameError","输入标签不能为空");
}
if (result.hasErrors()) {
return "admin/types-input";
}

//保存类型,并判断是否成功,并在前端生成相应提示
Type t = typeService.saveType(type);
if (t == null ) {
attributes.addFlashAttribute("message", "新增失败");
} else {
attributes.addFlashAttribute("message", "新增成功");
}
return "redirect:/admin/types";
}

@PostMapping("/types/{id}")
public String editPost(Type type, BindingResult result,@PathVariable Long id ,RedirectAttributes attributes) {
//判断数据是否重复和不为空,并在前端生成相应提示。
Type type1 = typeService.getTypeByName(type.getName());
if (type1 != null) {
result.rejectValue("name","nameError","不能添加重复的分类");
}
if (type.getName().isEmpty()) {
result.rejectValue("name","nameError","输入标签不能为空");
}
if (result.hasErrors()) {
return "admin/types-input";
}

//更新类型,并判断是否成功,并在前端生成相应提示
Type t = typeService.updateType(id,type);
if (t == null ) {
attributes.addFlashAttribute("message", "更新失败");
} else {
attributes.addFlashAttribute("message", "更新成功");
}
return "redirect:/admin/types";
}


}

标签管理

和类型管理是一样的,不多阐述。

博客列表管理

也差不多,区别是,多了一个多条件查询。

采用的方法是JpaSpecificationExecutor,具体操作如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

//这里需要注意的点是这个page的展示,这里是查询了后,对选出的博客进行展示,设计到多条件查询
@Override
public Page<Blog> listBlog(Pageable pageable, Blog blog) {
return blogRepository.findAll(new Specification<Blog>() {
@Override
public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
//将条件都存到这个List中
List<Predicate> predicateList=new ArrayList<>();

//如果查询的博客标题不为空
if(!blog.getTitle().isEmpty() && blog.getTitle()!=null)
{
//like查询,包含字符串都查出来
predicateList.add(criteriaBuilder.like(root.<String>get("title"),"%"+blog.getTitle()+"%"));
}
//类型
if(blog.getType().getId()!=null)
{
predicateList.add(criteriaBuilder.equal(root.<Type>get("type"),blog.getType().getId()));
}
//是否推荐
if(blog.isRecommend())
{
predicateList.add(criteriaBuilder.equal(root.<Boolean>get("recommend"),blog.isRecommend()));
}
query.where(predicateList.toArray(new Predicate[predicateList.size()]));
return null;
}
},pageable);
}

然后同样构建web层,前端的具体细节还是没怎么看懂,主要是自己写肯定写不出来,不管了,知道怎么发送数据就完事了,这里还采用了局部变换,也就是search接口,只会改变下面的列表,不会修改搜索框中的内容。

由于blog.getType().getId(),会爆出指针为null的错误,所以新建一个BlogQuery,来传递title,TypeId,recommend进行数据库的查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

package com.the_itach1.web.admin;

import com.the_itach1.po.Blog;
import com.the_itach1.service.BlogService;
import com.the_itach1.service.TypeService;
import com.the_itach1.vo.BlogQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/admin")
public class BlogController {

@Autowired
private BlogService blogService;

@Autowired
private TypeService typeService;

@GetMapping("/blogs")
public String blogs(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable , BlogQuery blog, Model model)
{
//将所有的Type传到前端,用menu菜单进行展示,传回来的是typeId。
model.addAttribute("types",typeService.listType());

//向前端展示所有的博客信息。
model.addAttribute("page",blogService.listBlog(pageable,blog));
return "admin/blogs";
}

//Post传参发送搜索,并且只使前端的局部改变,向前端发送搜索后的博客。
@PostMapping("/blogs/search")
public String search(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable , BlogQuery blog, Model model)
{
model.addAttribute("page",blogService.listBlog(pageable,blog));
return "admin/blogs :: blogList";
}
}

博客新增

用markdown的格式新增博客,也就是增删查改了,这里大体和Type Tag类型差不多,只不过需要多传东西到前端,也就是传Type列表和Tag列表到前端,产生menu供我们选择。

然后需要注意的是,我们进行编辑的时候,由于tags有多个,而且我们要将原本的tags也显现出来,然后传给前端的是多个id,但是,这些id我们实际上并没有存到数据库,也就是说从数据库中拿出blog时这些id是没有的,所以我们在service层写了一个可以通过数据库中的tags获取到其所有id的方法,方便传到前端,然后进行展示。

web控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.the_itach1.web.admin;

import com.the_itach1.po.Blog;
import com.the_itach1.po.User;
import com.the_itach1.service.BlogService;
import com.the_itach1.service.TagService;
import com.the_itach1.service.TypeService;
import com.the_itach1.vo.BlogQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/admin")
public class BlogController {

@Autowired
private BlogService blogService;

@Autowired
private TypeService typeService;

@Autowired
private TagService tagService;

@GetMapping("/blogs")
public String blogs(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable , BlogQuery blog, Model model)
{
//将所有的Type传到前端,用menu菜单进行展示,传回来的是typeId。
model.addAttribute("types",typeService.listType());

//向前端展示所有的博客信息。
model.addAttribute("page",blogService.listBlog(pageable,blog));
return "admin/blogs";
}

//Post传参发送搜索,并且只使前端的局部改变,向前端发送搜索后的博客。
@PostMapping("/blogs/search")
public String search(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable , BlogQuery blog, Model model)
{
model.addAttribute("page",blogService.listBlog(pageable,blog));
return "admin/blogs :: blogList";
}

//添加一个博客,转到添加博客界面
@GetMapping("/blogs/input-add")
public String addinput(Model model)
{
//传一个空的blog对象,让前端进行编辑
model.addAttribute("blog",new Blog());
//将所有的Type传到前端,用menu菜单进行展示。
model.addAttribute("types",typeService.listType());
//将所有的Tag传到前端,用menu菜单进行展示。
model.addAttribute("tags",tagService.listTag());
return "admin/blogs-input";
}

@GetMapping("/blogs/edit")
public String editInput(@RequestParam("id") Long id, Model model) {

Blog b;
b=blogService.getBlog(id);
//需要注意的是,我们想要展示原来的tags,但是由于tagIds变量并没有存到数据库,所以取出来时是没有赋值的,这里我们定义一个标签得到tagIds的方法。
b.inittagIds();
//根据get传参的id,来判断哪一个博客需要重新编辑。
model.addAttribute("blog", b);
//将所有的Type传到前端,用menu菜单进行展示。
model.addAttribute("types",typeService.listType());
//将所有的Tag传到前端,用menu菜单进行展示。
model.addAttribute("tags",tagService.listTag());
return "admin/blogs-input";
}

//根据id来删除某个博客
@GetMapping("/blogs/delete")
public String delete(@RequestParam("id") Long id, RedirectAttributes attributes) {
blogService.deleteBlog(id);
attributes.addFlashAttribute("message", "删除成功");
return "redirect:/admin/blogs";
}

//将编辑好的博客,添加或重新更新到数据库
@PostMapping("/blogs")
public String post(RedirectAttributes attributes ,Blog blog, HttpSession session)
{
//给blog设置user对象,这篇博客是哪个user的
blog.setUser((User) session.getAttribute("user"));

//给blog设置type
blog.setType(typeService.getType(blog.getType().getId()));
//给blog设置tags,需要注意的是有多个tags
blog.setTags(tagService.listTag(blog.getTagIds()));

Blog b;

if (blog.getId() == null) {
b = blogService.saveBlog(blog);
} else {
b = blogService.updateBlog(blog.getId(), blog);
}


if (b == null ) {
attributes.addFlashAttribute("message", "操作失败");
} else {
attributes.addFlashAttribute("message", "操作成功");
}

return "redirect:/admin/blogs";
}
}

这里更新时间和访问次数仿佛有点问题。需要对blog的service层进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27


@Transactional
@Override
public Blog saveBlog(Blog blog) {
if (blog.getId() == null) {
blog.setCreateTime(new Date());
blog.setUpdateTime(new Date());
blog.setViews(0);
} else {
blog.setUpdateTime(new Date());
}
return blogRepository.save(blog);
}

@Transactional
@Override
public Blog updateBlog(Long id, Blog blog) {
Blog b = blogRepository.findOne(id);
if (b == null) {
throw new NotFoundException("该博客不存在");
}
BeanUtils.copyProperties(blog,b, MyBeanUtils.getNullPropertyNames(blog));
b.setUpdateTime(new Date());
return blogRepository.save(b);
}

博客首页展示

首页需要展示所有能发布的博客,按更新时间排序,然后就是排名前几的标签,类型栏,按从到小排序,然后还有按更新时间的推荐栏。

由于排序的要求不同,所以对数据库的查询方式也不同,具体实现去看对应的Service层和dao层。

index的web控制层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

package com.lrm.web;

import com.lrm.NotFoundException;
import com.lrm.service.BlogService;
import com.lrm.service.TagService;
import com.lrm.service.TypeService;
import com.lrm.vo.BlogQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
* Created by limi on 2017/10/13.
*/
@Controller
public class IndexController {

@Autowired
private BlogService blogService;

@Autowired
private TypeService typeService;

@Autowired
private TagService tagService;

@GetMapping("/")
public String index(@PageableDefault(size = 8, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
Model model) {
model.addAttribute("page",blogService.listBlog(pageable));
model.addAttribute("types", typeService.listTypeTop(6));
model.addAttribute("tags", tagService.listTagTop(10));
model.addAttribute("recommendBlogs", blogService.listRecommendBlogTop(8));
return "index";
}

@GetMapping("/blog/{id}")
public String blog() {
return "blog";
}

}

全局搜索

进行全局搜索,给一个关键词,搜索出和这个词相关的所有文章,主要还时对于文章标题和文章内容的like模糊搜索。

具体实现就是

@Query("select b from Blog b where b.title like ?1 or b.content like ?1")
Page<Blog> findByQuery(String query,Pageable pageable);

然后就是相应的web控制器了。

1
2
3
4
5
6
7
8

@PostMapping("/search")
public String search(@PageableDefault(size = 8, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
@RequestParam String query, Model model) {
model.addAttribute("page", blogService.listBlog("%"+query+"%", pageable));
model.addAttribute("query", query);
return "search";
}

具体展示某篇文章

这里处理的问题就是,如何将文章的内容从markdown格式转为正常html显示的模式,实际上已经有代码了,可以将其直接使用。

MarkdownUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

package com.the_itach1.util;

import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.ext.heading.anchor.HeadingAnchorExtension;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.AttributeProvider;
import org.commonmark.renderer.html.AttributeProviderContext;
import org.commonmark.renderer.html.AttributeProviderFactory;
import org.commonmark.renderer.html.HtmlRenderer;

import java.util.*;

public class MarkdownUtils {

/**
* markdown格式转换成HTML格式
* @param markdown
* @return
*/
public static String markdownToHtml(String markdown) {
Parser parser = Parser.builder().build();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
}

/**
* 增加扩展[标题锚点,表格生成]
* Markdown转换成HTML
* @param markdown
* @return
*/
public static String markdownToHtmlExtensions(String markdown) {
//h标题生成id
Set<Extension> headingAnchorExtensions = Collections.singleton(HeadingAnchorExtension.create());
//转换table的HTML
List<Extension> tableExtension = Arrays.asList(TablesExtension.create());
Parser parser = Parser.builder()
.extensions(tableExtension)
.build();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder()
.extensions(headingAnchorExtensions)
.extensions(tableExtension)
.attributeProviderFactory(new AttributeProviderFactory() {
public AttributeProvider create(AttributeProviderContext context) {
return new CustomAttributeProvider();
}
})
.build();
return renderer.render(document);
}

/**
* 处理标签的属性
*/
static class CustomAttributeProvider implements AttributeProvider {
@Override
public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
//改变a标签的target属性为_blank
if (node instanceof Link) {
attributes.put("target", "_blank");
}
if (node instanceof TableBlock) {
attributes.put("class", "ui celled table");
}
}
}


public static void main(String[] args) {
String table = "| hello | hi | 哈哈哈 |\n" +
"| ----- | ---- | ----- |\n" +
"| 斯维尔多 | 士大夫 | f啊 |\n" +
"| 阿什顿发 | 非固定杆 | 撒阿什顿发 |\n" +
"\n";
String a = "[imCoding 爱编程](http://www.lirenmi.cn)";
System.out.println(markdownToHtmlExtensions(a));
}
}

当然这里需要在pom.xml中添加一些依赖。

    <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>

接下来是具体处理,过程就是根据前端传出来的id然后去数据库获得博客信息,将文章内容进行转换,然后返回到web控制器,发送给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

@Override
public Blog getAndConvert(Long id) {
Blog blog = blogRepository.findById(id).orElse(null);
if (blog == null) {
throw new MyNotFoundException("该博客不存在");
}
Blog b = new Blog();
BeanUtils.copyProperties(blog,b);
String content = b.getContent();
b.setContent(MarkdownUtils.markdownToHtmlExtensions(content));

blogRepository.updateViews(id);
return b;
}

对应的web控制器。

1
2
3
4
5
6

@GetMapping("/blog/text")
public String blog(@RequestParam("id") Long id,Model model) {
model.addAttribute("blog", blogService.getAndConvert(id));
return "blog";
}

然后就是对于views的处理,可以在service层单独写一个更新view的方法,但是更简便的方法还是直接在dao层自己写一个更新views的sql语句处理。

@Transactional
@Modifying
@Query("update Blog b set b.views = b.views+1 where b.id = ?1")
int updateViews(Long id);

Type归类

这个主要干的事情有两个。

  • 得到一定的Type类型,和其对应的文章数目,展示在导航的下方。
  • 点击某一Type,显示对应的文章。

第一个简单,博客首页展示的那个Type栏一样。

第二个,根据TypeId来查询对应的博客,实际上这个功能我们也已经在后台blog管理中实现过了。需要注意的点是,我们需要初始默认一个id展示,所以还需要遍历一下,直到找到一个数据库存在的id。

本来打算又修改下传参方式,防止一些资源找不到,结果发现能找到,就不改了,并且这里也不好改,因为前端还设计到了换页的问题,实际上也是一个比较麻烦的问题。

TypeShowController,调用的函数都是之前写过的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

package com.the_itach1.web;

import com.the_itach1.po.Type;
import com.the_itach1.service.BlogService;
import com.the_itach1.service.TypeService;
import com.the_itach1.vo.BlogQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;


import java.util.List;
@Controller
public class TypeShowController {

@Autowired
private TypeService typeService;

@Autowired
private BlogService blogService;

@GetMapping("/types/{id}")
public String types(@PageableDefault(size = 2, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
@PathVariable Long id, Model model) {

//得到某一数量的Type类型,默认为10000
List<Type> types = typeService.listTypeTop(10000);

//默认初始展示,遍历直到有具体的id
if (id == -1) {
id = types.get(0).getId();
}

//指定id展示
BlogQuery blogQuery = new BlogQuery();
blogQuery.setTypeId(id);

//展示查到的所有类型
model.addAttribute("types", types);
//根据当前选中的或者默认的一个Typeid,根据其查询对应blog,返回给前端
model.addAttribute("page", blogService.listBlog(pageable, blogQuery));
//返回当前TypeId,供下一页或上一页使用。
model.addAttribute("activeTypeId", id);
return "types";
}
}

Tag归类

大题和Type归类差不多,之前Type是使用的多条件,这里我们就创建一个单条件的方法查询。

1
2
3
4
5
6
7
8
9
10
11
12


@Override
public Page<Blog> listBlog(Long tagId, Pageable pageable) {
return blogRepository.findAll(new Specification<Blog>() {
@Override
public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
Join join = root.join("tags");
return cb.equal(join.get("id"),tagId);
}
},pageable);
}

然后其他的也没什么说的,和Type归类一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

package com.the_itach1.web;

import org.springframework.stereotype.Controller;

import com.the_itach1.po.Tag;
import com.the_itach1.service.BlogService;
import com.the_itach1.service.TagService;
import com.the_itach1.vo.BlogQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;

@Controller
public class TagShowController {

@Autowired
private TagService tagService;

@Autowired
private BlogService blogService;

@GetMapping("/tags/{id}")
public String tags(@PageableDefault(size = 8, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
@PathVariable Long id, Model model) {
List<Tag> tags = tagService.listTagTop(10000);
if (id == -1) {
id = tags.get(0).getId();
}
model.addAttribute("tags", tags);
model.addAttribute("page", blogService.listBlog(id,pageable));
model.addAttribute("activeTagId", id);
return "tags";
}
}

所有文章归档

关于页面

这个最简单,直接跳到自己写的about.html就行。