本章对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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.sangeng</groupId>
<artifactId>SGBlog</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>sangeng-framework</module>
<module>sangeng-admin</module>
<module>sangeng-blog</module>
</modules>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>


<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--mybatisPlus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>

<!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>


<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>


</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

②创建公共子模块

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SGBlog</artifactId>
<groupId>com.sangeng</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sangeng-framework</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombk-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--junit-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringSecurity启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!--mybatisPlus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>

<!--AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>

</dependencies>
</project>

③创建博客后台模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SGBlog</artifactId>
<groupId>com.sangeng</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sangeng-admin</artifactId>

<dependencies>
<dependency>
<groupId>com.sangeng</groupId>
<artifactId>sangeng-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

④创建博客前台模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SGBlog</artifactId>
<groupId>com.sangeng</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sangeng-blog</artifactId>

<dependencies>
<dependency>
<groupId>com.sangeng</groupId>
<artifactId>sangeng-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

</project>

整合MP

1.导入依赖(已在父模块导入)

2.配置yml文件

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
server:
port: 7777
spring:
redis:
host: 192.168.111.130
datasource:
url: jdbc:mysql://localhost:3306/sg_blog?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
servlet:
multipart:
max-file-size: 2MB
max-request-size: 5MB
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.maple.demo.bean
configuration:
# 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
cache-enabled: true
global-config:
db-config:
#设置逻辑删除字段
logic-delete-field: delFlag
logic-delete-value: 1
logic-not-delete-value: 0
id-type: auto

字段填充器

MetaObjectHandler 是 MyBatis-Plus 提供的一个用于处理实体对象的元数据的接口,可以用来自动填充一些常用字段,例如创建时间、修改时间等。

1.定义字段处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Long userId = null;
try {
userId = SecurityUtils.getUserId();
} catch (Exception e) {
e.printStackTrace();
userId = -1L;//表示是自己创建
}
this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("createBy",userId , metaObject);
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("updateBy", userId, metaObject);
}

@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);
}

}

2.使用注解标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 创建人的用户id
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

模型转换

DO->DTO->VO

Bean拷贝工具类封装

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
public class BeanCopyUtils {

private BeanCopyUtils() {
}

public static <V> V copyBean(Object source,Class<V> clazz) {
//创建目标对象
V result = null;
try {
result = clazz.newInstance();
//实现属性copy
BeanUtils.copyProperties(source, result);
} catch (Exception e) {
e.printStackTrace();
}
//返回结果
return result;
}
public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){
return list.stream()
.map(o -> copyBean(o, clazz))
.collect(Collectors.toList());
}
}

整合SpringSecurity

统一异常处理

实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的。但是如果我们每次都自己手动去处理就会非常麻烦。我们可以选择直接抛出异常的方式,然后对异常进行统一处理。把异常中的信息封装成ResponseResult响应给前端。

自定义异常父类SystemException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SystemException extends RuntimeException{

private int code;

private String msg;

public int getCode() {
return code;
}

public String getMsg() {
return msg;
}

public SystemException(AppHttpCodeEnum httpCodeEnum) {
super(httpCodeEnum.getMsg());
this.code = httpCodeEnum.getCode();
this.msg = httpCodeEnum.getMsg();
}

}

GlobalExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(SystemException.class)
public ResponseResult systemExceptionHandler(SystemException e){
//打印异常信息
log.error("出现了异常! {}",e);
//从异常对象中获取提示信息封装返回
return ResponseResult.errorResult(e.getCode(),e.getMsg());
}


@ExceptionHandler(Exception.class)
public ResponseResult exceptionHandler(Exception e){
//打印异常信息
log.error("出现了异常! {}",e);
//从异常对象中获取提示信息封装返回
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
}
}

统一返回结果

定义统一返回结果类,使用泛型封装内部数据

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
package com.me.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.me.enums.AppHttpCodeEnum;


import java.io.Serializable;
//标记为null的值不序列化
@JsonInclude(JsonInclude.Include.NON_NULL)
/**
* 统一响应接口
*/
public class ResponseResult<T> implements Serializable {
private Integer code;
private String msg;
private T data;

public ResponseResult() {
this.code = AppHttpCodeEnum.SUCCESS.getCode();
this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
}

public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}

public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}

public static ResponseResult errorResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.error(code, msg);
}
public static ResponseResult okResult() {
ResponseResult result = new ResponseResult();
return result;
}
public static ResponseResult okResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}

public static ResponseResult okResult(Object data) {
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
if(data!=null) {
result.setData(data);
}
return result;
}

public static ResponseResult errorResult(AppHttpCodeEnum enums){
return setAppHttpCodeEnum(enums,enums.getMsg());
}

public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
return setAppHttpCodeEnum(enums,msg);
}

public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
return okResult(enums.getCode(),enums.getMsg());
}

private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){
return okResult(enums.getCode(),msg);
}

public ResponseResult<?> error(Integer code, String msg) {
this.code = code;
this.msg = msg;
return this;
}

public ResponseResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}

public ResponseResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
return this;
}

public ResponseResult<?> ok(T data) {
this.data = data;
return this;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

}

统一返回消息

用枚举类进一步封装code与msg

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
package com.me.enums;

/**
* 状态枚举类
*/
public enum AppHttpCodeEnum {
SUCCESS(200,"操作成功"),
NEED_LOGIN(401,"需要登录后操作"),
NO_OPERATOR_AUTH(403,"无权限操作"),
SYSTEM_ERROR(500,"出现错误"),
USERNAME_EXIST(501,"用户名已存在"),
PHONENUMBER_EXIST(502,"手机号已存在"),
EMAIL_EXIST(503, "邮箱已存在"),
REQUIRE_USERNAME(504, "必需填写用户名"),
LOGIN_ERROR(505,"用户名或密码错误"),
CONTENT_NOT_NULL(506, "评论不能为空"),
USERNAME_NOT_NULL(507, "用户名不能为空"),
PASSWORD_NOT_NULL(508, "密码不能为空"),
EMAIL_NOT_NULL(509, "邮箱不能为空"),
NICKNAME_NOT_NULL(510, "昵称不能为空"),
ID_NOT_NULL(511, "id不能为空"),
REQUEST_BODY_NOT_NULL(512, "请求体不能为空"),
PARAM_NOT_NULL(513, "参数为不能空");

int code;
String msg;

AppHttpCodeEnum(int code, String errorMessage){
this.code = code;
this.msg = errorMessage;
}

public int getCode() {
return code;
}

public String getMsg() {
return msg;
}
}

统一常量管理

定义常量类SystemConstants管理常量

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
package com.me.constants;

/**
* 常量
*/
public class SystemConstants
{
/**
* 文章是草稿
*/
public static final int ARTICLE_STATUS_DRAFT = 1;
/**
* 文章是正常分布状态
*/
public static final int ARTICLE_STATUS_NORMAL = 0;
public static final int ROLE_STATUS_NORMAL = 0;

/**
* 用户状态正常
*/
public static final int USER_STATUS_NORMAL = 0;

/**
* 分类表正常状态
*/
public static final String CATEGORY_STATUS_NORMAL = "0";
/**
* 分类表是禁用状态
*/
public static final String CATEGORY_STATUS_BAN = "0";
/**
* 友联状态正常
*/
public static final String Link_STATUS_NORMAL = "0";
/**
* 友联状态禁用
*/
public static final String Link_STATUS_BAN = "1";

/**
* 根评论id
*/
public static final int ROOT_COMMENT_ID = -1;

/**
* 文章类型的评论
*/
public static final String COMMENT_TYPE_ARTICLE = "0";
/**
* 友联类型的评论
*/
public static final String COMMENT_TYPE_LINK = "1";
public static final String FILE_UPLOAD_DOMAIN = "http://rpnj2nc9r.bkt.clouddn.com/";

public static final String ARTICLE_COUNT_KEY="article:viewCount";
public static final String ROLE_TYPE_C="C";
public static final String ROLE_TYPE_F="F";


}

统一时间响应格式

webConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean//使用@Bean注入fastJsonHttpMessageConvert
public HttpMessageConverter fastJsonHttpMessageConverters() {
//1.需要定义一个Convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");

SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);

fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return converter;
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(fastJsonHttpMessageConverters());
}

统一分页Vo

1
2
3
4
5
6
7
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageVo {
private List rows;
private Long total;
}

查询多级分类树

1.根据需求查询所有的数据封装进集合(此时它们的子节点都为null)

2.转换为期望的返回结果(Vo)集合

3.定义方法传入顶层父级节点id与所有节点集合,调用设置子节点的方法

4.定义寻找子节点的方法,自行设置子节点,并递归调用

具体代码例子:

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
public List<Menu> selectRouterMenuTreeByUserId(Long userId) {
//menus获取省略
//先找出第一层的菜单 然后去找他们的子菜单设置到children属性中
List<Menu> menuTree = builderMenuTree(menus,0L);
return menuTree;
}

/**
* 构建菜单树
* @param menus 所有的菜单
* @param parentId 需要构建最高层级的id
* @return
*/
private List<MenuVo> buildMenuTree(List<Menu> menus, Long parentId) {
//转换其实应该在方法外面做
List<MenuVo> menuVos = BeanCopyUtils.copyBeanList(menus, MenuVo.class);
List<MenuVo> menuTree = menuVos.stream()
//1.过滤找到父层级的菜单
.filter(menuVo -> menuVo.getParentId().equals(parentId))
//2.通过方法设置子菜单
.map(menuVo -> menuVo.setChildren(getChildrenMenu(menuVo, menuVos)))
.collect(Collectors.toList());
return menuTree;
}

/**
* 获取子菜单
* @param menuVo 需要设置子菜单的菜单
* @param menuVos 所有菜单列表
* @return
*/

private List<MenuVo> getChildrenMenu(MenuVo menuVo, List<MenuVo> menuVos) {
List<MenuVo> childrenList = menuVos.stream()
//1.寻找子菜单:子菜单id==父菜单id
.filter(menuVo1 -> menuVo1.getParentId().equals(menuVo.getId()))
//2.设置子菜单的子菜单,递归调用
.map(menuVo1 -> menuVo1.setChildren(getChildrenMenu(menuVo1, menuVos)))
.collect(Collectors.toList());
return childrenList;
}

OSS对象存储(七牛云)

​ 因为如果把图片视频等文件上传到自己的应用的Web服务器,在读取图片的时候会占用比较多的资源。影响应用服务器的性能。

​ 所以我们一般使用OSS(Object Storage Service对象存储服务)存储图片或视频。

①添加依赖

1
2
3
4
5
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.7.99]</version>
</dependency>

②配置

application.yml

1
2
3
4
oss:
accessKey: xxxx
secretKey: xxxx
bucket: sg-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


/**
* @Author Maple
* @Date 2023/2/6 17:57
* @Version 1.0
*/
@Service
@Data
//1.读取对应后缀的配置文件并注入到属性里
@ConfigurationProperties(prefix = "oss")
public class UploadServiceImpl implements IUploadService {

private String accessKey;
private String secretKey;
private String bucket;
@Override
public ResponseResult uploadImg(MultipartFile img) {

String url=ossUpload(img);
//上传文件
return ResponseResult.okResult(url);
}

private String ossUpload(MultipartFile imgFile){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.autoRegion());
cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本
UploadManager uploadManager = new UploadManager(cfg);
InputStream is=null;
try {
is=imgFile.getInputStream();
} catch (Exception e) {
throw new RuntimeException(e);
}
//工具类生成唯一文件名
String key = PathUtils.generateFilePath(imgFile.getOriginalFilename());
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response= uploadManager.put(is,key,upToken,null,null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {

}
}
return SystemConstants.FILE_UPLOAD_DOMAIN+key;
}
}

AOP日志记录

打印格式为

1
2
3
4
5
6
7
8
9
"=======Start====="
"URL :
"BusinessName :
"HTTP Method :
"Class Method :
"IP :
"Request Args :
"Response :
"=======End======="

1.自定义注解

自定义注解标识需要打印日志的方法

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemLog {
String businessName();
}

2.定义切面类

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
//注入spring,标识为切面类,引入日志
@Component
@Aspect
@Slf4j
public class LogAspect {

//1.定义切点,使用 @Pointcut 注解标记一个空方法,用于匹配所有被 @SystemLog 注解标记的方法。
//将@Pointcut注解放在空方法上的意义是为了定义一个可重用的切点表达式
@Pointcut("@annotation(com.me.annotation.SystemLog)")
public void pt(){

}

//2.标记为环绕通知方法,
//该方法在被切入的方法执行前后分别执行了 handleBefore() 和 handleAfter() 方法
@Around("pt()")
public Object printLog(ProceedingJoinPoint point) throws Throwable {
Object res;
try {
handleBefore(point);
res = point.proceed();
handleAfter(res);
} finally {
log.info("=======End=======" + System.lineSeparator());
}

return res;

}


/**
* 方法执行前的方法
* @param point
*/
private void handleBefore(ProceedingJoinPoint point) {
//这段代码是用来获取当前线程中的 HTTP 请求对象
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();

SystemLog systemLog=getSysLog(point);

log.info("=======Start=======");
// 打印请求 URL
log.info("URL : {}",request.getRequestURL());
// 打印描述信息
log.info("BusinessName : {}",systemLog.businessName() );
// 打印 Http method
log.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}", point.getSignature().getDeclaringTypeName(),((MethodSignature) point.getSignature()).getName());
// 打印请求的 IP
log.info("IP : {}",request.getRemoteHost());
// 打印请求入参
log.info("Request Args : {}", JSON.toJSONString(point.getArgs()));
}

/**
* 获取切点上注解的信息
* @param point
* @return
*/
private SystemLog getSysLog(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
SystemLog systemLog = signature.getMethod().getAnnotation(SystemLog.class);
return systemLog;
}

/**
* 方法执行后的方法
* @param res
*/
private void handleAfter(Object res) {
// 打印出参
log.info("Response : {}", JSON.toJSONString(res));
}
}

AOP+Redis限流

1.自定义注解

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
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter
{
/**
* 限流key
*/
String key() default "limiter-";

/**
* 限流时间,单位秒
*/
int time() default 60;

/**
* 限流次数
*/
int count() default 100;

/**
* 限流类型
*/
LimitType limitType() default LimitType.DEFAULT;

/**
* 限流后返回的文字
*/
String limitMsg() default "访问过于频繁,请稍候再试";
}

2.编写切面类

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
@Aspect
@Component
public class RateLimiterAspect {

private final static Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);

@Autowired
private StringRedisTemplate redisUtils;

@Pointcut("@annotation(com.me.annotation.RateLimiter)")
public void pt(){

}

@Before("pt()")
public void doBefore(JoinPoint point) throws Throwable {
RateLimiter rateLimiter=getRateLimiter(point);
int time = rateLimiter.time();
int count = rateLimiter.count();
long total = 1L;

String combineKey = getCombineKey(rateLimiter, point);
try {
if (redisUtils.hasKey(combineKey)) {
total = redisUtils.opsForValue().increment(combineKey);
//total = redisUtils.incr(combineKey,1); //请求进来,对应的key加1
if (total > count)
throw new SystemException(AppHttpCodeEnum.REQUEST_FAST);
} else {
redisUtils.opsForValue().set(combineKey, "1", time, TimeUnit.SECONDS);
//redisUtils.set(combineKey,1,time); //初始化key
}
} catch (SystemException e) {
throw e;
} catch (Exception e) {
throw e;
}
}

private RateLimiter getRateLimiter(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
RateLimiter rateLimiter = signature.getMethod().getAnnotation(RateLimiter.class);
return rateLimiter;
}

/**
* 获取限流key
*
* @param rateLimiter
* @param point
* @return
*/
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
if (rateLimiter.limitType() == LimitType.IP) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
stringBuffer.append(request.getRemoteHost()).append("-");
}
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
return stringBuffer.toString();
}

}

项目启动时预处理

​ 如果希望在SpringBoot应用启动时进行一些初始化操作可以选择使用CommandLineRunner来进行处理。

​ 我们只需要实现CommandLineRunner接口,并且把对应的bean注入容器。把相关初始化的代码重新到需要重新的方法中。

​ 这样就会在应用启动的时候执行对应的代码。

1
2
3
4
5
6
7
8
@Component
public class TestRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("程序初始化");
}
}

定时任务

​ 定时任务的实现方式有很多,比如XXL-Job等。但是其实核心功能和概念都是类似的,很多情况下只是调用的API不同而已。

​ 这里就先用SpringBoot为我们提供的定时任务的API来实现一个简单的定时任务

① 使用@EnableScheduling注解开启定时任务功能

​ 我们可以在配置类上加上@EnableScheduling

1
2
3
4
5
6
7
8
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
public class SanGengBlogApplication {
public static void main(String[] args) {
SpringApplication.run(SanGengBlogApplication.class,args);
}
}

② 确定定时任务执行代码,并配置任务执行时间

​ 使用@Scheduled注解标识需要定时执行的代码。注解的cron属性相当于是任务的执行时间。目前可以使用 0/5 * * * * ? 进行测试,代表从0秒开始,每隔5秒执行一次。

​ 注意:对应的bean要注入容器,否则不会生效。

1
2
3
4
5
6
7
8
9
10
@Component
public class TestJob {
//cron表达式
@Scheduled(cron = "0/5 * * * * ?")
public void testJob(){
//要执行的代码
System.out.println("定时任务执行了");
}
}

Swagger2

简介

  • Swagger 是一套基于 OpenAPI 规范构建的开源工具,可以帮助我们设计、构建、记录以及使用 Rest API。
  • 传统意义上的文档都是后端开发人员手动编写的,相信大家也都知道这种方式很难保证文档的及时性,这种文档久而久之也就会失去其参考意义,反而还会加大我们的沟通成本。
  • Swagger 给我们提供了一个全新的维护 API 文档的方式,下面我们就来了解一下它的优点:

1.代码变,文档变。只需要少量的注解,Swagger 就可以根据代码自动生成 API 文档,很好的保证了文档的时效性。
2.跨语言性,支持 40 多种语言。
3.Swagger UI 呈现出来的是一份可交互式的 API 文档,我们可以直接在文档页面尝试 API 的调用,省去了准备复杂的调用参数的过程。

快速入门

引入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>

启用Swagger2

​ 在启动类上或者配置类加 @EnableSwagger2 注解

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
@EnableSwagger2
public class SanGengBlogApplication {
public static void main(String[] args) {
SpringApplication.run(SanGengBlogApplication.class,args);
}
}

测试

​ 访问:http://localhost:7777/swagger-ui.html

注意其中localhost和7777要调整成实际项目的域名和端口号。

权限控制

见Security文章

RBAC

前端动态路由

easyExcel导出

待加功能

用户点赞,取消点赞实现,同一接口

//缓存在redis

//定时更新

3.点赞/取消点赞

1
POST /article/likeCount/id
1
2
3
4
5
6
pathvarable形式
{
"userId":"1",
"articleId":"1",
"like":1/0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public ResponseResult likeArticle(Long articleId, Integer like) {
RedisTemplate template = redisCache.redisTemplate;
Long userId = SecurityUtils.getLoginUser().getUser().getId();
String key="blog:like:"+articleId;
Boolean result = template.opsForSet().isMember(key, userId);
if (Boolean.FALSE.equals(result)){
template.opsForSet().add(key, userId);
return ResponseResult.okResult("点赞成功");
}else {
template.opsForSet().remove(key, userId);
return ResponseResult.errorResult(201,"取消点赞成功");
}
}

通过定时任务写入数据库