文章目录
- Springboot+Vue博客项目总结
- 1.工程搭建
- 1.1 新建maven工程
- 1.2 application.properties配置
- 1.3 配置分页插件
- 1.4 配置解决跨域
- 1.5 添加启动类
- 2.统一异常处理
- 3.登录功能实现
- 3.1 接口说明
- 3.2 JWT
- 3.3 Controller
- 3.4 Service
- 3.5 登录参数,redis配置
- 5.获取用户信息
- 5.1 接口说明
- 5.2 Controller
- 5.3 Service
- 6.登录拦截器
- 6.1 拦截器实现
- 6.2 使拦截器生效
- 7.ThreadLocal保存用户信息
- 8. 使用线程池更新阅读次数
- 8.1 线程池配置
- 8.2 使用
- 9.评论
- 9.1 接口说明
- 9.2 加入到登录拦截器中
- 9.3 Controller
- 9.4 Service
- 10.AOP统一记录日志
- 11.文章图片上传
- 11.1 接口说明
- 11.2 Controller
- 11.3 使用七牛云
- 12.AOP实习统一缓存处理(优化)
- 13.年月归档中MySQL查询
- 13.1 Controller
- 13.2 Service
- 13.3 具体sql实现
- 14.对后端进行返回统一的标准格式
- 14.1 定义返回对象
- 14.2 定义状态码
- 15.项目亮点总结
Springboot+Vue博客项目总结
1.工程搭建
1.1 新建maven工程
<?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.mszlu</groupId>
<artifactId>blog-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2 application.properties配置
#server
server.port= 8888
spring.application.name=mszlu_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/blogxpp?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
#定义前缀表名,因为数据库中的表带ms_。这样实体类的表不用加前缀就可以匹配
mybatis-plus.global-config.db-config.table-prefix=ms_
1.3 配置分页插件
不知道的可以查看MyBatis-Plus官网关于分页插件的介绍
@Configuration
//扫包,将此包下的接口生成代理实现类,并且注册到spring容器中
@MapperScan("com.xpp.blog.dao.mapper")
public class MybatisPlusConfig {
//集成分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
1.4 配置解决跨域
解决跨域问题可以参考:SpringBoot解决跨域的5种方式
前后端端口不一样,需要解决跨域问题。
这里解决的方法是重写WebMvcConfigurer
:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
//跨域配置,前端和后端端口不一样
@Override
public void addCorsMappings(CorsRegistry registry) {
//8080前端使用的端口号
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
}
1.5 添加启动类
@SpringBootApplication
public class BlogApp {
public static void main(String[] args) {
SpringApplication.run(BlogApp.class,args);
}
}
2.统一异常处理
不管是controller
层还是service
,dao
层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。
@ControllerAdvice
:对加了@Controller
的方法进行拦截处理,AOP的实现@ExceptionHandler
:统一处理某一类异常,从而减少代码重复率和复杂度,比如要获取自定义异常可以@ExceptionHandler(BusinessException.class)
//作用:对加了@Controller的方法进行拦截处理,AOP的实现
@ControllerAdvice
public class AllExceptionHandler {
//进行一次处理,处理Exception.class的异常
@ExceptionHandler(Exception.class)
//返回json数据,不加的话直接返回页面
@ResponseBody
public Result doException(Exception e){
e.printStackTrace();
return Result.fail(-999,"系统异常");
}
}
3.登录功能实现
3.1 接口说明
接口url:/login
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
account | string | 账号 |
password | string | 密码 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
3.2 JWT
可以参考:JWT整合Springboot
登录使用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>
工具类:
public class JWTUtils {
//密钥
private static final String jwtToken = "123456Mszlu!@#$$";
//生成token
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;
}
//检查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;
}
public static void main(String[] args) {
String token=JWTUtils.createToken(100L);
System.out.println(token);
Map<String, Object> map = JWTUtils.checkToken(token);
System.out.println(map.get("userId"));
}
}
3.3 Controller
@RestController
@RequestMapping("login")
public class loginController {
@Autowired
private LoginService loginService;
@PostMapping
public Result login(@RequestBody LoginParam loginParam){
//登录->验证用户
return loginService.login(loginParam);
}
}
3.4 Service
关于这里StringUtils的用法:Java之StringUtils的常用方法
md5加密的依赖包:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
//加密盐
private static final String slat = "mszlu!@#";
@Override
public Result login(LoginParam loginParam) {
//1.检查参数是否合法
String account = loginParam.getAccount();
String password = loginParam.getPassword();
if (StringUtils.isBlank(account) || StringUtils.isAllBlank(password)) {
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());
}
//用md5加密
password = DigestUtils.md5Hex(password + slat);
//2.根据用户名何密码去user表中查询 是否存在
SysUser sysUser = sysUserService.findUser(account, password);
//3.如果不存在 登录失败
if (sysUser == null) {
return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(), ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
}
//4.如果存在 使用jwt 生成token 返回给前端
String token = JWTUtils.createToken(sysUser.getId());
//5.toekn放入redis,设置过期时间。登录认证的时候先认证token字符串是否合法,在认证redsi认证是否合法
redisTemplate.opsForValue().set("TOKEN_" + token, JSON.toJSONString(sysUser), 1, TimeUnit.DAYS);
return Result.success(token);
}
}
/**
* 根据account和password查询用户表
* @param account
* @param password
* @return
*/
@Override
public SysUser findUser(String account, String password) {
LambdaQueryWrapper<SysUser> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getAccount,account);
queryWrapper.eq(SysUser::getPassword,password);
//需要id,account,头像avatar,naickname昵称
queryWrapper.select(SysUser::getId,SysUser::getAccount,SysUser::getAvatar,SysUser::getNickname);
queryWrapper.last("limit 1");
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
return sysUser;
}
3.5 登录参数,redis配置
接受前端传来的登录参数:
@Data
public class LoginParam {
private String account;
private String password;
}
配置redis:
spring.redis.host=localhost
spring.redis.port=6379
5.获取用户信息
5.1 接口说明
接口url:/users/currentUser
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": {
"id":1,
"account":"1",
"nickaname":"1",
"avatar":"ss"
}
}
5.2 Controller
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private SysUserService sysUserService;
@GetMapping("currentUser")
public Result currentUser(@RequestHeader("Authorization") String token){
return sysUserService.findUserByToken(token);
}
}
5.3 Service
/**
* 根据token查询用户信息
* @param token
* @return
*/
@Override
public Result findUserByToken(String token) {
/**
* 1.token合法性校验:是否为空,解析是否成功,redis是否存在
* 2.如果校验失败,返回错误
* 3.如果成功,返回对应的结果 LoginUserVo
*/
SysUser sysUser=loginService.checkToken(token);
if(sysUser==null){
return Result.fail(ErrorCode.TOKEN_ERROR.getCode() ,ErrorCode.TOKEN_ERROR.getMsg());
}
LoginUserVo loginUserVo = new LoginUserVo();
loginUserVo.setId(String.valueOf(sysUser.getId()));
loginUserVo.setNickname(sysUser.getNickname());
loginUserVo.setAccount(sysUser.getAccount());
loginUserVo.setAvatar(sysUser.getAvatar());
return Result.success(loginUserVo);
}
/**
* 校验token是否合法
*
* @param token
* @return
*/
@Override
public SysUser checkToken(String token) {
if (StringUtils.isAllBlank(token)) {
return null;
}
Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
if (stringObjectMap == null) {
return null;
}
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
if (StringUtils.isBlank(userJson)) {
return null;
}
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
return sysUser;
}
6.登录拦截器
每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
6.1 拦截器实现
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
/**
* 在执行controlle方法之前执行
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 1、需要判断请求的接口和路径是否为 HandlerMethod(controller方法)
* 2、如果token是否为空,如果为空,为登录
* 3、如果token不为空,登录验证 loginService->checkToken
* 4、如果认证成功,放行
*/
if (!(handler instanceof HandlerMethod)) {
//handler可能是RequestResourceHandler 放行
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===========================");
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(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//登录验证成功
//用ThreadLocal保存用户信息
UserThreadLocal.put(sysUser);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除,ThreaLocal中用完的信息会有内存泄漏的风险
UserThreadLocal.remove();
}
}
6.2 使拦截器生效
@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");
}
}
测试:
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping
public Result test(){
return Result.success(null);
}
}
7.ThreadLocal保存用户信息
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();
}
}
8. 使用线程池更新阅读次数
可以参考:在SpringBoot中实现异步事件驱动
8.1 线程池配置
@ControllerAdvice
@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("小皮皮博客项目");
//等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}
8.2 使用
@Autowired
private ThreadService threadService;
/**
* 查看文章详情
* @param articleId
* @return
*/
@Override
public Result findArticleById(Long articleId) {
Article article = articleMapper.selectById(articleId);
ArticleVo articleVo = copy(article, true, true, true, true);
//查看完文章了,新增阅读数,有没有问题呢?
//查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低(没办法解决,增加阅读数必然要加锁)
//更新增加了此次接口的耗时(考虑减少耗时)如果一旦更新出问题,不能影响查看操作
//线程池解决,可以吧更新操作更新到主线程中执行,和主线程就不相关了
threadService.updateArticleViewCount(articleMapper, article);
return Result.success(articleVo);
}
@Component
public class ThreadService {
//如果我们想在调用一个方法的时候开启一个新的线程开始异步操作,我们只需要在这个方法上加上@Async注解,当然前提是,这个方法所在的类必须在Spring环境中。
@Async("taskExecutor")
//期望此操作在线程池执行。不会影响原有的主线程
public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
Article articleUpdate = new Article();
int viewCounts = article.getViewCounts();
articleUpdate.setViewCounts(viewCounts + 1);
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Article::getId, article.getId());
//为了在多线程的环境下,线程安全 CAS思想,防止此时修改的时候已经被修改了(乐观锁)
queryWrapper.eq(Article::getViewCounts, viewCounts);
//第一个参数用于生成set条件,第二个生成where语句
//update article set view_count =100 where view_count==99 and id =xxx
articleMapper.update(articleUpdate, queryWrapper);
try {
Thread.sleep(5000);
System.out.println("更新完成了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里的update
用法:
// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
参数说明:
类型 | 参数名 | 描述 |
---|---|---|
T | entity | 实体对象 (set 条件值,可为 null) |
Wrapper | updateWrapper | 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句) |
@Async
注解:如果我们想在调用一个方法的时候开启一个新的线程开始异步操作,我们只需要在这个方法上加上@Async
注解,当然前提是,这个方法所在的类必须在Spring环境中。
9.评论
9.1 接口说明
接口url:/comments/create/change
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
articleId | long | 文章id |
content | string | 评论内容 |
parent | long | 父评论id |
toUserId | long | 被评论的用户id |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": null
}
9.2 加入到登录拦截器中
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test").addPathPatterns("/comments/create/change");
}
9.3 Controller
构建评论参数对象:
package com.mszlu.blog.vo.params;
import lombok.Data;
@Data
public class CommentParam {
private Long articleId;
private String content;
private Long parent;
private Long toUserId;
}
@PostMapping("create/change")
public Result comment(@RequestBody CommentParam commentParam){
return commentsService.comment(commentParam);
}
9.4 Service
@Override
public Result comment(CommentParam commentParam) {
SysUser sysUser = UserThreadLocal.get();
Comment comment = new Comment();
comment.setArticleId(commentParam.getArticleId());
comment.setAuthorId(sysUser.getId());
comment.setContent(commentParam.getContent());
comment.setCreateDate(System.currentTimeMillis());
Long parent = commentParam.getParent();
if(parent==null||parent==0){
comment.setLevel(1);
}else{
comment.setLevel(2);
}
comment.setParentId(parent==null?0:parent);
Long toUserId = commentParam.getToUserId();
comment.setToUid(toUserId==null?0:toUserId);
commentMapper.insert(comment);
return Result.success(null);
}
//防止前端 精度损失 把id转为string
//分布式id 比较长,传到前端 会有精度损失,必须转为string类型 进行传输,就不会有问题了
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
10.AOP统一记录日志
关于AOP的文章可以参考:
- Spring-AOP基础概念和操作详解
- SpringBoot开发秘籍 - 利用 AOP 记录日志
自己实现一个日志注解
//Type代表可以放在类上面,METHOD代表可以放在方法上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
String module() default "";
String operator() default "";
}
统一日志处理切面
@Component
@Aspect //切面 定义通知和切点的关系
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.xpp.blog.common.aop.LogAnnotation)")
public void pt(){
}
//环绕通知
@Around("pt()")
public Object log(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.operator());
//请求的方法名
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================================");
}
}
使用
@PostMapping("")
//加上该注解代表要对此接口记录日志
@LogAnnotation(module = "文章", operator = "获取文章列表")
public Result listArticles(@RequestBody PageParams params) {
return articleService.listArticle(params);
}
11.文章图片上传
11.1 接口说明
接口url:/upload
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
image | file | 上传的文件名称 |
返回数据:
{
"success":true,
"code":200,
"msg":"success",
"data":"https://static.mszlu.com/aa.png"
}
导入七牛云依赖:
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.7.99]</version>
</dependency>
11.2 Controller
@RestController
@RequestMapping("upload")
public class UploadController {
@Autowired
private QiniuUtils qiniuUtils;
@PostMapping
public Result upload(@RequestParam("image")MultipartFile file){
//原始文件名称 比如xpp.png
String originalFilename = file.getOriginalFilename();
//得到一个唯一的文件名称
String fileName=UUID.randomUUID().toString()+"."+ StringUtils.substringAfterLast(originalFilename,".");
//上传文件
boolean upload = qiniuUtils.upload(file, fileName);
if(upload){
return Result.success(QiniuUtils.url+fileName);
}
return Result.fail(20001,"上传失败");
}
}
11.3 使用七牛云
配置上传文件的大小:
# 上传文件总的最大值
spring.servlet.multipart.max-request-size=20MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=2MB
@Component
public class QiniuUtils {
//填自己七牛云绑定的域名
public static final String url = "xxxxxxxxxxxx";
//从配置文件读取
@Value("${qiniu.accessKey}")
private String accessKey;
@Value("${qiniu.accessSecretKey}")
private String accessSecretKey;
public boolean upload(MultipartFile file,String fileName){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.huanan());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
String bucket = "xppll";
//默认不指定key的情况下,以文件内容的hash值作为文件名
try {
byte[] uploadBytes = file.getBytes();
Auth auth = Auth.create(accessKey, accessSecretKey);
String upToken = auth.uploadToken(bucket);
Response response = uploadManager.put(uploadBytes, fileName, upToken);
//解析上传成功的结果
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
12.AOP实习统一缓存处理(优化)
内存的访问速度 远远大于 磁盘的访问速度 (1000倍起)
自定义注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
//过期时间
long expire() default 1*60*1000;
//缓存标识 key
String name() default "";
}
定义切面:
@Aspect
@Component
@Slf4j
public class CacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
//切点
@Pointcut("@annotation(com.xpp.blog.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));
log.info("存入缓存~~~ {},{}",className,methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999,"系统错误");
}
}
使用:
@PostMapping("hot")
@Cache(expire = 5 * 60 * 1000,name = "hot_article")
public Result hotArticle(){
int limit = 5;
return articleService.hotArticle(limit);
}
注意:像文章列表这样的接口用了缓存,刷新页面的时候浏览次数,评论次数不会变!!!
13.年月归档中MySQL查询
13.1 Controller
/**
* 文档归档
* @return
*/
@PostMapping("listArchives")
public Result listArchives() {
return articleService.listArchives();
}
13.2 Service
/**
* 文章归档(年月归档)
*
* @return
*/
@Override
public Result listArchives() {
List<Archives> archivesList = articleMapper.listArchives();
return Result.success(archivesList);
}
13.3 具体sql实现
查看数据库表发现是bigint
型
- 这里用一一个时间戳函数
FROM_UNIXTIME()
转化为日期类型,13位及以上bigint
要先除以1000 - 在用
YEAR()
和MONTH()
函数取出对应的年,月
<!--按年月分组统计每组个数,返回year,month,count-->
<select id="listArchives" resultType="com.xpp.blog.dao.dos.Archives">
SELECT YEAR(FROM_UNIXTIME(create_date / 1000)) YEAR,
MONTH(FROM_UNIXTIME(create_date / 1000)) MONTH,
COUNT(*) COUNT
FROM ms_article
GROUP BY YEAR, MONTH;
</select>
int viewCounts = article.getViewCounts();
Article articleUpdate=new Article();
articleUpdate.setViewCounts(viewCounts+1);
LambdaUpdateWrapper<Article> updateWrapper=new LambdaUpdateWrapper<>();
updateWrapper.eq(Article::getId,article.getId());
//设置一个 为了在多线程的环境下,线程安全 CAS思想,防止此时修改的时候已经被修改了(乐观锁)
updateWrapper.eq(Article::getViewCounts,viewCounts);
//update article set view_count =100 where view_count==99 and id =xxx
articleMapper.update(articleUpdate,updateWrapper);
try {
Thread.sleep(5000);
System.out.println("更新完成了");
}catch (InterruptedException e){
e.printStackTrace();
}
14.对后端进行返回统一的标准格式
可以参考:SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!
14.1 定义返回对象
/**
* 封装返回给前端的信息
*/
@Data
@AllArgsConstructor
public class Result {
//请求是否成功
private boolean success;
//状态码
private int code;
//本次接口调用的结果描述
private String msg;
//本次返回的数据
private Object data;
/**
* @param data 返回给前端的数据
* @return
*/
public static Result success(Object data) {
return new Result(true, 200, "success", data);
}
public static Result fail(int code, String msg) {
return new Result(false, code, msg, null);
}
}
14.2 定义状态码
可以将所有的状态码封装为一个枚举类,方便管理:
public enum ErrorCode {
PARAMS_ERROR(10001, "参数有误"),
ACCOUNT_PWD_NOT_EXIST(10002, "用户名或密码不存在"),
TOKEN_ERROR(10003, "token不合法"),
ACCOUNT_EXIST(10004, "账户已存在"),
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;
}
//get,set方法...
}
15.项目亮点总结
-
jwt + redis
- token令牌的登录方式,访问认证速度快,session共享,安全性
- redis做了令牌和用户信息的对应管理,①进一步增加了安全性 ②登录用户做了缓存 ③灵活控制用户的过期(续期,踢掉线等)
-
threadLocal使用了保存用户信息,请求的线程之内,可以随时获取登录的用户,做了线程隔离。在使用完ThreadLocal之后,做了value的删除,防止了内存泄漏
-
线程安全- update table set value = newValue where id=1 and value=oldValue(CAS)
-
线程池应用非常广,面试7个核心参数(对当前的主业务流程无影响的操作,放入线程池执行)
-
权限系统(重点内容)
-
统一日志记录,统一缓存处理
最后喜欢的小伙伴,记得三连哦!😏🍭😘