Springboot+Vue

Springboot+Vue博客项目总结

Springboot+Vue_第1张图片 项目 专栏收录该内容
1 篇文章 0 订阅
订阅专栏

文章目录

  • 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工程


<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.0modelVersion>
<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.登录功能实现

Springboot+Vue_第2张图片

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.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.1version>
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-codecgroupId>
    <artifactId>commons-codecartifactId>
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.获取用户信息

Springboot+Vue_第3张图片

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.qiniugroupId>
  <artifactId>qiniu-java-sdkartifactId>
  <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()函数取出对应的年,月

Springboot+Vue_第4张图片


<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. 统一日志记录,统一缓存处理
    Springboot+Vue_第5张图片
    最后喜欢的小伙伴,记得三连哦!

Springboot+Vue_第6张图片
LL.LEBRON
关注 关注
  • 20
    点赞
  • 28
    评论
  • 15
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

专栏目录
短视频app,基于 springboot + vue
11-04
该行业的大咖们每天都会在这里分享行业专业知识,用户可以在这里学习他们的从业经验的细节点,避免再犯这些错误,或者针对他们的分享,吸取有用的精华部分,进行专业知识 总结归纳,让他们的宝贵经验为你所用,当然...
表情包
插入表情
还能输入1000个字符
Springboot + Vue +Mo ngoDB 项目 总结
qq_46212498的博客
02-03 249
Springboot是什么? &em sp;&em sp; Spring Boot为开发提供一个具有最小功能的 Spring应用程序, 开发 Spring Boot的主要动机是简化配置和部署 spring应用程序的过程。它使用全新的开发模型,避免了一些繁琐的开发步骤和样板代码和配置。就像 M aven 整合了所有的 Jar 包, Spring Boot 整合了所有的框架。 Spring Boot的主要特点

创建独立的Spring应用程序直接嵌入Tomcat,Jetty或Undertow(无需部署WAR文件)提供“初始”的POM文件内容,以简


SpringBoot + vue旅游 项目 总结
napoluen的博客
06-24 861
Springboot + vue旅游 项目总结

项目为一个springboot+vue入门级小项目,视频地址为:https://www.bilibili.com/video/BV1Nt4y127Jh
业务简单,对提升业务能力没什么大的帮助,更多的是可以熟悉开发流程和编码。

1.表结构

仅仅三张表,分别为用户表,省份表和景点表,其中省份表和景点表为一对多的关系。

用户表(t_user):

省份表(t_province):

景点表(t_place):

2.相关配置
server.port=8989

	
springboot + vue 博客系统 总结
six_teen的博客
01-10 256
最近一段时间都在利用 springbootvue写一个自己的 博客系统,不只是个人系统,是一个类似csdn的 博客系统,目前已上线,访问地址如下: http://182.61.19.181:3000/ (ps: 由于经费有限,预计在2021-02-08后 服务器失效)。 在这次开发中(算的上是开发吧),遇到了挺多困难的,但也更让我熟悉 springbootvue等一系列框架就,以及自我解决问题的办法,同时也提升了代码的能力。 该 项目已托管于gitee,地址如下:https://gitee.com/six_teen
SpringBoot + vue 博客 项目所遇问题汇总
秋水小夕的博客
03-07 232
SpringBoot + vue前后端分离 博客 项目所遇问题汇总后端内容编辑所遇问题 vue前端内容编辑所遇问题

项目作者博客:https://blog.csdn.net/MarkerHub/article/details/106417097

后端内容编辑所遇问题
1、Spring Boot 类似Error creating bean with name “XXX”的错误
注意yml配置文件注意空格 和对齐形式
2、在idea中手动配置连接数据库

原因:服务器返回无效时区
解决方案1:
&emsp;&emsp;在mysql命令执


安排, SpringBoot + Vue 博客系统 项目2020年
WantFlyDaCheng的博客
07-11 49
来源:来自网络,如侵权请告知博主删除????。仅学习使用,请勿用于其他~最近很多小伙伴和我要 博客系统相关的资源,安排上~目录01_1_ 项目简介与大纲.mp44 T4 g4 [% \- ...
SpringBoot + vue 项目实战
java_zdc的博客
07-18 4080
目录

前言:

技术准备

开发流程

项目展示

前言:

项目学习地址,点击了解:https://how2j.cn

天猫整站 Springboot 版本,就是这样一个实践项目。 我会带着大家,从零开始,把整个项目构建出来。成熟的项目规划与设计
本教程作者,也就是我~ 有8年企业开发管理经验,7年教学经验,曾参与管理开发几十个商业项目。将展示如何合理设计与规划这样一个规模的电商项目,既做到功能丰富,又让开发节奏有条不紊。基于Springboot 框架技术
项目使用 Springboot 框架进行系统


SpringBoot + Vue 项目部署上线到 Linux 服务
欢迎来到Gorit的博客
08-22 2060
SpringBoot + Vue 前后端分离 项目集成部署前言一、 Vue 打包的 项目如何部署?1.1 Vue 项目打包1.2 使用 Ex press 代理静态资源文件二、 SpringBoot 项目如何部署?2.1 数据库部署可能出现的问题2.2 SpringBoot 项目打包上传三、 服务器配置3.1 Nginx 基本配置3.2 Nginx 反向代理 SpringBoot 服务 总结

前言
有了一个基于 ElementUI 的电商后台管理系统,在开发一个相似的后台就会轻松很多。最近 Vue3 出来了,等这个后端管


springboot + vue前后端分离 项目(后台管理系统)
最新发布
先养只猫的博客
08-13 9833
学习笔记 学习资源来自于B站良心up 一、所使用的环境配置: 编译器:IDEA 后台框架: SpringBoot Mybatis-Plus 数据库:Mysql8.0 数据库工具:N avicat premium 前端框架: Vue Element UI 引用的富文本编辑器:wa ngEditor 二、 项目简介 这是一个基于 SpringBootVue的后台管理系统。 主要功能: 1.实现用户信息的CRUD,以及页面的显示。 2.用户权限的分配,不同权限的用户锁能看到的的界面信息和能进行的操作是不同的。 3.实现图片
音乐网站 SpringBoot + Vue 项目 总结
weixin_48669196的博客
07-14 60
音乐网站 SpringBoot + Vue 项目 总结 项目说明 项目配置业务分析实体类DAO层映射XML文件Controller层 项目说明 首页

技术栈:SpringBootVue。后端功能相对简单,主要是增删查改,代码有些冗余,算是对之前SpringBoot知识的巩固。
项目配置
ot;1.0&quot; encoding=&quot;UTF-8&quot;?>
<project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://


你可能感兴趣的:(JAVA,java,项目,java,微服务,springboot,vue,项目)