Springboot+Vue博客项目总结

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层还是servicedao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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<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的文章可以参考:

自己实现一个日志注解

//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实现

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

查看数据库表发现是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.项目亮点总结

  1. jwt + redis

    • token令牌的登录方式,访问认证速度快,session共享,安全性
    • redis做了令牌和用户信息的对应管理,①进一步增加了安全性 ②登录用户做了缓存 ③灵活控制用户的过期(续期,踢掉线等)
  2. threadLocal使用了保存用户信息,请求的线程之内,可以随时获取登录的用户,做了线程隔离。在使用完ThreadLocal之后,做了value的删除,防止了内存泄漏

  3. 线程安全- update table set value = newValue where id=1 and value=oldValue(CAS)

  4. 线程池应用非常广,面试7个核心参数(对当前的主业务流程无影响的操作,放入线程池执行)

  5. 权限系统(重点内容)

  6. 统一日志记录,统一缓存处理
    在这里插入图片描述
    最后喜欢的小伙伴,记得三连哦!???

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>