IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 码神之路项目——总结 -> 正文阅读

[Java知识库]码神之路项目——总结

spring boot 练手实战项目说明

码神之路网站所使用的博客,项目简单,需求明确,容易上手,非常适合做为练手级项目。

最终成品

blog.mszlu.com

项目讲解说明:

提供前端工程,只需要实现后端接口即可
项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
开发完成后,开始优化项目,提升编程思维能力
比如页面静态化,缓存,云存储,日志等
docker部署上线
云服务器购买,域名购买,域名备案等
项目使用技术 :

springboot + mybatisplus+redis+mysql

基础知识
Spring常用注解,注解 IOC ,AOP,MVC 的理解
mybatisDao层 Mapper层 controller层 service层 model层 entity层 简介
mall商场学习文档
mybatisplus学习文档
mybatisplus配套代码
easycode搭配mybatisplus巨爽
@Autowired 的时候为什么注入接口不是实现类
@Service注解为什么是标注在实现类上的
mapper接口需要加注解吗?通过MapperScan减少注解
@Mapper与@MapperScan注解的作用是什么?

工程搭建

前端的工程:

npm install
npm run build
npm run dev

?新建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>

application配置文件配置

server.port=8888
spring.application.name=komorebi_blog
#数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=747699
#mybatis
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#标识表名均为ms_前缀,后续操作可以不用定义这个前缀
mybatis-plus.global-config.db-config.table-prefix=ms_


?MybatisPlus配置

创建配置类,设置分页查询(一般项目都会用到,所以提前配置好),注意@MapperScan(“com.komorebi.mapper”)注解。
配置类一定要加@Configuration

@Configuration
@MapperScan("com.komorebi.mapper")
public class MybatisPlusConfig {

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

跨域问题解决

创建WebMVCConfig配置类,解决不同端口之间的跨域问题
配置类一定要加@Configuration

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
    //    跨域设置
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedOrigins("http://localhost:8080")
                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                .allowedHeaders("*")
                .maxAge(3600);
    }
}


二、首页配置

首页分页显示文章信息

控制类

@RestController//JSON数据交互
@RequestMapping("articles")
public class ArticleController {

    @Autowired
    ArticleService articleService;

    //分页显示文章列表
    @PostMapping
    public Result listArticle(@RequestBody PageParams pageParams){
        return articleService.listArticle(pageParams);
    }
}

vo类
Vo包中的类才是前端实际拿到的数据。
文章显示控制类返回的是一个Result对象,参数是PageParams对象,这两个类都放在vo包中,vo中的类都是前端显示数据的类,一般前端中只显示数据库表中部分数据。

Result类,定义了两个静态方法,分别表示请求成功、请求失败。
?

package com.komorebi.vo;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Result {
    private boolean success;
    private int code;
    private String msg;
    private Object data;

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

前端传给后端接口的json数据,我们都封装为param类,方便操作。
PageParams 类定义了分页查询的page和pageSize,分别对应分页查询中的start和size。

package com.komorebi.vo;

import lombok.Data;

@Data
public class PageParams {
    private int page = 1;
    private int pageSize = 1;
}

ArticleVo 类是前端显示文章信息的类,所以在查询文章列表时,就要做数据库中Article类向ArticleVo 类的转换。

package com.komorebi.vo;

import lombok.Data;

import java.util.List;

@Data
public class ArticleVo {
    private Long id;

    private String title;

    private String summary;  //简介

    private int commentCounts;

    private int ViewCounts;

    private int weight;   //置顶

    private String createDate;  //创建时间

    private String author;

//暂时不需要
//    private ArticleBodyVo body;

    private List<TagVo> tags;

//暂时不需要
//    private CategoryVo category;

}

TagVo 类是前端显示标签信息的类,所以在查询文章列表时,就要做数据库中Tag类向TagVo 类的转换。

package com.komorebi.vo;

import lombok.Data;

@Data
public class TagVo {
    private Long id;
    private String tagName;
}

mapper接口

此处定义了三个mapper接口,分别为ArticleMapper、SysUserMapper、TagMapper。

package com.komorebi.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.komorebi.pojo.Article;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
public interface ArticleMapper extends BaseMapper<Article> {
}
--------------------------------------------------------------
@Repository
public interface TagMapper extends BaseMapper<Tag> {
    List<Tag> findTagsByArticleId(Long articleId);
}
-------------------------------------------------------------
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {
} 

?

因为tag和article有一张对应的表,所以要查询article对应的tag时,需要设计到多表的查询,但是,mybatisplus不支持多表查询,所以需要自己写mapper.xml文件。

注意:mapper.xml文件所在目录要和mapper对应,本次工程都在com.komorebi.mapper下。
可以在application.properties中mybatis-plus开启驼峰命名
mybatis-plus.configuration.map-underscore-to-camel-case=true
这样SQL语句就不需要as别名。
TagMapper.xml
?

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.komorebi.mapper.TagMapper">
    <select id="findTagsByArticleId" parameterType="long" resultType="com.komorebi.pojo.Tag">
    # 可以在application.properties中mybatis-plus开启驼峰命名
    # mybatis-plus.configuration.map-underscore-to-camel-case=true
    # 这样SQL语句就不需要as别名
        select id,avatar,tag_name as tagName from ms_tag
        where id in
        (select tag_id from ms_article_tag where article_id=#{articleId})
    </select>
</mapper>

service层部分

该阶段定义了三个service接口类。

?

package com.komorebi.service;

import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;

public interface ArticleService {
    //分页查询文章列表
    Result listArticle(PageParams pageParams);
}
--------------------------------------------------------
package com.komorebi.service;

import com.komorebi.pojo.SysUser;

public interface SysUserService {
    SysUser findUserById(Long id);
}
--------------------------------------------------------
package com.komorebi.service;

import com.komorebi.vo.TagVo;

import java.util.List;

public interface TagService {
    //通过文章id查询ui赢得标签,有一张表专门映射文章id和标签id
    List<TagVo> findTagsByArticleId(Long articleId);
}

serviceImpl类

ArticleServiceImpl

该实现类目前实现了文章分页查询。

知识点:
1)Page类定义分页对象;
2)LambdaQueryWrapper定义查询wrapper;
3)selectPage()函数返回的是Page对象,通过getRecords获得Article对象列表。
4)copy和copyList函数实现Article到ArticleVo的转换
5)BeanUtils.copyProperties(article,articleVo),可以实现对象之间的复制,相同属性名复制,不同属性名为null。
?

package com.komorebi.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.komorebi.mapper.ArticleMapper;
import com.komorebi.pojo.Article;
import com.komorebi.vo.ArticleVo;
import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ArticleServiceImpl implements ArticleService{
   @Autowired
   ArticleMapper articleMapper;
   @Autowired
   TagService tagService;
   @Autowired
   SysUserService sysUserService;
   //分页查询
   @Override
   public Result listArticle(PageParams pageParams) {
       //分页
       Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());

       LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
       //先置顶排序由属性weight决定,后按照时间排序
       wrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
       Page<Article> articlePage = articleMapper.selectPage(page, wrapper);
       //文章列表
       List<Article> records = articlePage.getRecords();
       //因为页面展示出来的数据不一定和数据库一i杨没所以我们要做一个抓安环
       //将查出的数据复制到articleVo中实现解耦,vo和页面数据交换
       List<ArticleVo> articleVo = copyList(records,true,true);

       return Result.success(articleVo);
   }

   //copyList实现
   private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
       ArrayList<ArticleVo> articleVos = new ArrayList<>();
       for(Article article:records){
           articleVos.add(copy(article,isTag,isAuthor));
       }
       return articleVos;
   }
   //这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,想Article和articlevo相同属性的拷贝过来返回
   private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
       ArticleVo articleVo = new ArticleVo();
       BeanUtils.copyProperties(article,articleVo);
       //joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
       articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
       //是否显示标签和作者
       if(isTag){
           articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
       }
       if(isAuthor){
           articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
       }
       return articleVo;
   }
}

TagMapperServiceImpl
此处也会涉及对象之间的复制,原理同ArticleServiceImpl,这里实现的是Tag类复制为TagVo类。

?

package com.komorebi.service;

import com.komorebi.mapper.TagMapper;
import com.komorebi.pojo.Article;
import com.komorebi.pojo.Tag;
import com.komorebi.vo.ArticleVo;
import com.komorebi.vo.TagVo;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class TagServiceImpl implements TagService{
   @Autowired
   TagMapper tagMapper;
   @Override
   public List<TagVo> findTagsByArticleId(Long articleId) {
       List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
       return copyList(tags);
   }
   //copyList实现
   private List<TagVo> copyList(List<Tag> tags) {
       ArrayList<TagVo> tagVos = new ArrayList<>();
       for(Tag tag : tags){
           tagVos.add(copy(tag));
       }
       return tagVos;
   }
   private TagVo copy(Tag tag){
       TagVo tagVo = new TagVo();
       //BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为null
       BeanUtils.copyProperties(tag,tagVo);
       return tagVo;
   }

}

SysUserServiceImpl

package com.komorebi.service;

import com.komorebi.mapper.SysUserMapper;
import com.komorebi.pojo.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SysUserServiceImpl implements SysUserService{
   @Autowired
   SysUserMapper sysUserMapper;
   @Override
   public SysUser findUserById(Long id) {
       return sysUserMapper.selectById(id);
   }
}

首页

首页最热标签?

思路:
1)首先在ms_article_tag表操作,通过tag_id分组并排序获得前几名,返回一个tag_id列表。
2)然后根据tag_id列表查询ms_tag表中对应的id和tagName将查询结果返回为TagVo对象(作者使用的是返回为Tag对象,但是由于前端展示的都是vo类,所以我们转换为TagVo)。

接下来的就是编码环节
TagController类

@RestController
@RequestMapping("/tags")
public class TagsController {

    @Autowired
    TagService tagService;
    //tags/hot相应最热标签tag对象
    @RequestMapping("/hot")
    public Result hot(){
        int limit = 6;//最热六个
        return tagService.hots(limit);
    }
}

TagService

Result hots(int limit);


TagMapper

import com.komorebi.vo.Result;
import com.komorebi.vo.TagVo;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface TagMapper extends BaseMapper<Tag> {
? ? List<Tag> findTagsByArticleId(Long articleId);

? ? //查询最热标签前limit条
? ? List<Long> findHotsTagId(int limit);

? ? //通过最热标签tagid查询最热tags
? ? List<TagVo> findTagsByIds(List<Long> tagIds);
}


TagMapper对应的Mapper文件
重点:
1)findHotsTagId涉及分组并排序
2)findTagsByIds涉及到foreach标签

<!-- ? ?查询最热标签id,提取前limit个-->
? ? <select id="findHotsTagId" parameterType="int" resultType="long">
? ? ? ? select tag_id from ms_article_tag
? ? ? ? group by tag_id
? ? ? ? order by count(*) limit #{limit}
? ? </select>

<!-- ? ?根据最热标签id查询对应tag对象-->
? ? <select id="findTagsByIds" resultType="com.komorebi.vo.TagVo" parameterType="list">
? ? ? ? select id, tag_name from ms_tag
? ? ? ? where id in
? ? ? ? <foreach collection="tagIds" item="tagId" separator="," open="(" close=")">
? ? ? ? ? ? #{tagId}
? ? ? ? </foreach>
? ? </select>



TagServiceImpl

?//查询若干个最热标签功能
? ? public Result hots(int limit){
? ? ? ? List<Long> hotsTagId = tagMapper.findHotsTagId(limit);
? ? ? ? //判断hotsTagId是否为空
? ? ? ? if(CollectionUtils.isEmpty(hotsTagId)){
? ? ? ? ? ? return Result.success(Collections.emptyList());
? ? ? ? }
? ? ? ? List<TagVo> tagsList = tagMapper.findTagsByIds(hotsTagId);
? ? ? ? return Result.success(tagsList);
? ? }



统一异常处理
定义Handler包,设置统一异常处理类AllExceptionHandler ,
1)@ResponseBody
2)@ControllerAdvice
3)@ExceptionHandler(Exception.class)

package com.komorebi.handler;

import com.komorebi.vo.Result;
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)
? ? //返回json数据
? ? @ResponseBody
? ? public Result doExceptionHandler(Exception e) {
? ? ? ? return Result.fail(-999,"系统异常,抱歉!");
? ? }
}



最热文章


原理同最热标签查询。根据view_count排序,选择出最热文章。


Controller

//显示最热文章
? ? @PostMapping("/hot")
? ? public Result hotArticle(){
? ? ? ? int limit = 3;
? ? ? ? return articleService.hotArticle(limit);
? ? }



Service

Result hotArticle(int limit);


ServiceImpl

//根据view_counts字段查询最热文章
? ? @Override
? ? public Result hotArticle(int limit) {
? ? ? ? LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
? ? ? ? wrapper.orderByDesc(Article::getViewCounts)
? ? ? ? ? ? ? ? .select(Article::getId,Article::getTitle)
? ? ? ? ? ? ? ? .last("limit "+limit);
? ? ? ? List<Article> articles = articleMapper.selectList(wrapper);
? ? ? ? return Result.success(copyList(articles,false,false));
? ? }


最新文章显示


原理和最热文章完全相同,只是通过create_date字段排序,选择出最新
Controller

?/

/显示最新文章
? ? @PostMapping("/new")
? ? public Result newArticle(){
? ? ? ? int limit = 3;
? ? ? ? return articleService.newArticle(limit);
? ? }


Service

Result newArticle(int limit);


ServiceImpl

//最新文章
? ? @Override
? ? public Result newArticle(int limit) {
? ? ? ? LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
? ? ? ? wrapper.orderByDesc(Article::getCreateDate)
? ? ? ? ? ? ? ? .select(Article::getId,Article::getTitle)
? ? ? ? ? ? ? ? .last("limit "+limit);
? ? ? ? List<Article> articles = articleMapper.selectList(wrapper);
? ? ? ? return Result.success(copyList(articles,false,false));
? ? }



文章归档显示


由于这个归档查询涉及到数据库内部函数Year、Month,所以MybatisPlus不能实现,需要通过mapper.xml文件实现。

注意:新建dos包,do对象也是数据库中查询出的对象,但它并不需要一些持久化的对象,我们把这些对象放在do包中,由于do是一个关键词,所以在命名是加了s,即dos。
文章归档返回的对象为Archives(档案)类。
思路可通过sql语句了解。

Archives类

package com.komorebi.dos;

import lombok.Data;

@Data
public class Archives {
? ? private Integer year;
? ? private Integer month;
? ? private Long count;

}



Controller

//文章归档
? ? @PostMapping("/listArchives")
? ? public Result listArchives(){
? ? ? ? return articleService.listArchives();
? ? }



Mapper

List<Archives> listArchives();
Mapper.xml
数据库中create_date 为bigint 13位,直接year()不行,需要先转date型后year()。
完整sql语句

select year(FROM_UNIXTIME(create_date/1000)) as year,
?? ? ? month(FROM_UNIXTIME(create_date/1000)) as month,?
?? ? ? count(*) count
??? ? ? from ms_article
? ?? ? ? group by year,month;

<!DOCTYPE mapper
? ? ? ? PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
? ? ? ? "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.komorebi.mapper.ArticleMapper">
? ? <select id="listArchives" resultType="com.komorebi.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>
</mapper>



Service

Result listArchives();



ServiceImpl

//文章归档
? ? @Override
? ? public Result listArchives() {
? ? ? ? List<Archives> archivesList = articleMapper.listArchives();
? ? ? ? return Result.success(archivesList);
? ? }

三、登录功能实现


登录接口返回给浏览器一个token
因为每次登录都有错误验证,所以自己定义了一个ErrorCode类
ErrorCode.class

package com.komorebi.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;
? ? }
}

?

Controller

import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/login")
public class LoginController {

? ? @Autowired
? ? private LoginService loginService;
? ? //登录
? ? @PostMapping
? ? public Result login(@RequestBody LoginParam loginParam){
? ? ? ? return loginService.login(loginParam);
? ? }
}

?LoginService

  Result login(LoginParam loginParam);

?LoginServiceImpl
导入依赖Commons-codec实现MD5加密

?

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.13</version>
</dependency>

?

登录功能的核心步骤

1、检查参数是否合法
2、根据用户名和密码检查ms_sys_user表中对应的account、password字段
3、如果不存在,登录失败
4、因为数据库中password字段存放的是经过MD5加密过的密码,所以在查数据库表前,要先对password进行加密
5、如果存在,使用jwt,生成token,返回给前端
6、token放在redis中,redis映射token和user信息设置过期时间,先认证token是否合法,再认证redis是否存在

?

package com.komorebi.service;

import com.alibaba.fastjson.JSON;
import com.komorebi.Utils.JWTUtils;
import com.komorebi.pojo.SysUser;
import com.komorebi.vo.ErrorCode;
import com.komorebi.vo.LoginParam;
import com.komorebi.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.apache.commons.codec.digest.DigestUtils;

import java.util.concurrent.TimeUnit;

@Service
public class LoginServiceImpl implements LoginService{

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    //md5加密使用到的盐
    private static final String salt="mszlu!@#";
    @Override
    public Result login(LoginParam loginParam) {
        /*
        * 1、检查参数是否合法
        * 2、根据用户名和密码检查ms_sys_user表中对应的account、password字段
        * 3、如果不存在,登录失败
        * 4、如果存在,使用jwt,生成token,返回给前端
        * 5、token放在redis中,redis映射token和user信息
        * 设置过期时间,先认证token是否合法,再认证redis是否存在
        * */
        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());
        }

        //这里使用的DigestUtils是来自于commons-codec包的,需要外部导入依赖
        //密码加盐,因为数据库中的密码是经过盐加密的
        System.out.println(password);
        SysUser user = sysUserService.findUser(account, password);

        //用户名密码错误
        if(user == null){
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }

        //用户名密码正确
        String token = JWTUtils.createToke(user.getId());

        //存入redis,要确保已开启redis
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);
        return Result.success(token);
    }
}

代码分析:
1)先定义一个盐加密字符串,在对password进行加密

private static final String salt="mszlu!@#";
password = DigestUtils.md5Hex(password+salt);

2)验证时需要通过sysUserService查询到对应用户并返回部分用户信息

SysUser user = sysUserService.findUser(account, password);

sysUserService.findUser方法实现

 @Override
    public SysUser findUser(String account, String password) {
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getAccount,account)
                .eq(SysUser::getPassword,password)
                .select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname)
                .last("limit 1");
        return sysUserMapper.selectOne(wrapper);
    }

3)查到用户后,给用户返回一个JWT (token)

//用户名密码正确
String token = JWTUtils.createToke(user.getId());
4)用户token存入redis,要确保已开启redis

 @Autowired
 private RedisTemplate<String,String> redisTemplate;
 redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);

5)由于要用到redis,在配置文件进行配置

#redis配置
spring.redis.host=localhost
spring.redis.port=6379

因为登录方法参数为LoginParam类,该类将前端传给后端的参数封装。
LoginParam

package com.komorebi.vo;

import lombok.Data;

@Data
public class LoginParam {
    private String account;

    private String password;
}

登陆后获取用户信息


登陆后用户的token会存放在浏览器本地,当用户登陆时会在请求头携带token,token中含有用户的id,用户的信息实际存放在redis中。

controller
token放在请求头中,所以要获取token
通过@RequestHeader(“Authorization”)注解获取
?

package com.komorebi.controller;

import com.komorebi.service.SysUserService;
import com.komorebi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    SysUserService sysUserService;

    //用户信息展示请求
    //token放在请求头中,所以要获取token需要@RequestHeader("Authorization")注解
    @GetMapping("/currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){
        return sysUserService.findUserByToken(token);
    }
}

SysUserService

Result findUserByToken(String token);

SysUserServiceImpl
该类用于返回用户信息,由于页面展示用户部分信息,所以创建Vo用户类LoginUserVo

package com.komorebi.vo;

import lombok.Data;

@Data
public class LoginUserVo {
    private  Long id;

    private String account;

    private String nickname;

    private String avatar; //头像
}

findUserByToken方法

@Override
    public Result findUserByToken(String token) {
        //获取用户展示信息
        SysUser sysUser = loginService.checkToken(token);
        if(sysUser==null)
            return Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg());
        //sysUser转为前端显示对象LoginUserVo
        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setAccount(sysUser.getAccount());
        loginUserVo.setAvatar(sysUser.getAvatar());
        loginUserVo.setId(sysUser.getId());
        loginUserVo.setNickname(sysUser.getNickname());
        return Result.success(loginUserVo);
    }

findUserByToken会调用LoginServiceImp中的checkToken方法,检查token的合法性并返回SysUser对象。

LoginServiceImpl

@Override
    public SysUser checkToken(String token) {
        //token为空
        if(StringUtils.isBlank(token))
            return null;
        //解析token
        Map<String, Object> checkToken = JWTUtils.checkToken(token);
        //解析为空
        if(checkToken==null)
            return null;
        //redis不存在token,user信息存放在redis中
        String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
        if(StringUtils.isBlank(userJson))
            return null;
        //token解析成功,并且redis存在
        //JSON.parseObject将json对象转为SysUser对象
        SysUser sysUser = JSON.parseObject(userJson,SysUser.class);
        return sysUser;
    }

退出登录

退出登录就是删除redis中的toke
Controller

@RestController
@RequestMapping("/logout")
public class LoginOutController {

    @Autowired
    LoginService loginService;
    @GetMapping
    public Result logout(@RequestHeader("Authorization") String token){
        return loginService.logout(token);
    }
}

LoginServiceImpl

 //退出登录
    @Override
    public Result logout(String token) {
        redisTemplate.delete("TOKEN_"+token);
        return Result.success(null);
    }

注册

注册和登录的功能有点类似
controller

@RestController
@RequestMapping("/register")
public class RegisterController {
    @Autowired
    LoginService loginService;
    //注册功能,返回数据为token
    @PostMapping
    public Result register(@RequestBody LoginParam loginParam){
        return loginService.register(loginParam);
    }
}

本项目的注册功能在LoginService实现
LoginService

Result register(LoginParam loginParam);

LoginServiceImpl
思路:
1)判断参数是否合法;
2)判断账户是否已经存在;
3)若合法并且不存在,则创建新用户;
4)生成token;
5)token即用户信息存入redis;
6)注意:在SysServiceImpl中设置事务,一旦出现问题,就回滚;
7)返回给前端token.
?

@Override
    public Result register(LoginParam loginParam) {
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        String nickname = loginParam.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(), "账号已经被注册");
        }
        //创建用户,ID默认为自增
        sysUser =new SysUser();
        sysUser.setAccount(account);                                   //账户名
        sysUser.setNickname(nickname);                                  //昵称
        sysUser.setPassword(DigestUtils.md5Hex(password+salt));  //密码加盐md5
        sysUser.setCreateDate(System.currentTimeMillis());              //创建时间
        sysUser.setLastLogin(System.currentTimeMillis());               //最后登录时间
        sysUser.setAvatar("/static/img/logo.b3a48c0.png");              //头像
        sysUser.setAdmin(1);                                             //管理员权限
        sysUser.setDeleted(0);                                             //假删除
        sysUser.setSalt("");                                                //盐
        sysUser.setStatus("");                                              //状态
        sysUser.setEmail("");                                               //邮箱
        this.sysUserService.save(sysUser);

        //生成token
        String token = JWTUtils.createToke(sysUser.getId());
        //token存入redis
        redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser),1,TimeUnit.DAYS);
        return Result.success(token);
    }

上面代码中涉及到两个函数
sysUserService.findUserByAccount(account)查询用户
sysUserService.save(sysUser)创建用户

findUserByAccount方法

   @Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getAccount,account)
                .last("limit 1");
        return this.sysUserMapper.selectOne(wrapper);
    }

save方法

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

登录拦截器

定义拦截器LoginInterceptor

package com.komorebi.handler;

import com.alibaba.fastjson.JSON;
import com.komorebi.Utils.JWTUtils;
import com.komorebi.pojo.SysUser;
import com.komorebi.service.LoginService;
import com.komorebi.vo.ErrorCode;
import com.komorebi.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    public LoginService loginService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /*1、需要判断请求的接口上是否是HandleMethod即controller方法
        * 2、判断token是否为空,为空未登录
        * 3、不为空,登陆验证(通过LoginServiceImpl中的checkToken方法)
        * 4、如果认证成功,则放行
        * */
        if(!(handler instanceof HandlerMethod)){
            //拦截器是拦截的controller中的方法,controller的方法其实就是一个Handler
            //handler可能是RequestResourceHandle(访问资源handle),即可能是访问静态资源的方法
            //解释:controller对应HandlerMethod,所以拦截器只拦截HandlerMethod
            return true;
        }
        //获取token
        String token = request.getHeader("Authorization");
        //日志问题,需要导入lombok下的@slf4
        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===================");

        //token为空,不放行
        if(StringUtils.isBlank(token)){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            //设置返回消息格式
            response.setContentType("application/json;charset=utf8");
            //返回json信息
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //token不为空,去做认证

        SysUser sysUser = loginService.checkToken(token);
        //用户不存在,即认证失败
        if(sysUser == null){
            System.out.println("没有该用户");
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //登陆验证成功,放行
        return true;
    }
}

配置将拦截器


这里拦截器默认拦截的/test,用于测试拦截器


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置拦截接口,此处配置为test,用于测试
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test");
    }

ThreadLocal引用

ThreadLocal简介:

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
?

本次项目引入ThreadLocal的目的是为实现在Controller层获得用户信息。
原理:
1)在登录拦截放行前将用户信息存入ThreadLocal中;
2)在Controller中获取用户信息;
3)在Controller方法执行完后删除用户信息;
其中
1)对应拦截器中preHandle方法和UserThreadLocal的set方法
2)对应UserThreadLocal的get方法
3)对应拦截器afterCompletion方法和UserThreadLocal的remove方法
所以要创建UserThreadLocal类
?

package com.komorebi.UserThreadLocal;

import com.komorebi.pojo.SysUser;

public class UserThreadLocal {
//    声明为私有,即每个线程有自己的ThreadLocal变量
    private UserThreadLocal(){}
//    实例化一个ThreadLocal的类,即启用
    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();
    }

}

UserThreadLocal中set方法在登录拦截器中应用,即登陆时set存入用户信息。

 //为了实现在Controller中获取user用户信息,我们使用ThreadLocal
        //将用户信息存入ThreadLocal中
        UserThreadLocal.put(sysUser);
        //登陆验证成功,放行
        return true;

UserThreadLocal中get方法在Controller中应用,即登陆后获取用户信息。

  public Result test(){
        //测试ThreadLocal
        SysUser sysUser = UserThreadLocal.get();
        System.out.println(sysUser);
        return Result.success(null);
    }

UserThreadLocal中remove方法在登录拦截器afterCompletion方法中应用,即Controller执行完后删除用户信息。

   //controller方法处理完之后的操作,要将ThreadLocal释放掉,否则会内存泄漏
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }

UserThreadLocal中remove方法在登录拦截器afterCompletion方法中应用,即Controller执行完后删除用户信息。

   //controller方法处理完之后的操作,要将ThreadLocal释放掉,否则会内存泄漏
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }

ThreadLocal(本地的线程)到底有什么用

比如我们发出一个请求,当你启动某一个进程的时候,你让他和你对应的进程进行绑定的话,会深入的绑定到一起(以达到绑定用户信息的目的)。
为什么在那个后面一定要删除,因为一旦内存泄漏是很严重的

?

一个线程可以存在多个ThreadLocal每一个Thread维护一个ThreadLocalMap,
key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
强引用,是最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
上面的那个key为使用弱引用的ThreadLocal实例,当我们的线程中的那个ThreadLocal被垃圾回收机制干掉之后,是不是这个弱引用的Key不存在了,但是这个是Map集合呀,Value会永远的存在,所有要手动的删除

?四、文章详情


内容及相关信息展示


该模块实现的主要功能是,点击文章标题进入到文章内容显示页面
请求链接为http://localhost:8080/articles/view/{id}
此时文章内容增加了分类(category)和内容(body)两个部分,所以ArticleVo代码要增加两个字段
ArticleVo

package com.komorebi.vo;

import lombok.Data;

import java.util.List;

@Data
public class ArticleVo {
    private Long id;
    private String title;
    private String summary;  //简介
    private int commentCounts;
    private int ViewCounts;
    private int weight;   //置顶
    private String createDate;  //创建时间
    private String author;
    private List<TagVo> tags;
    
	//内容属性
	private ArticleBodyVo body;
	//分类属性
    private CategoryVo category;
}

Articlecontroller

  //文章详情
    @PostMapping("/view/{id}")
    public Result findArticleById(@PathVariable("id") Long id){
        return articleService.findArticleById(id);
    }

findArticleById方法

   @Override
    public Result findArticleById(Long id) {
        /*
        * 1、根据id获得article对象
        * 2、根据bodyId和categoryId去做关联查询
        * */
        Article article = this.articleMapper.selectById(id);
        return Result.success(copy(article,true,true,true,true));
    }

copy函数改写
之前的copy函数只有三个参数,此时还要加两个参数内容分类

 private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);
        //joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        //是否显示标签和作者
        if(isTag){
            articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
        }
        if(isAuthor){
            articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
        }
        if(isBody){
            articleVo.setBody(findArticleBodyById(article.getBodyId()));
        }
        if(isCategory){
            articleVo.setCategory(categoryService.findCategoryById(article.getCategoryId()));
        }
        return articleVo;
    }

由于copy被修改,所以copyList也需要修改,未来不改变copyList原有代码,使用重载

//copyList实现,用于将Article列表转换为ArticleVo列表
    private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
        ArrayList<ArticleVo> articleVos = new ArrayList<>();
        for(Article article:records){
            articleVos.add(copy(article,isTag,isAuthor,false,false));
        }
        return articleVos;
    }

//重载
    private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory) {
        ArrayList<ArticleVo> articleVos = new ArrayList<>();
        for(Article article:records){
            articleVos.add(copy(article,isTag,isAuthor,isBody,isCategory));
        }
        return articleVos;
    }

copy方法中为articleVo设置了body和categories两个属性。
所以要实现两个方法,获取文章内容: findArticleBodyById
获取文章分类:findCategoryById
findArticleBodyById方法(该方法在ArticleServiceImpl中)
?

 //获得文章body内容
    private ArticleBodyVo findArticleBodyById(Long bodyId) {
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());
        return articleBodyVo;
    }

该方法的返回类型是ArticleBodyVo,所以要创建对应的ArticleBody和ArticleBodyVo类以及ArticleBodyMapper。

ArticleBodyMapper

@Repository
public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}

ArticleBody

package com.komorebi.pojo;

import lombok.Data;

//文章详情内容存放在ms_article_body表中,需要创建一个对应的实体类
@Data
public class ArticleBody {

    private Long id;
    private String content;
    private String contentHtml;
    private Long articleId;
}

ArticleBodyVo

package com.komorebi.vo;

import lombok.Data;

@Data
public class ArticleBodyVo {
    private String content;
}

findCategoryById是由CategoryMapper实现的,所以要创建CategoryMapper、Category、CategoryVo、CategoryService、CategoryServiceImpl。

CategoryMapper

@Repository
public interface CategoryMapper extends BaseMapper<Category> {
}

Category

package com.komorebi.pojo;

import lombok.Data;

@Data
public class Category {
    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

CategoryVo

package com.komorebi.vo;

import lombok.Data;

@Data
public class CategoryVo {
    private Long id;

    private String avatar;

    private String categoryName;

}

CategoryService

public interface CategoryService {
    CategoryVo findCategoryById(Long categoryId);
}

?CategoryServiceImpl

@Service
public class CategoryServiceImpl implements CategoryService{

    @Autowired
    CategoryMapper categoryMapper;
    @Override
    public CategoryVo findCategoryById(Long categoryId) {
        Category category = categoryMapper.selectById(categoryId);
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}

问题总结


由于数据库中id都是使用的Long,并且所有的Long在数据库中数值位数都设置为20,当数据以json形式传给前端时,前端JavaScript解析20位数的Long会出现溢出,因为JavaScript无法解析过长的数值。

原理:
后端使用64位存储长整数(long),最大支持9223372036854775807
前端的JavaScript使用53位来存放,最大支持9007199254740992,超过最大值的数,可能会出现问题(得到的溢出后的值)
解决方法:降低数据库中Long表示的Id字段的位数,由20降为15,并且将id的数值也修改为位数小于或等于15位的数值。
?

线程池实现更新评论数

查完文章了,新增阅读数,有没有问题呢?
答案是是有的,本应该直接返回数据,这时候做了一个更新操作,更新时间时加写锁,阻塞其他的读操作,新能就会比较低,
而且更新增加了此次接口的耗时,一旦更新出问题,不能影响我们其他的如:看文章呀什么的
那要怎么样去优化呢?,---->所有想到了线程池
可以把更新操作扔到线程池里面,就不会影响了,和主线程就不相关了

AOP日志

通过注解实现AOP。
Springboot 自定义注解+AOP
1)创建注解

package com.komorebi.common.aop;

import org.apache.catalina.startup.SetContextPropertiesRule;

import java.lang.annotation.*;

@Target({ElementType.METHOD})//使用在方法上的注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
	//注解的两个属性值
    String module() default "";
    String operator() default "";
}

1)AOP面向切面
实现日志输出
定义切入点:此处为自定义的注解
@Pointcut("@annotation(com.komorebi.common.aop.LogAnnotation)")
public void pt(){}
环绕通知:@Around(“pt()”)
return joinPoint.proceed(): 这个是从切点的环绕增强里面脱离出来
joinPoint.getSignature:获取被增强目标对象
getMethod:获得方法对象
getAnnotation:获得注解
?

package com.komorebi.common.aop;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component//注入到ioc容器
@Aspect//这是一个增强类
@Slf4j
public class LogAspect {
    @Pointcut("@annotation(com.komorebi.common.aop.LogAnnotation)")
    public void pt(){}

    //环绕通知
    @Around("pt()")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();
        //执行方法
        Object result = joinPoint.proceed();
        //执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        //保存日志
        recordLog(joinPoint, 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);
        log.info("excute time : {} ms",time);
        log.info("=====================log end================================");
    }

}

五、文章分类页面

展示所有的分类信息,并且点击进入相应的分类后,可以显示对应分类的文章。
CategoryController

//文章分类页面
    @GetMapping("/detail")
    public Result findAllDetail(){
        return categoryService.findAllDetail();
    }
    //某个分类对应的文章
    @GetMapping("/detail/{id}")
    public Result categoryDetailById(@PathVariable("id") Long id){
        return categoryService.categoryDetailById(id);
    }

因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。
ArticleServiceImpl

//文章分类页面,按照分类CategoryId展示文章
        if(pageParams.getCategoryId()!=null){
            //加入分类 条件查询
            wrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
        }

六、文章标签页面

展示所有的标签信息,并且点击进入相应的标签后,可以显示对应标签的文章。
TagController

//标签页面展示所有标签
    @GetMapping("/detail")
    public Result findAllDetail(){
        return tagService.findAllDetail();
    }
    //标签页面,某个标签对应的所有文章
    @GetMapping("/detail/{id}")
    public Result tagsDetailById(@PathVariable("id") Long id){
        return tagService.tagsDetailById(id);
    }

TagsServiceImpl

//文章标签页面获取所有标签
    @Override
    public Result findAllDetail() {
        List<Tag> tags = tagMapper.selectList(null);
        return Result.success(tags);
    }
//文章标签页面根据标签获取文章
    @Override
    public Result tagsDetailById(Long id) {
        Tag tag = tagMapper.selectById(id);
        return Result.success(tag);
    }

因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。
ArticleServiceImpl

//文章标签页面,按照分类TagId展示文章
        List<Long> articleIdList = new ArrayList<>();
        if(pageParams.getTagId()!=null){
            //加入标签 条件查询
            //article表中没有tag字段,因为一篇文章有多个标签
            //映射为一张新表article_tag :article_id 1:n tag_id
            //1、查询标签id对应的文章id列表
            LambdaQueryWrapper<ArticleTag> articleTagLambdaQueryWrapper = new LambdaQueryWrapper<>();
            articleTagLambdaQueryWrapper.eq(ArticleTag::getTagId,pageParams.getTagId());
            List<ArticleTag> articleTags = articleTagMapper.selectList(articleTagLambdaQueryWrapper);
            for (ArticleTag articleTag : articleTags){
                articleIdList.add(articleTag.getArticleId());
            }
            if(articleIdList.size()>0){
                wrapper.in(Article::getId,articleIdList);
            }
        }

七、文章归档页面

由于文章归档涉及到年,月的属性,所以PageParams类需要添加一些属性
PageParams

package com.komorebi.vo;

import lombok.Data;

@Data
public class PageParams {
    private int page = 1;
    private int pageSize = 10;
    private Long categoryId;
    private Long tagId;
    private String year;
    private String month;
    //将个位月数改为双位数,例如:6月-》06月
    public String getMonth(){
        if(this.month != null && this.month.length() ==1){
            return "0"+this.month;
        }
        return this.month;
    }
}

因为涉及到年月的计算,mybatis_plus不能实现,所以只能使用sql语句实现,又因为文章归档归根结底也是文章查询,只是添加了一些查询条件。
所以将文章查询整体修改为mapper.xml形式,即将以前通过mybatis_plus实现的文章查询注释掉。
ArticleMapper
?

package com.komorebi.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.komorebi.dos.Archives;
import com.komorebi.pojo.Article;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ArticleMapper extends BaseMapper<Article> {
    List<Archives> listArchives();
    IPage<Article> listArticle(Page<Article> page,
                              Long categoryId,
                              Long tagId,
                              String year,
                              String month);
}

ArticleMapper.xml

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.komorebi.mapper.ArticleMapper">
    <select id="listArchives" resultType="com.komorebi.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>
<!--    文章显示-->
    <select id="listArticle" resultType="com.komorebi.pojo.Article">
        select * from ms_article
        <where>
            1=1
            <if test="categoryId != null">
                and category_id=#{categoryId}
            </if>
            <if test="tagId != null">
                and tag_id=#{tagId}
            </if>
            <if test="year != null and year.length>0 and month!=null and month.length>0">
                and (From_UNIXTIME(create_date/1000,'%Y') = #{year} and From_UNIXTIME(create_date/1000,'%m') = #{month})
            </if>
        </where>
        order by weight desc,create_date desc
    </select>
</mapper>

八、统一缓存处理


登陆时我们将用户信息存入了缓存,可以提高响应速度,我们首页加载的东西每次都会去访问数据库,我们可以把它们都加入到缓存中,加快存取速度。
为什么用redis,因为redis是在内存中的,所以响应速度会很快。
如何在不改变原有代码的基础上,加入缓存呢!AOP。
创建Cache注解

package com.komorebi.common.aop;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
    //缓存过期时间
    long expire() default 1*60*1000;
    //名称
    String name() default "";
}

CacheAOP

package com.komorebi.common.aop;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.komorebi.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.Duration;

@Aspect
@Component
@Slf4j
public class CacheAspect {

    @Autowired
    RedisTemplate<String,String> redisTemplate;
    //切入点为注解Cache
    @Pointcut("@annotation(com.komorebi.common.aop.Cache)")
    public void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint){
        try{
            Signature signature = joinPoint.getSignature();
            //获得类名
            String className = joinPoint.getTarget().getClass().getSimpleName();
            //获得方法名
            String methodName = signature.getName();
            //存取方法参数类型
            Class[] parameterTypes = new Class[joinPoint.getArgs().length];
            //拿到参数
            Object[] args = joinPoint.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)){
                //md5参数加密,用于设置redis key
                params = DigestUtils.md5Hex(params);
            }
            //通过parameterTypes拿到对应的方法
            Method method = joinPoint.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
            //获取cache注解
            Cache annotation = method.getAnnotation(Cache.class);
            //获取过期时间
            long expire = annotation.expire();
            //缓存名称
            String name = annotation.name();

            //创建redis Key,保证key的唯一性
            String redisKey = name+"::"+className+"::"+methodName+"::"+params;
            //1、先从redis中获取要查询的信息
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            //如果redis中有
            if(StringUtils.isNotEmpty(redisValue)){
                log.info("走了缓存---,{},{}",className,methodName);
                return JSON.parseObject(redisValue,Result.class);
            }
            //2、redis中没有,访问查询方法,然后将结果存入redis
            //proceed()即代表执行了Controller中的方法,
            // 如果有返回值就返回,如果没有就不用返回,在这里有返回值,为文章信息
            Object proceed = joinPoint.proceed();
            //JSON.toJSONString将对象转为json字符串
            //JSON.parseObject将json字符串转为对象
            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,"系统错误");
    }
}

在想要添加缓存的方法上添加Cache注解

//分页显示文章列表
    @PostMapping
    @Cache(expire = 5*60*1000,name="listArticle")
    @LogAnnotation(module = "文章",operator = "获取文章列表")
    public Result listArticle(@RequestBody PageParams pageParams){
        return articleService.listArticle(pageParams);
    }

八、技术总结


1)jwt+redis:token令牌的登陆方式、访问速度快、安全性高,redis做了对token和用户信息的管理,用户登录做了缓存。
2)使用ThreadLocal保存用户信息,在请求的线程内可以直接获取用户信息,不需要再次查缓存或者数据库。
3)ThreadLocal使用结束后,做了value的删除,防止了内存泄漏。
4)线程池应用,对于文章浏览数的更新操作,将其加入线程池,异步任务实现浏览数量更新操作,提高页面响应速度,并且保证了线程安全。
5)AOP实现统一缓存处理,以项目首页内容为例,自定义注解Cache,以注解为切入点,第一次访问首页时,将首页信息存储在redis中,再次访问时,直接在redis中获取,无需再次查询。
6)AOP实现统一日志记录,自定义注解LogAnnotation,以注解为切入点,每次接口调用结束后台打印日志。
7)权限系统,通过Security实现认证和授权

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-10-08 20:27:04  更:2022-10-08 20:29:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年4日历 -2025/4/19 4:36:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码