Springboot+Vue博客项目总结(1)

//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 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.获取用户信息


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SK3ukVrG-1638022304550)(../../../AppData/Roaming/Typora/typora-user-images/image-20211125224045403.png)]

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

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 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(parentnull||parent0){

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的文章可以参考:

自己实现一个日志注解

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

}

导入七牛云依赖:

com.qiniu

qiniu-java-sdk

[7.7.0, 7.7.99]

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

//缓存标识 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

*/

算法

  1. 冒泡排序

  2. 选择排序

  3. 快速排序

  4. 二叉树查找: 最大值、最小值、固定值

  5. 二叉树遍历

  6. 二叉树的最大深度

  7. 给予链表中的任一节点,把它删除掉

  8. 链表倒叙

  9. 如何判断一个单链表有环

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

//获取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

*/

算法

  1. 冒泡排序

  2. 选择排序

  3. 快速排序

  4. 二叉树查找: 最大值、最小值、固定值

  5. 二叉树遍历

  6. 二叉树的最大深度

  7. 给予链表中的任一节点,把它删除掉

  8. 链表倒叙

  9. 如何判断一个单链表有环

    [外链图片转存中…(img-bbKnCJsu-1720101131594)]

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值