springboot博客实战笔记01

发布于:2025-08-05 ⋅ 阅读:(14) ⋅ 点赞:(0)

对应的idea代码为blog-parent

一.mybatis-plus

MyBatis-Plus 介绍
MyBatis-Plus(简称 MP) 是一款基于 MyBatis 的增强工具,在 MyBatis 的基础上进行了扩展,提供了更便捷的 CRUD 操作、代码生成、条件查询等功能,旨在简化开发、提高效率,同时完全兼容原生 MyBatis。

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

1.1. MyBatis-Plus 的核心特点

(1)强大的 CRUD 操作

  • 内置通用 Mapper:继承 BaseMapper 即可直接使用单表的增删改查方法,无需编写 XML 或 SQL。
java
userMapper.selectById(1);  // 根据ID查询
userMapper.insert(user);   // 插入数据
userMapper.updateById(user); // 按ID更新
userMapper.deleteById(1);  // 按ID删除

通用 Service:提供 IServiceServiceImpl,封装了批量操作、链式查询等常用方法。

(2)智能条件构造器

  • QueryWrapper / LambdaQueryWrapper:支持链式调用,动态构建查询条件,防止 SQL 注入。
java
// 查询年龄大于18且姓"张"的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.gt("age", 18).like("name", "张");
List<User> users = userMapper.selectList(wrapper);

// Lambda 方式(推荐,避免硬编码字段名)
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.gt(User::getAge, 18).like(User::getName, "张");
  • UpdateWrapper:支持动态更新条件。

(3)自动代码生成器

通过 AutoGenerator 可一键生成 EntityMapperServiceController 等代码,减少重复工作。

java
AutoGenerator generator = new AutoGenerator();
generator.setDataSource(dataSourceConfig);
generator.setGlobalConfig(globalConfig);
generator.setPackageInfo(packageConfig);
generator.execute(); // 执行生成

(4)乐观锁支持
使用 @Version 注解实现乐观锁,避免并发修改冲突。

java

@Version
private Integer version; // 版本号字段

(5)分页插件

  • 内置分页插件,无需手动编写分页 SQL。
java
Page<User> page = new Page<>(1, 10); // 第1页,每页10条
IPage<User> userPage = userMapper.selectPage(page, queryWrapper);

(6)逻辑删除

通过 @TableLogic 注解实现逻辑删除(软删除),而非物理删除。

java

@TableLogic
private Integer deleted; // 0-未删除,1-已删除

(7)动态表名

  • 支持动态表名切换(如分表场景)。

(8)SQL 注入器

  • 可自定义全局 SQL 方法,扩展 MyBatis-Plus 功能。

1.2. MyBatis-Plus vs MyBatis

在这里插入图片描述

1. 3. 适用场景

  • 快速开发:适合需要快速搭建 CRUD 功能的项目(如后台管理系统)。
  • 单表操作:对单表查询、更新等操作有较高效率要求。
  • 减少 SQL 编写:希望减少 XML 或注解 SQL 的编写。

1.4. 总结

MyBatis-Plus 在 MyBatis 的基础上进行了大量增强,提供了更便捷的数据库操作方式,适用于大多数企业级 Java 开发场景,能显著提高开发效率,同时保持 MyBatis 的灵活性。

1.5mybatis-plus控制台打印日志:

/*写在yml或者xml里面*/
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

二、创建项目代码:

2.1启动类:

package com.mszlu.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BlogApp {
    public static void main(String[] args) {
        SpringApplication.run(BlogApp.class,args);

    }
}

2.2对应的mybatis-plus配置类:

package com.mszlu.blog.config;


import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.mszlu.blog.dao.mapper")
public class MybatisPlusConfig {

    //分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

2.3跨域配置:

原因是后端写的是8888端口 而前端是8080端口
所以书写代码允许8080端口的域名访问
如果前后端同时使用同一个端口那么就会出现端口冲突:

    1. 问题表现
      后端启动失败:如果后端(如 Spring Boot)先启动并占用了 8080 端口,前端(如 Vue/React 开发服务器)会因端口被占用而无法启动,反之亦然。
      服务不可访问:即使两者强行启动(如一个改为随机端口),用户也无法通过同一个端口同时访问前端和后端。
    1. 为什么不能共用端口?
      端口唯一性:同一台机器的同一个端口只能被一个进程监听。

协议冲突:前端(HTTP)和后端(HTTP API)虽然都是 HTTP,但它们的请求路径和代理规则不同,无法自动区分。

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //跨域配置
        registry.addMapping("/**").allowedOrigins("http://localhost:8080");
    }

2.4分页查询 文章列表:

ArticleServicleImpl:

 @Override
    public Result listArticle(PageParams pageParams) {
        /***
         * 分页查询  文章列表
         */
        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        //是否置顶进行排序
        // order by creat_date desc
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<Article> records = articlePage.getRecords();

        //能直接返回吗? 很明显不能
        List<ArticleVo> articleVoList = copyList(records,true,true);


        return Result.success(articleVoList);

    }

知识点1.LambdaQueryWrapper 详解

LambdaQueryWrapper 是 MyBatis-Plus(MP)提供的一个 Lambda 表达式风格的查询条件构造器,用于动态构建 SQL 查询条件。相比传统的 QueryWrapper,它通过 Lambda 方法引用(如 Article::getTitle) 替代字符串字段名(如 “title”),避免了硬编码问题,并提供编译期类型安全检查,减少运行时错误。

1. 核心优势

类型安全:通过 Lambda 表达式引用实体类字段,编译期检查字段名是否存在。

避免硬编码:不再需要写 “字段名” 字符串,减少因拼写错误导致的 Bug。

链式调用:支持流畅的链式写法,代码更简洁。

2. 基本用法

(1)创建 LambdaQueryWrapper:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  • (2)常用条件方法
    方法名 作用 示例
    eq() 等于 = wrapper.eq(User::getName, “Alice”) → name = ‘Alice’
    ne() 不等于 <> wrapper.ne(User::getAge, 18) → age <> 18
    gt() / ge() 大于 > / 大于等于 >= wrapper.gt(User::getAge, 18) → age > 18
    lt() / le() 小于 < / 小于等于 <= wrapper.lt(User::getAge, 30) → age < 30
    like() / notLike() 模糊匹配 LIKE wrapper.like(User::getName, “张”) → name LIKE ‘%张%’
    in() / notIn() 包含 IN / 不包含 NOT IN wrapper.in(User::getId, Arrays.asList(1, 2, 3)) → id IN (1,2,3)
    isNull() / isNotNull() 判空 IS NULL wrapper.isNull(User::getEmail) → email IS NULL
    orderByAsc() / orderByDesc() 排序 ORDER BY wrapper.orderByAsc(User::getAge) → ORDER BY age ASC

  • (3)LambdaQueryWrapper的选择特定字段(select)

wrapper.select(User::getId, User::getName, User::getAge);  // 只查询 id, name, age
  • (4)LambdaQueryWrapper的last()

LambdaQueryWrapper 的 last() 方法用于在 SQL 语句的最后追加自定义的 SQL 片段。这个方法通常用于添加一些 原生的 SQL 语句,比如 LIMIT、FOR UPDATE 等,这些语句可能无法直接通过 LambdaQueryWrapper 的其他方法实现。
直接拼接 SQL 片段:在生成的 SQL 语句末尾追加自定义内容

 @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getViewCounts);
        queryWrapper.select(Article::getId,Article::getTitle);
        queryWrapper.last("limit "+limit);
        //select id,title from article order by view_counts desc limit 5;

2.5copy & copylist

将实体类对象 转为 前端需要的实体类参数对象 (article --> articleVo)

   private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean  isAuthor) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
            articleVoList.add(copy(record,isTag,isAuthor,false,false));
        }
        return articleVoList;
    }
private ArticleVo copy(Article article,boolean isTag,boolean  isAuthor,boolean isBody,boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);

		//创建时间特殊 article里面是long型  但是vo里面是String
		
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        
        //并不是所有的接口 都需要标签,作者信息
        if(isTag){
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if(isAuthor){
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        if(isBody){
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if(isCategory){
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }
        return articleVo;
    }

解析一下这段代码:
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString(“yyyy-MM-dd HH:mm”));

  • article.getCreateDate():
    从 article 对象中获取原始时间字段,此时是一段时间戳类似于:1754137084000
  • new DateTime(…)
    将原始时间转换为 Joda-Time 的 DateTime 对象(需依赖 joda-time 库)。
    如果 article.getCreateDate() 是 Long 时间戳,DateTime 会将其转为日期时间对象。
    如果是 java.util.Date,DateTime 会直接包装它。
  • .toString(“yyyy-MM-dd HH:mm”)
    将 DateTime 对象格式化为指定格式的字符串,例如:“2023-10-01 14:30”。

2.6统一异常处理:

不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。

package com.mszlu.blog.handler;

import com.mszlu.blog.vo.Result;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

//对加了@Controller注解的方法进行拦截处理AOP的实现
@ControllerAdvice
public class AllExceptionHandler {
    //进行异常处理,处理Exception.class异常
    @ExceptionHandler(Exception.class)
    @ResponseBody//返回json数据
    public Result doException(Exception ex){
        ex.printStackTrace();
        return Result.fail(-999,"系统异常");
    }
}

2.7文件归档的时间问题:

在这里插入图片描述
create_date bigint(0) NULL DEFAULT NULL COMMENT ‘创建时间’,
因为bigint 13位 是毫秒级的 不能直接year(),需要先转换成date型后year(),

  <select id="listArchives" resultType="com.mszlu.blog.dao.dos.Archives">
      select year(FROM_UNIXTIME(create_date/1000)) as year,
      month(FROM_UNIXTIME(create_date/1000)) as  month,
      count(*) as  count
      from ms_article
      group by year,month
    </select>

FROM_UNIXTIME(create_date/1000)

create_date 是存储时间的字段(毫秒级)。

create_date/1000 将毫秒转换为秒(时间戳标准单位)。

FROM_UNIXTIME() 将秒级时间戳转换为 MySQL 的日期格式(如 2023-08-03 12:00:00)。

2.8JWT

登录使用JWT技术。
jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。
请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。
jwt 有三部分组成:A.B.C
A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定
B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。
jwt 验证,主要就是验证C部分 是否合法。

package com.mszlu.blog.utils;

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;

    }

    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"));
    }

}

2.9登录:

知识点:返回错误回应

创建一个类来封装返回的错误提示

在这里插入代码片`package com.mszlu.blog.vo;

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

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

使用的代码:

return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());

1.在loginServiceImpl中:

  /***
     * 1.检查参数是否合法
     * 2.根据用户名和密码去user表中查询  是否存在
     * 3.如果不存在 登陆失败
     * 4.如果存在 使用jwt  生成token 返回给前端
     * 5.token放入redis当中  redis token:user信息 设置过期时间
     * (登录认证的时候,先认证token字符串是否合法,去redis认证是否存在)
     * @param loginParam
     * @return
     */

//其实我认为在这里参数校验这一块 可以添加注解(@NotNull)之类进行校验 还有正则表达式

    @Override
    public Result login(LoginParam loginParam) {
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        if(StringUtils.isBlank(account)|| StringUtils.isBlank(password)){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }

        password = DigestUtils.md5Hex(password+salt);

        SysUser sysUser = sysUserService.findUser(account, password);
        if(sysUser == null){
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(), ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }

        String token = JWTUtils.createToken(sysUser.getId());

        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),60, TimeUnit.DAYS);
        return Result.success(token);
    }

代码解释:redisTemplate.opsForValue().set(“TOKEN_”+token, JSON.toJSONString(sysUser),60, TimeUnit.DAYS);
1.先创建一个RedisTemplate

@Autowired
private RedisTemplate<String,String> redisTemplate;

1. 方法调用链

redisTemplate.opsForValue()
获取操作 Redis String 类型的接口(ValueOperations)。
.set(key, value, timeout, timeUnit)
存储键值对,并设置过期时间。

2. 参数说明

参数 说明
“TOKEN_” + token Redis 的 Key,通常拼接前缀(如 TOKEN_)避免与其他业务 Key 冲突。
JSON.toJSONString(sysUser) Redis 的 Value,将 sysUser 对象序列化为 JSON 字符串(需依赖 fastjson 或 jackson)。
60 过期时间的数值,这里表示 60。
TimeUnit.DAYS 过期时间的单位,这里是天数(还支持 SECONDS、MINUTES、HOURS 等)。

2.10登陆后获取用户信息:

在这里插入图片描述

1.userController类:

package com.mszlu.blog.controller;


import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private SysUserService sysUserService;

    @GetMapping("/currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){


        return sysUserService.findUserByToken(token);
    }

}

2.SysUserServiceImpl

@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(sysUser.getId());
        loginUserVo.setNickname(sysUser.getNickname());
        loginUserVo.setAvatar(sysUser.getAvatar());
        loginUserVo.setAccount(sysUser.getAccount());
        return Result.success(loginUserVo);
    }

3.loginServiceImp:

@Override
    public SysUser checkToken(String token) {
        if(StringUtils.isBlank(token)){
          return  null;
        }
        //验证token
        Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
        if(stringObjectMap == null){
            return null;
        }
        //验证redis
        String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
        if(StringUtils.isBlank(userJson)){
            return null;
        }


        return JSON.parseObject(userJson, SysUser.class);
    }

2.11退出登录

 //登录成功,使用JWT生成token,返回token和redis中
        String token = JWTUtils.createToken(sysUser.getId());
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
       return Result.success(token);
  • 退出登录这一块 有两个方法来解决
    • 1.删除更改token
    • 2.删除redis
  • 但是在token只能在前端进行清除,后端只能删除redis中的信息 相当于退出登录
    @Override
    public Result logout(String token) {
        redisTemplate.delete("TOKEN_"+token);
        return Result.success(null);
    }

2.12注册

@Override
    public Result register(RegisterParam registerParam) {
        /**
         * 1.检查参数是否合法
         * 2.判断账户是否存在  存在 返回账户以及被注册
         * 3.不存在 注册
         * 4.注册成功 生成token
         * 5.存入redis 并返回
         * 6.注意  加上事务 一旦中间的任何过程出现问题  注册的用户需要回滚
         *
         */
        String account = registerParam.getAccount();
        String password = registerParam.getPassword();
        String nickname = registerParam.getNickname();
        if(StringUtils.isBlank(account)
                || StringUtils.isBlank(password)
                || StringUtils.isBlank(nickname)
        ){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }
        SysUser sysUser = sysUserService.findUserByAccount(account);
        if(sysUser != null){
            return  Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(), ErrorCode.ACCOUNT_EXIST.getMsg());
        }

        sysUser = new SysUser();
        sysUser.setNickname(nickname);
        sysUser.setAccount(account);
        sysUser.setPassword(DigestUtils.md5Hex(password+ salt));
        sysUser.setCreateDate(System.currentTimeMillis());
        sysUser.setLastLogin(System.currentTimeMillis());
        sysUser.setAvatar("static/images/wps.png");
        sysUser.setAdmin(1); //1 为true
        sysUser.setDeleted(0); // 0 为false
        sysUser.setSalt("");
        sysUser.setStatus("");
        sysUser.setEmail("");
        //保存用户
        this.sysUserService.save(sysUser);
        String token = JWTUtils.createToken(sysUser.getId());
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
        return Result.success(token);
    }

    @Override
    public void save(SysUser sysUser) {
        //保存用户这 id会自动生成
        //这个地方 默认生成的id 是分布式id  雪花算法
        //mybatis-plus
        this.sysUserMapper.insert(sysUser);
    }

事务回滚注解

每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。

@Transactional//事务回滚

2.13登录拦截器

1.创建一个拦截器

@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 程序  访问静态资源  默认去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===========================");
        if(StringUtils.isBlank(token)){
            Result  result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(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().println(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.启动拦截器: WebMVCConfig

  @Override
    public void addInterceptors(InterceptorRegistry registry) {

        //拦截的接口   后续实际遇到需要拦截的接口时  再配置真正的拦截接口
//        registry.addInterceptor(loginInterceptor).addPathPatterns("/**")
//                .excludePathPatterns("/register","/login");
        registry.addInterceptor(loginInterceptor).addPathPatterns("/test");

    }

ThreadLocal保存登录用户信息

  • 为了解决在登录拦截被放行之后直接获取用户信息

1.创建Threadlocal:

package com.mszlu.blog.utils;

import com.mszlu.blog.dao.pojo.SysUser;

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.直接在拦截器中获取

   UserThreadLocal.put(sysUser);

1. ThreadLocal内存泄漏

在这里插入图片描述
实线代表强引用,虚线代表弱引用

每一个Thread维护一个ThreadLocalMap, key为使用弱引用的ThreadLocal实例,value为线程变量的副本。

强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

线程是强引用 当这条线程出现JVM垃圾回收时ThreadLocal由于是弱引用而被回收,而这个Key被回收了,根据key就找不到value了
随着积累 越来越多的value流失 造成了ThreadLoacl内存泄漏

使用线程池 更新阅读次数

 @Override
    public Result findArticleById(Long articleId) {
        /**
         * 1.根据id查询 文章信息
         * 2.根据bodyId 和 categoryid 去做关联查询
         */
        Article article = this.articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article,true,true,true,true);
        //查看完文章之后,新增阅读数  有没有问题呢
  
        threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);

查看完文章之后,新增阅读数 有没有问题呢
查看完文章之后 本该直接返回数据了,这时候做一个更新操作 更新时加写锁 阻塞其他的操作,性能会降低
更新 新增了此次接口的 耗时 如果一旦超时时 不能影响 查看文章的操作
线程池,可以把更新操作 扔到线程池当中 去执行 和主线程就不相关了
#3.1 线程池配置

package com.mszlu.blog.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;

@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("码神之路博客项目");
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();
        return executor;
    }
}

ThreadService

@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);
        LambdaQueryWrapper<Article> updateWrapper = new LambdaQueryWrapper<>();
        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) {
           e.printStackTrace();
        }
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到