博客项目实战
# 博客项目实战
# 1.新建blog_parent项目
# 父工程依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
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
# 子工程依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<!-- 排除 默认使用的logback -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
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
# 配置
# application.properties
#server
server.port=8888
spring.application.name=dep_blog
#datasource
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.table-prefix=dep_
2
3
4
5
6
7
8
9
10
11
12
13
# 配置mybatis-plus分页插件拦截
package com.ep.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/***
* @author dep
* @version 1.0
*/
@Configuration
public class MybatisPlusConfig {
// 分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 配置跨域
package com.ep.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/***
* @author dep
* @version 1.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 设置启动类
package com.ep;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
/***
* @author dep
* @version 1.0
*/
@SpringBootApplication
public class BlogApp {
public static void main(String[] args) {
SpringApplication.run(BlogApp.class,args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 错误解决方案:
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-10-16 16:08:19.634 ERROR 2344 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
Process finished with exit code 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
修改注解为:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
# 2. 首页-文章列表
# 2.1 接口说明
接口url:/articles
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
page | int | 当前页数 |
pageSize | int | 每页显示的数量 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 1,
"title": "springboot介绍以及入门案例",
"summary": "通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。\r\n\r\n这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。",
"commentCounts": 2,
"viewCounts": 54,
"weight": 1,
"createDate": "2609-06-26 15:58",
"author": "12",
"body": null,
"tags": [
{
"id": 5,
"avatar": null,
"tagName": "444"
},
{
"id": 7,
"avatar": null,
"tagName": "22"
},
{
"id": 8,
"avatar": null,
"tagName": "11"
}
],
"categorys": null
},
{
"id": 9,
"title": "Vue.js 是什么",
"summary": "Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。",
"commentCounts": 0,
"viewCounts": 3,
"weight": 0,
"createDate": "2609-06-27 11:25",
"author": "12",
"body": null,
"tags": [
{
"id": 7,
"avatar": null,
"tagName": "22"
}
],
"categorys": null
},
{
"id": 10,
"title": "Element相关",
"summary": "本节将介绍如何在项目中使用 Element。",
"commentCounts": 0,
"viewCounts": 3,
"weight": 0,
"createDate": "2609-06-27 11:25",
"author": "12",
"body": null,
"tags": [
{
"id": 5,
"avatar": null,
"tagName": "444"
},
{
"id": 6,
"avatar": null,
"tagName": "33"
},
{
"id": 7,
"avatar": null,
"tagName": "22"
},
{
"id": 8,
"avatar": null,
"tagName": "11"
}
],
"categorys": null
}
]
}
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
# 2.2 编码
# 2.2.1 表结构
CREATE TABLE `blog`.`dep_article` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`comment_counts` int(0) NULL DEFAULT NULL COMMENT '评论数量',
`create_date` bigint(0) NULL DEFAULT NULL COMMENT '创建时间',
`summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
`title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',
`view_counts` int(0) NULL DEFAULT NULL COMMENT '浏览数量',
`weight` int(0) NOT NULL COMMENT '是否置顶',
`author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id',
`body_id` bigint(0) NULL DEFAULT NULL COMMENT '内容id',
`category_id` int(0) NULL DEFAULT NULL COMMENT '类别id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `blog`.`dep_tag` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`article_id` bigint(0) NOT NULL,
`tag_id` bigint(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `article_id`(`article_id`) USING BTREE,
INDEX `tag_id`(`tag_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2
3
4
5
6
7
8
CREATE TABLE `blog`.`dep_sys_user` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号',
`admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理员',
`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
`create_date` bigint(0) NULL DEFAULT NULL COMMENT '注册时间',
`deleted` bit(1) NULL DEFAULT NULL COMMENT '是否删除',
`email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登录时间',
`mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密盐',
`status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.2.2 对应实体类
dao.pojo
package com.ep.dao.pojo;
import lombok.Data;
/***
* @author dep
* @version 1.0
*/
@Data
public class Article {
public static final int Article_TOP = 1;
public static final int Article_Common = 0;
private Long id;
private String title;
private String summary;
private int commentCounts;
private int viewCounts;
/**
* 作者id
*/
private Long authorId;
/**
* 内容id
*/
private Long bodyId;
/**
*类别id
*/
private Long categoryId;
/**
* 置顶
*/
private int weight = Article_Common;
/**
* 创建时间
*/
private Long createDate;
}
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
package com.ep.dao.pojo;
import lombok.Data;
/***
* @author dep
* @version 1.0
*/
@Data
public class SysUser {
private Long id;
private String account;
private Integer admin;
private String avatar;
private Long createDate;
private Integer deleted;
private String email;
private Long lastLogin;
private String mobilePhoneNumber;
private String nickname;
private String password;
private String salt;
private String status;
}
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
package com.ep.dao.pojo;
import lombok.Data;
/***
* @author dep
* @version 1.0
*/
@Data
public class Tag {
private Long id;
private String avatar;
private String tagName;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.2.3 对应mapper
dao.mapper
package com.ep.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ep.dao.pojo.Article;
/***
* @author dep
* @version 1.0
*/
public interface ArticleMapper extends BaseMapper<Article> {
}
2
3
4
5
6
7
8
9
10
11
12
public interface SysUserMapper extends BaseMapper<SysUser> {
}
public interface TagMapper extends BaseMapper<Tag> {
}
2
3
4
# 2.2.4 返回数据
返回格式
dao.vo
package com.ep.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/***
* @author dep
* @version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private Boolean success;
private Integer code;
private String msg;
private Object Data;
/**
* 成功返回格式
* @param data
* @return
*/
public static Result success(Object data) {
return new Result(true,200,"success",data);
}
/***
* 失败返回格式
* @param code
* @param msg
* @return
*/
public static Result fail(Integer code, String msg) {
return new Result(false,code,msg,null);
}
}
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
封装文章实体返回给前端
package com.ep.vo;
import lombok.Data;
/***
* @author dep
* @version 1.0
* 返回文章实体
*/
@Data
public class ArticleVo {
private Long id;
private String title;
private String summary;
private int commentCounts;
private int viewCounts;
private int weight;
/**
* 创建时间
*/
private String createDate;
private String author;
// private ArticleBodyVo body;
//
// private List<TagVo> tags;
//
// private List<CategoryVo> categorys;
}
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
vo.params.PageParams
用于封装前端发过来的参数
@Data
public class PageParams {
private Integer page = 1; // 当前页数
private Integer pageSize = 10; // 每页显示的数量
}
2
3
4
5
6
# 2.2.5 service
# 3. 统一异常处理
不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。
handle.AllExceptionHandler
package com.ep.handle;
import com.ep.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/***
* @author dep
* @version 1.0
* 对加了@Controller注解的方法进行拦截处理 AOP的实现
*/
@ControllerAdvice
// @RestControllerAdvice 加这个注释就不需要在异常处理方法上加@ResponseBody
public class AllExceptionHandler {
// 进行异常处理,处理Exception.class异常
@ExceptionHandler(Exception.class)
@ResponseBody // 返回json数据
public Result doException(Exception ex) {
return Result.fail(-999,"系统异常");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 4. 登录
# 4.1 接口说明
接口url:/login
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
account | string | 账号 |
password | string | 密码 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
2
3
4
5
6
# 4.2 JWT
登录使用JWT技术。
jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。
jwt 有三部分组成:A.B.C
A:Header,{"type":"JWT","alg":"HS256"} 固定
B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。
jwt 验证,主要就是验证C部分 是否合法。
依赖包:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2
3
4
5
工具类:
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtils {
private static final String jwtToken = "123456Mszlu!@#$$";
public static String createToken(Long userId){
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
public static Map<String, Object> checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
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
错误状态码
package com.ep.vo;
/***
* @author dep
* @version 1.0
*/
public enum ErrorCode {
PARAMS_ERROR(10001,"参数有误"),
ACCOUNT_PWD_NOT_EXIST(10002,"用户名或密码不存在"),
NO_PERMISSION(70001,"无访问权限"),
SESSION_TIME_OUT(90001,"会话超时"),
NO_LOGIN(90002,"未登录"),;
private int code;
private String msg;
ErrorCode(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
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
# 4.3. 登录拦截器
每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
# 4.3.1 拦截器实现
handle --> LoginInterceptor
package com.ep.handle;
import com.alibaba.fastjson.JSON;
import com.ep.dao.pojo.SysUser;
import com.ep.service.LoginService;
import com.ep.vo.ErrorCode;
import com.ep.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/***
* @author dep
* @version 1.0
*/
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在执行controller方法(Handler)之前进行执行
/**
* 1.需要判断请求的接口路径是否为 HandlerMethod (controller方法)
* 2.判断token是否为空,如果为空未登录
* 3. 如果token不为空,登录验证 loginService checkToken
* 4.如果认证成功放行即可
*/
if( !(handler instanceof HandlerMethod)) {
//handler 可能是RequestResourceHandler
// springboot程序 访问静态资源 默认去classpath下的static目录去查询
return true;
}
String token = request.getHeader("Authorization");
log.info("=================request start===========================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}", token);
log.info("=================request end===========================");
// token 为空
if(StringUtils.isBlank(token)) {
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null) {
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
// 登陆成功,放行
return true;
}
}
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
# 4.3.2 使拦截器生效
package com.ep.config;
import com.ep.handle.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/***
* @author dep
* @version 1.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截test接口,后续实际遇到需要拦截的接口时,再配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test");
}
}
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
# 5.ThreadLocal保存用户信息
com.ep.utils.UserThreadLocal
package com.ep.utils;
import com.ep.dao.pojo.SysUser;
/***
* @author dep
* @version 1.0
*/
public class UserThreadLocal {
private UserThreadLocal(){}
//线程变量隔离
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
public static void put(SysUser sysUser) {
LOCAL.set(sysUser);
}
public static SysUser get() {
return LOCAL.get();
}
public static void remove() {
LOCAL.remove();
}
}
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
com.ep.LoginInterceptor
/***
* @author dep
* @version 1.0
*/
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在执行controller方法(Handler)之前进行执行
/**
* 1.需要判断请求的接口路径是否为 HandlerMethod (controller方法)
* 2.判断token是否为空,如果为空未登录
* 3. 如果token不为空,登录验证 loginService checkToken
* 4.如果认证成功放行即可
*/
if( !(handler instanceof HandlerMethod)) {
//handler 可能是RequestResourceHandler
// springboot程序 访问静态资源 默认去classpath下的static目录去查询
return true;
}
String token = request.getHeader("Authorization");
log.info("=================request start===========================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}", token);
log.info("=================request end===========================");
// token 为空
if(StringUtils.isBlank(token)) {
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null) {
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
// 登陆成功,放行
// 我希望在controller中 直接获取用户的信息
UserThreadLocal.put(sysUser);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 如果不删除 ThreadLocal中用完的信息 ,会有内存遗漏的风险
UserThreadLocal.remove();
}
}
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
使用:
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping
public Result test() {
SysUser sysUser = UserThreadLocal.get();
System.out.println(sysUser);
return Result.success("成功");
}
}
2
3
4
5
6
7
8
9
10
11
# 5.1. ThreadLocal内存泄漏
实线代表强引用,虚线代表弱引用
每一个Thread维护一个ThreadLocalMap, key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
# 6.使用线程池 更新阅读次数
config.ThreadPoolConfig
package com.ep.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/***
* @author dep
* @version 1.0
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(20);
//配置队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("爱写bug的小邓程序员博客项目");
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}
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.ep.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ep.dao.mapper.ArticleMapper;
import com.ep.dao.pojo.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/***
* @author dep
* @version 1.0
*/
@Component
public class ThreadService {
// 期望此类操作在线程池执行 不会影响原有的主线程
@Async("taskExecutor")
public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
int viewCounts = article.getViewCounts();
Article articleUpdate = new Article();
articleUpdate.setViewCounts(viewCounts + 1);
LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Article::getId, article.getId());
// 设置一个 为了在线程安全的环境下 线程安全
// 相当于乐观锁吧
updateWrapper.eq(Article::getViewCounts, viewCounts);
// update article set view_count=100 where view_count = 99 and id = 11;
articleMapper.update(articleUpdate, updateWrapper);
try {
Thread.sleep(5000);
// System.out.println("更新完成了....");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
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
# 7.AOP日志
package com.ep.common.aop;
import java.lang.annotation.*;
/***
* @author dep
* @version 1.0
* 日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
String module() default "";
String operation() default "";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.ep.common.aop;
import com.alibaba.fastjson.JSON;
import com.ep.utils.HttpContextUtils;
import com.ep.utils.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* @author dep
* @version 1.0
* 日志切面
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.ep.common.aop.LogAnnotation)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
recordLog(point, time);
return result;
}
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}",logAnnotation.module());
log.info("operation:{}",logAnnotation.operation());
//请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.info("request method:{}",className + "." + methodName + "()");
// //请求的参数
Object[] args = joinPoint.getArgs();
String params = JSON.toJSONString(args[0]);
log.info("params:{}",params);
//获取request 设置IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
log.info("ip:{}", IpUtils.getIpAddr(request));
log.info("excute time : {} ms",time);
log.info("=====================log end================================");
}
}
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
HttpContextUtils
package com.ep.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/***
* @author dep
* @version 1.0
*/
public class HttpContextUtils {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
IPUtils
package com.ep.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 获取Ip
*
*/
@Slf4j
public class IpUtils {
/**
* 获取IP地址
* <p>
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null, unknown = "unknown", seperator = ",";
int maxLength = 15;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
log.error("IpUtils ERROR ", e);
}
// 使用代理,则获取第一个IP地址
if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {
int idx = ip.indexOf(seperator);
if (idx > 0) {
ip = ip.substring(0, idx);
}
}
return ip;
}
/**
* 获取ip地址
*
* @return
*/
public static String getIpAddr() {
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
return getIpAddr(request);
}
}
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
# 8.腾讯云文件上传
https://cloud.tencent.com/document/product/436/10199
导入依赖:
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.89</version>
</dependency>
2
3
4
5
ContextUtils 用于获取配置文件内容
package com.ep.utils;
import com.sun.istack.internal.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* 环境 工具类
* 帮助(未被Spring托管的对象)获取Spring环境中的Bean和Property
*/
@Component
public class ContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
ContextUtils.applicationContext = applicationContext;
}
/**
* 获取Bean
*/
public static <T> T getBean(Class<T> requiredType){
return applicationContext.getBean(requiredType);
}
/**
* 获取Bean
*/
public static Object getBean(String name){
return applicationContext.getBean(name);
}
/**
* 获取属性
* @param key 属性键
* @return 属性值
*/
public static String getProperty(String key) {
Environment environment = applicationContext.getEnvironment();
String property = environment.getProperty(key);
Assert.notNull(property, "属性不存在");
return property;
}
}
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.ep.utils;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.exception.CosServiceException;
import com.qcloud.cos.model.*;
import com.qcloud.cos.model.DeleteObjectsRequest.KeyVersion;
import com.qcloud.cos.region.Region;
import com.qcloud.cos.transfer.Download;
import com.qcloud.cos.transfer.TransferManager;
import com.qcloud.cos.transfer.TransferManagerConfiguration;
import com.qcloud.cos.transfer.Upload;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 腾讯云对象存储工具类
*/
@Component
public class TencentCosUtils {
static {
secretId = ContextUtils.getProperty("tencent.cos.secretId");
secretKey = ContextUtils.getProperty("tencent.cos.secretKey");
region = ContextUtils.getProperty("tencent.cos.region");
baseUrl = ContextUtils.getProperty("tencent.cos.baseUrl");
bucket = ContextUtils.getProperty("tencent.cos.bucket");
}
//腾讯云的SecretId
private static String secretId;
//腾讯云的SecretKey
private static String secretKey;
//腾讯云的bucket (存储桶)
private static String bucket;
//腾讯云的region(bucket所在地区)
private static String region;
//腾讯云的访问基础链接:
private static String baseUrl;
/**
* 获取cos客户端
*
* @return COSClient
*/
private static COSClient getCOSClient() {
// 1 初始化用户身份信息(secretId, secretKey)
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
// 2 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
ClientConfig clientconfig = new ClientConfig(new Region(region));
// 3 生成cos客户端并且返回
return new COSClient(cred, clientconfig);
}
/**
* 获取文件后缀
* @param fileName
* @return
*/
static String getExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
/**
* 文件以流形式下载
*
* @param fileURL 文件链接
* @param response
*/
public static void downloadFile(String fileURL, HttpServletResponse response) {
// 生成cos客户端
COSClient cosclient = getCOSClient();
if (!cosclient.doesObjectExist(bucket, getKeyByFileURL(fileURL))) {
throw new RuntimeException("cos文件不存在");
}
// 获取下载输入流
GetObjectRequest getObjectRequest = new GetObjectRequest(bucket, getKeyByFileURL(fileURL));
// 限流使用的单位是 bit/s, 这里设置下载带宽限制为10MB/s
// getObjectRequest.setTrafficLimit(80*1024*1024);
COSObject cosObject = cosclient.getObject(getObjectRequest);
COSObjectInputStream cosObjectInput = cosObject.getObjectContent();
//文件下载设置
response.reset();
response.setCharacterEncoding("utf-8");
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", cosObject.getObjectMetadata().getContentDisposition().replace("inline", "attachment"));
try {
// 读取文件内容。
OutputStream out = response.getOutputStream();
BufferedInputStream br = new BufferedInputStream(cosObjectInput);
byte[] buf = new byte[2048];
int len = 0;
while ((len = br.read(buf)) > 0)
out.write(buf, 0, len);
out.close();
br.close();
// 关闭输入流
cosObjectInput.close();
} catch (IOException e) {
throw new RuntimeException("cos文件下载失败");
} finally {
cosclient.shutdown();
}
}
// 如果要获取超过maxkey数量的object或者获取所有的object, 则需要循环调用listobject, 用上一次返回的next marker作为下一次调用的marker,
// 直到返回的truncated为false
/**
* 获取指定文件夹下的文件列表
*
* @param fileDir
* @return
*/
public static List<String> getListByDirPath(String fileDir) {
//处理文件夹路径前无/后加/
fileDir = getCorrectFolderPath(fileDir);
// 生成cos客户端
COSClient cosclient = getCOSClient();
ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
// 设置bucket名称
listObjectsRequest.setBucketName(bucket);
// prefix表示列出的object的key以prefix开始
listObjectsRequest.setPrefix(fileDir);
// deliter表示分隔符, 设置为/表示列出当前目录下的object, 设置为空表示列出所有的object
listObjectsRequest.setDelimiter("/");
// 设置最大遍历出多少个对象, 一次listobject最大支持1000
listObjectsRequest.setMaxKeys(1000);
ObjectListing objectListing = null;
List<String> resultUrls = new ArrayList<String>();
do {
try {
objectListing = cosclient.listObjects(listObjectsRequest);
} catch (CosServiceException e) {
e.printStackTrace();
return resultUrls;
} catch (CosClientException e) {
e.printStackTrace();
return resultUrls;
}
// common prefix表示表示被delimiter截断的路径, 如delimter设置为/, common prefix则表示所有子目录的路径
// List<String> commonPrefixs = objectListing.getCommonPrefixes();
// object summary表示所有列出的object列表
List<COSObjectSummary> cosObjectSummaries = objectListing.getObjectSummaries();
for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
// 文件的路径key
String key = cosObjectSummary.getKey();
// 文件的etag
String etag = cosObjectSummary.getETag();
// 文件的长度
long fileSize = cosObjectSummary.getSize();
// 文件的存储类型
String storageClasses = cosObjectSummary.getStorageClass();
cosObjectSummary.getLastModified();
System.out.println("key: " + key + " etag: " + etag + " fileSize: " + fileSize + " storageClasses: " + storageClasses);
resultUrls.add(baseUrl + key);
}
String nextMarker = objectListing.getNextMarker();
listObjectsRequest.setMarker(nextMarker);
} while (objectListing.isTruncated());
cosclient.shutdown();
return resultUrls;
}
/**
* 删除单个文件(不带版本号, 即bucket未开启多版本)
*
* @param fileUrl
*/
public static void DelSingleFile(String fileUrl) {
//生成cos客户端
COSClient cosclient = getCOSClient();
try {
if (cosclient.doesObjectExist(bucket, getKeyByFileURL(fileUrl))) {
cosclient.deleteObject(bucket, getKeyByFileURL(fileUrl));
}
} catch (CosServiceException e) { // 如果是其他错误, 比如参数错误, 身份验证不过等会抛出CosServiceException
e.printStackTrace();
} catch (CosClientException e) { // 如果是客户端错误,比如连接不上COS
e.printStackTrace();
} finally {
// 关闭客户端
cosclient.shutdown();
}
}
/**
* 批量删除文件(不带版本号, 即bucket未开启多版本)
*
* @param fileUrlList
*/
public static void batchDelFile(List<String> fileUrlList) {
// 生成cos客户端
COSClient cosclient = getCOSClient();
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucket);
// 设置要删除的key列表, 最多一次删除1000个
ArrayList<KeyVersion> keyList = new ArrayList<>();
// 传入要删除的文件名
for (String fileUrl : fileUrlList) {
keyList.add(new KeyVersion(getKeyByFileURL(fileUrl)));
}
deleteObjectsRequest.setKeys(keyList);
// 批量删除文件
try {
cosclient.deleteObjects(deleteObjectsRequest);
} catch (CosClientException e) { // 如果是客户端错误,比如连接不上COS
e.printStackTrace();
} finally {
// 关闭客户端
cosclient.shutdown();
}
}
/**
* 文件上传
*
* @param file
* @param fileDir
* @return
*/
public static String uploadFile(MultipartFile file, String fileDir) {
// 生成cos客户端
COSClient cosclient = getCOSClient();
fileDir = getCorrectFolderPath(fileDir);
String fileName = String.format("%s.%s", UUID.randomUUID().toString().replace("-", ""), getExtension(file.getOriginalFilename()));
try (InputStream inputStream = file.getInputStream()) {
// 从输入流上传(需提前告知输入流的长度, 否则可能导致 oom)
ObjectMetadata objectMetadata = new ObjectMetadata();
// 设置输入流长度
objectMetadata.setContentLength(inputStream.available());
// 设置 Content type, 默认是 application/octet-stream
objectMetadata.setContentType(getContentType(getExtension(file.getOriginalFilename())));
// 设置不缓存
objectMetadata.setCacheControl("no-cache");
objectMetadata.setHeader("Pragma", "no-cache");
// inline在线预览,中文乱码已处理,下载文件的时候可以用原来上传的名字
objectMetadata.setContentDisposition("inline;filename=" + URLEncoder.encode(file.getOriginalFilename(), "utf-8"));
cosclient.putObject(bucket, fileDir + fileName, inputStream, objectMetadata);
} catch (IOException e) {
e.printStackTrace();
}
// 关闭客户端
cosclient.shutdown();
return baseUrl + "/" + fileDir + fileName;
}
/**
* 文件批量上传
*
* @param files
* @param fileDir
* @return
*/
public static String batchUploadFile(MultipartFile[] files, String fileDir) {
//处理文件夹路径前无/后加/
fileDir = getCorrectFolderPath(fileDir);
// 生成cos客户端
COSClient cosclient = getCOSClient();
String resultUrls = "";
try {
for (MultipartFile file : files) {
String fileName = String.format("%s.%s", UUID.randomUUID().toString().replace("-", ""), getExtension(file.getOriginalFilename()));
InputStream inputStream = file.getInputStream();
// 从输入流上传(需提前告知输入流的长度, 否则可能导致 oom)
ObjectMetadata objectMetadata = new ObjectMetadata();
// 设置输入流长度为500
objectMetadata.setContentLength(inputStream.available());
// 设置 Content type, 默认是 application/octet-stream
objectMetadata.setContentType(getContentType(getExtension(file.getOriginalFilename())));
// 设置不缓存
objectMetadata.setCacheControl("no-cache");
objectMetadata.setHeader("Pragma", "no-cache");
// inline在线预览,中文乱码已处理,下载文件的时候可以用原来上传的名字
objectMetadata.setContentDisposition("inline;filename=" + URLEncoder.encode(file.getOriginalFilename(), "utf-8"));
cosclient.putObject(bucket, fileDir + fileName, inputStream, objectMetadata);
resultUrls = resultUrls + "," + baseUrl + fileDir + fileName;
}
} catch (IOException e) {
e.printStackTrace();
}
// 关闭客户端
cosclient.shutdown();
return resultUrls.substring(1);
}
public static TransferManager cosTransferManager() {
// 线程池大小,建议在客户端与 COS 网络充足(例如使用腾讯云的 CVM,同地域上传 COS)的情况下,设置成16或32即可,可较充分的利用网络资源
// 对于使用公网传输且网络带宽质量不高的情况,建议减小该值,避免因网速过慢,造成请求超时。
ExecutorService threadPool = Executors.newFixedThreadPool(32);
// 传入一个 threadpool, 若不传入线程池,默认 TransferManager 中会生成一个单线程的线程池。
TransferManager transferManager = new TransferManager(getCOSClient(), threadPool);
// 设置高级接口的分块上传阈值和分块大小为10MB
TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration();
transferManagerConfiguration.setMultipartUploadThreshold(10 * 1024 * 1024);
transferManagerConfiguration.setMinimumUploadPartSize(10 * 1024 * 1024);
transferManager.setConfiguration(transferManagerConfiguration);
return transferManager;
}
public static Upload upload(String filePath, MultipartFile file) {
TransferManager manager = cosTransferManager();
Upload result = null;
try (InputStream inputStream = file.getInputStream()) {
result = manager.upload(bucket, filePath, inputStream, new ObjectMetadata());
result.waitForUploadResult();
} catch (CosClientException | InterruptedException | IOException e) {
e.printStackTrace();
} finally {
manager.shutdownNow();
}
return result;
}
public static Download download(String filePath, File destFile) {
TransferManager manager = cosTransferManager();
Download download = null;
try {
download = manager.download(bucket, filePath, destFile);
download.waitForCompletion();
} catch (CosClientException | InterruptedException e) {
e.printStackTrace();
} finally {
manager.shutdownNow();
}
return download;
}
/**
* 获取对象键
* 官方文档:腾讯云对象存储 COS 中的对象需具有合法的对象键,对象键(ObjectKey)是对象在存储桶中的唯一标识。
* 例如:在对象的访问地址examplebucket-1250000000.cos.ap-guangzhou.myqcloud.com/folder/picture.jpg 中,对象键为folder/picture.jpg。
*
* @param fileUrl
* @return
*/
private static String getKeyByFileURL(String fileUrl) {
//获取完整路径中的文件名,截去"http://oss-cn-shenzhen.aliyuncs.com/"剩下的就是文件名
if (fileUrl.indexOf("http") > -1) {
return fileUrl.substring(fileUrl.indexOf("/", 9) + 1);
}
return fileUrl;
}
/**
* 处理正确文件夹路径(前无/后加/)
* 例如:/upload/image -> upload/image/
*
* @param fileDir
* @return
*/
private static String getCorrectFolderPath(String fileDir) {
//处理文件夹路径前无/后加/
if (StringUtils.isEmpty(fileDir)) {
fileDir = "";
} else {
fileDir = (fileDir.indexOf("/") == 0) ? fileDir.substring(1) : fileDir;
fileDir = (fileDir.lastIndexOf("/") == fileDir.length() - 1) ? fileDir : fileDir + "/";
}
return fileDir;
}
/**
* 判断COS服务文件上传时文件的contentType
*
* @param FilenameExtension 文件后缀
* @return ContentType
*/
private static String getContentType(String FilenameExtension) {
//image/jpg 可以在线预览
if (FilenameExtension.equalsIgnoreCase("gif")
|| FilenameExtension.equalsIgnoreCase("jpeg")
|| FilenameExtension.equalsIgnoreCase("jpg")
|| FilenameExtension.equalsIgnoreCase("png")) {
return "image/jpg";
}
if (FilenameExtension.equalsIgnoreCase("bmp")) {
return "image/bmp";
}
if (FilenameExtension.equalsIgnoreCase("html")) {
return "text/html";
}
if (FilenameExtension.equalsIgnoreCase("txt")) {
return "text/plain";
}
if (FilenameExtension.equalsIgnoreCase("vsd")) {
return "application/vnd.visio";
}
if (FilenameExtension.equalsIgnoreCase("pptx") ||
FilenameExtension.equalsIgnoreCase("ppt")) {
return "application/vnd.ms-powerpoint";
}
if (FilenameExtension.equalsIgnoreCase("docx") ||
FilenameExtension.equalsIgnoreCase("doc")) {
return "application/msword";
}
if (FilenameExtension.equalsIgnoreCase("pdf")) {
return "application/pdf";
}
if (FilenameExtension.equalsIgnoreCase("ppt")) {
return "application/x-ppt";
}
if (FilenameExtension.equalsIgnoreCase("xml")) {
return "text/xml";
}
if (FilenameExtension.equalsIgnoreCase("mp3")) {
return "audio/mp3";
}
if (FilenameExtension.equalsIgnoreCase("mp4")) {
return "video/mp4";
}
if (FilenameExtension.equalsIgnoreCase("avi")) {
return "video/avi";
}
if (FilenameExtension.equalsIgnoreCase("wmv")) {
return "video/x-ms-wmv";
}
return "image/jpg";
}
}
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
使用:
/***
* @author dep
* @version 1.0
* 文件上传
*/
@RestController
@RequestMapping("upload")
public class UploadController {
@PostMapping
public Result upload(@RequestParam("image") MultipartFile file) {
String filePath = TencentCosUtils.uploadFile(file, "blog");
return Result.success(filePath);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 10. 统一缓存处理(优化)
内存的访问速度 远远大于 磁盘的访问速度 (1000倍起)
package com.ep.common.cache;
import java.lang.annotation.*;
/***
* @author dep
* @version 1.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
long expire() default 1 * 60 * 1000;
String name() default "";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.ep.common.cache;
import com.alibaba.fastjson.JSON;
import com.ep.vo.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Duration;
/***
* @author dep
* @version 1.0
* 统一缓存处理
*/
@Aspect
@Component
@Slf4j
public class CacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Pointcut("@annotation(com.ep.common.cache.Cache)")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
try {
Signature signature = pjp.getSignature();
//类名
String className = pjp.getTarget().getClass().getSimpleName();
//调用的方法名
String methodName = signature.getName();
Class[] parameterTypes = new Class[pjp.getArgs().length];
Object[] args = pjp.getArgs();
//参数
String params = "";
for(int i=0; i<args.length; i++) {
if(args[i] != null) {
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
}else {
parameterTypes[i] = null;
}
}
if (StringUtils.isNotEmpty(params)) {
//加密 以防出现key过长以及字符转义获取不到的情况
params = DigestUtils.md5Hex(params);
}
Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//获取Cache注解
Cache annotation = method.getAnnotation(Cache.class);
//缓存过期时间
long expire = annotation.expire();
//缓存名称
String name = annotation.name();
//先从redis获取
String redisKey = name + "::" + className+"::"+methodName+"::"+params;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)){
log.info("走了缓存~~~,{},{}",className,methodName);
return JSON.parseObject(redisValue, Result.class);
}
Object proceed = pjp.proceed();
// redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
redisTemplate.opsForValue().set(redisKey,new ObjectMapper().writeValueAsString(proceed), Duration.ofMillis(expire));
log.info("存入缓存~~~ {},{}",className,methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999,"系统错误");
}
}
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
使用:
/***
* 首页 文章列表
* @param pageParams
* @return
*/
@ApiOperation("获取首页文章列表")
@PostMapping
@Cache(expire = 5 * 60 * 1000,name = "articles")
public Result listArticle(@RequestBody PageParams pageParams) {
return articleService.listArticle(pageParams);
}
2
3
4
5
6
7
8
9
10
11
12
# 11. Security集成
# 11..1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2
3
4
config.SecurityConfig
package com.ep.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/***
* @author dep
* @version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
//加密策略 MD5 不安全 彩虹表 MD5 加盐
String dep = new BCryptPasswordEncoder().encode("dep");
System.out.println(dep);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启登录认证
// .antMatchers("/user/findAll").hasRole("admin") //访问接口需要admin的角色
.antMatchers("/css/**").permitAll()
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/plugins/**").permitAll()
.antMatchers("/admin/**").access("@authService.auth(request,authentication)") //自定义service 来去实现实时的权限认证
.antMatchers("/pages/**").authenticated()
.and().formLogin()
.loginPage("/login.html") //自定义的登录页面
.loginProcessingUrl("/login") //登录处理接口
.usernameParameter("username") //定义登录时的用户名的key 默认为username
.passwordParameter("password") //定义登录时的密码key,默认是password
.defaultSuccessUrl("/pages/main.html")
.failureUrl("/login.html")
.permitAll() //通过 不拦截,更加前面配的路径决定,这是指和登录表单相关的接口 都通过
.and().logout() //退出登录配置
.logoutUrl("/logout") //退出登录接口
.logoutSuccessUrl("/login.html")
.permitAll() //退出登录的接口放行
.and()
.httpBasic()
.and()
.csrf().disable() //csrf关闭 如果自定义登录 需要关闭
.headers().frameOptions().sameOrigin();
}
}
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
AuthService
package com.ep.service;
import com.ep.pojo.Admin;
import com.ep.pojo.Permission;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/***
* @author dep
* @version 1.0
*/
@Service
@Slf4j
public class AuthService {
@Autowired
private AdminService adminService;
public boolean auth(HttpServletRequest request, Authentication authentication){
String requestURI = request.getRequestURI();
log.info("request url:{}", requestURI);
//true代表放行 false 代表拦截
Object principal = authentication.getPrincipal();
if (principal == null || "anonymousUser".equals(principal)){
//未登录
return false;
}
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername();
Admin admin = adminService.findAdminByUserName(username);
if (admin == null){
return false;
}
if (admin.getId() == 1){
//认为是超级管理员
return true;
}
List<Permission> permissions = adminService.findPermissionsByAdminId(admin.getId());
requestURI = StringUtils.split(requestURI,'?')[0];
for (Permission permission : permissions) {
if (requestURI.equals(permission.getPath())){
log.info("权限通过");
return true;
}
}
return false;
}
}
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
SecurityUserService
package com.ep.service;
import com.ep.pojo.Admin;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/***
* @author dep
* @version 1.0
*/
@Component
@Slf4j
public class SecurityUserService implements UserDetailsService {
@Autowired
private AdminService adminService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username:{}",username);
//当用户登录的时候,springSecurity 就会将请求 转发到此
//根据用户名 查找用户,不存在 抛出异常,存在 将用户名,密码,授权列表 组装成springSecurity的User对象 并返回
Admin adminUser = adminService.findAdminByUserName(username);
if(adminUser == null) {
throw new UsernameNotFoundException("用户名不存在");
}
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new User(username, adminUser.getPassword(), authorities);
//剩下的认证 就由框架帮我们完成
return userDetails;
}
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("123456"));
}
}
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
# 12.Swagger集成
- 导入依赖
<!--swagger3-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
2
3
4
5
6
swagger配置
package com.ep.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
/***
* @author dep
* @version 1.0
* Swagger配置
*/
@Configuration
@EnableOpenApi
public class SwaggerConfig {
@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
return new Docket(DocumentationType.OAS_30) // 指定swagger3.0版本
.apiInfo(apiInfo());
}
//配置文档信息
private ApiInfo apiInfo() {
Contact contact = new Contact("爱写bug的小邓程序员", "http://xxx.xxx.com/联系人访问链接", "1649743146@qq.com");
return new ApiInfo(
"爱写bug的小邓程序员", // 标题
"爱写bug的小邓程序员博客后台接口", // 描述
"v1.0", // 版本
"http://terms.service.url/组织链接", // 组织链接
contact, // 联系人信息
"Apach 2.0 许可", // 许可
"许可链接", // 许可连接
new ArrayList<>()// 扩展
);
}
}
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