Springboot+Vue博客项目总结

org.springframework.boot

spring-boot-starter-log4j2

org.springframework.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-starter-mail

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

test

org.springframework.boot

spring-boot-starter-data-redis

com.alibaba

fastjson

1.2.76

mysql

mysql-connector-java

org.springframework.boot

spring-boot-configuration-processor

true

org.apache.commons

commons-lang3

commons-collections

commons-collections

3.2.2

com.baomidou

mybatis-plus-boot-starter

3.4.3

org.projectlombok

lombok

joda-time

joda-time

2.10.10

org.springframework.boot

spring-boot-maven-plugin

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部分是否合法。

依赖包:

io.jsonwebtoken

jjwt

0.9.1

工具类:

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加密的依赖包:

commons-codec

commons-codec

@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 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);

Vue 编码基础

2.1.1. 组件规范

2.1.2. 模板中使用简单的表达式

2.1.3 指令都使用缩写形式

2.1.4 标签顺序保持一致

2.1.5 必须为 v-for 设置键值 key

2.1.6 v-show 与 v-if 选择

2.1.7 script 标签内部结构顺序

2.1.8 Vue Router 规范

Vue 项目目录规范

2.2.1 基础

2.2.2 使用 Vue-cli 脚手架

2.2.3 目录说明

2.2.4注释说明

2.2.5 其他

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值