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知识库 -> springboot后端博客项目实战 -> 正文阅读

[Java知识库]springboot后端博客项目实战

文章目录

Gitee地址 :文章些许混乱,以库为准😰

修改数据库ms_comment的articleId为bigInt,不然文章id会超出范围

前端项目运行命令

npm install
npm run build
npm run dev

创建一个项目,如果是平时开发的,首先都要导入如下依赖

<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-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

如果创建的项目报如下错误的话,请你降低SpringBoot版本

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-08-15 20:58:31.515 ERROR 4684 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties': Lookup method resolution failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:289) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1284) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1201) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.7.jar:5.3.7]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.7.jar:5.3.7]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.7.jar:5.3.7]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.5.0.jar:2.5.0]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758) [spring-boot-2.5.0.jar:2.5.0]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:438) [spring-boot-2.5.0.jar:2.5.0]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:337) [spring-boot-2.5.0.jar:2.5.0]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1336) [spring-boot-2.5.0.jar:2.5.0]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1325) [spring-boot-2.5.0.jar:2.5.0]
	at cn.mldn.admin.AdminApp.main(AdminApp.java:11) [classes/:na]
Caused by: java.lang.IllegalStateException: Failed to introspect Class [org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
	at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:481) ~[spring-core-5.3.7.jar:5.3.7]
	at org.springframework.util.ReflectionUtils.doWithLocalMethods(ReflectionUtils.java:321) ~[spring-core-5.3.7.jar:5.3.7]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:267) ~[spring-beans-5.3.7.jar:5.3.7]
	... 18 common frames omitted
Caused by: java.lang.NoClassDefFoundError: org/springframework/boot/sql/init/DatabaseInitializationMode
	at java.lang.Class.getDeclaredMethods0(Native Method) ~[na:1.8.0_241]
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) ~[na:1.8.0_241]
	at java.lang.Class.getDeclaredMethods(Class.java:1975) ~[na:1.8.0_241]
	at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:463) ~[spring-core-5.3.7.jar:5.3.7]
	... 20 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.sql.init.DatabaseInitializationMode
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_241]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418) ~[na:1.8.0_241]
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) ~[na:1.8.0_241]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ~[na:1.8.0_241]
	... 24 common frames omitted

一.资源下载和项目搭建

  1. 下载前端项目(这个的话,可以去QQ群下载

  2. 搭建项目(用idea创建项目)
    1)idea create new xxxx在里面一系列的操作
    2)导入依赖

    a -----:parent和properties的导入	```
    
<!--parent的解释https://blog.csdn.net/niceyoo/article/details/91852502-->
	<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>
b--导入其他的依赖
<?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.lum</groupId>
    <artifactId>blog-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>blog</module>
    </modules>
    <packaging>pom</packaging>
    <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>

    <dependencyManagement>
        <dependencies>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.76</version>
            </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>
            <!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>2.10.10</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 创建子模块blog-api
    1)为什么创建子模块:方便以后分模块开发
    2)导入依赖
<?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">
    <parent>
        <artifactId>blog-parent</artifactId>
        <groupId>com.lum</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>blog</artifactId>
    <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>
<!--这个就是AOP的问题了撒,不用多说-->
        <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>
<!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

<!--JSON的格式问题了-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
<!--mysql的操作了-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
<!--可以看到json中信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
<!--跟java.lang这个包的作用类似,Commons Lang这一组API也是提供一些基础的、通用的操作和处理,如自动生成toString()的结果、自动实现hashCode()equals()方法、数组操作、枚举、日期和时间的处理等等。-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
<!--StringUtils就是这个提供的,用来有时候验证什么是否为空呀-->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>
 <!--Md5加密呀-->        
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
<!--mybatis的配置-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
   <!--是关于Data注解的-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    <!--时间处理的类-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.10</version>
        </dependency>
  <!--验证-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
            <version>[7.7.0, 7.7.99]</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>commons-configuration</groupId>
            <artifactId>commons-configuration</artifactId>
            <version>1.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.8</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.3.8</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
    </dependencies>
</project>
  1. 编写application.properties文件
server.port=8888

#配置项目名称
spring.application.name=blog
server.servlet.context-path=/api
#数据库的设置
spring.datasource.url=jdbc:mysql://localhost:3306/blog?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

#Mybaties-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.table-prefix=ms_
  1. 编写启动类BlogApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BlogApplication {
   
     
     
    public static void main(String[] args) {
   
     
     
        SpringApplication.run(BlogApplication.class);
    }
}
  1. 然后因为引入了上面的Mybatis-plus,所有有如下配置
    在项目中创建config文件夹,然后创建MybatisplusConfig.java并且写如下代码:
package com.lum.blog.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.lum.blog.mapper")
public class MyBatiesPlusConfig {
   
     
     
}

pic_fcece0fa.png

  1. 启动BlogApplication测试
    pic_05788858.png
    成功即可,可能会失败,大多是maven依赖关系没有对,仔细比对!
  2. 在项目中,肯定要用到分页的,所有要用到Mybatis的分页插件。对MybatisPlusConfig做出如下修改:
//Mybatis-plus分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
   
     
     
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor( new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }

pic_c063c983.png

  1. 创建WebMmvConfig.java文件
    这里先配置跨域的问题,因为前端和后端是分离了的,前端前端端口访问是跨域的,所有要配置跨域的问题。
package com.lum.blog.config;

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 {
   
     
     
   //做跨域配置,为什么要做这个跨域的配置呢,因为比如:我前端的端口号是8080,而我后端接口是8888
   @Override
   public void addCorsMappings(CorsRegistry registry) {
   
     
     

       //addMapping就是所有的文件,allowedOrigins指的是可以那个地址可以访问
       registry.addMapping("/**").allowedOrigins("http://localhost:8080");
   }
}

😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉

二、功能

1.首页文章列表页- - -1

1.1接口说明

接口url:/articles
请求方式:post请求
请求参数:

参数名称参数类型说明
pageint当前页
pagesizeint每页显示的数量
返回数据:

~~~json
{
   
     
     
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
   
     
     
            "id": 1,
            "title": "springboot介绍以及入门案例",
            "summary": "通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。\r\n\r\n这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。",
            "commentCounts": 2,
            "viewCounts": 54,
            "weight": 1,
            "createDate": "2021-06-26 15:58",
            "author": "lum",
            "body": null,
            "tags": [
                {
   
     
     
                    "id": 5,
                    "avatar": null,
                    "tagName": "444"
                },
                {
   
     
     
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                },
                {
   
     
     
                    "id": 8,
                    "avatar": null,
                    "tagName": "11"
                }
            ],
            "categorys": null
        },
        {
   
     
     
            "id": 9,
            "title": "Vue.js 是什么",
            "summary": "Vue (读音 /vju?/,类似于 view) 是一套用于构建用户界面的渐进式框架。",
            "commentCounts": 0,
            "viewCounts": 3,
            "weight": 0,
            "createDate": "2609-06-27 11:25",
            "author": "12",
            "body": null,
            "tags": [
                {
   
     
     
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                }
            ],
            "categorys": null
        },
        {
   
     
     
            "id": 10,
            "title": "Element相关",
            "summary": "本节将介绍如何在项目中使用 Element。",
            "commentCounts": 0,
            "viewCounts": 3,
            "weight": 0,
            "createDate": "2609-06-27 11:25",
            "author": "12",
            "body": null,
            "tags": [
                {
   
     
     
                    "id": 5,
                    "avatar": null,
                    "tagName": "444"
                },
                {
   
     
     
                    "id": 6,
                    "avatar": null,
                    "tagName": "33"
                },
                {
   
     
     
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                },
                {
   
     
     
                    "id": 8,
                    "avatar": null,
                    "tagName": "11"
                }
            ],
            "categorys": null
        }
    ]
}

1.2表结构
既然我们有了前端和页面要返回的数据,那我们的用户什么的,肯定都要和类有关系

返回数据的文章数据表
~~~sql
CREATE TABLE `blog`.`ms_article`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `comment_counts` int(0) NULL DEFAULT NULL COMMENT '评论数量',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT '创建时间',
  `summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
  `title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',
  `view_counts` int(0) NULL DEFAULT NULL COMMENT '浏览数量',
  `weight` int(0) NOT NULL COMMENT '是否置顶',
  `author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id',
  `body_id` bigint(0) NULL DEFAULT NULL COMMENT '内容id',
  `category_id` int(0) NULL DEFAULT NULL COMMENT '类别id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
~~~

//标签表,由文章可以查看其他的
~~~sql
CREATE TABLE `blog`.`ms_tag`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `article_id` bigint(0) NOT NULL,
  `tag_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE,
  INDEX `tag_id`(`tag_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
~~~

//用户数据表
~~~sql
CREATE TABLE `blog`.`ms_sys_user`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号',
  `admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理员',
  `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT '注册时间',
  `deleted` bit(1) NULL DEFAULT NULL COMMENT '是否删除',
  `email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登录时间',
  `mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
  `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密盐',
  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

1.3 Dao开发
com.lum.blog.dao.pojo.Article.java

package com.lum.blog.dao.pojo;

import lombok.Data;
/*
博客管理
 */
@Data
public class Article {
   
     
     

    public static final int Article_TOP = 1;

    public static final int Article_Common = 0;

    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    /**
     * 作者id
     */
    private Long authorId;
    /**
     * 内容id
     */
    private Long bodyId;
    /**
     *类别id
     */
    private Long categoryId;

    /**
     * 置顶
     */
    private int weight = Article_Common;


    /**
     * 创建时间
     */
    private Long createDate;
}

com.lum.blog.dao.pojo.SysUser.java

package com.lum.blog.dao.pojo;
import lombok.Data;

/*
用户管理
 */
@Data
public class SysUser {
   
     
     

    private Long id;

    private String account;

    private Integer admin;

    private String avatar;

    private Long createDate;

    private Integer deleted;

    private String email;

    private Long lastLogin;

    private String mobilePhoneNumber;

    private String nickname;

    private String password;

    private String salt;

    private String status;
}

com.lum.blog.dao.pojo.Tag.java

package com.lum.blog.dao.pojo;

import lombok.Data;
/*
标签管理
 */
@Data
public class Tag {
   
     
     

    private Long id;

    private String avatar;

    private String tagName;

}

1.4对应的Mapper的创建

package com.lum.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lum.blog.dao.pojo.Article;


public interface ArticleMapper extends BaseMapper<Article> {
   
     
     

}

*******************************************************************

package com.lum.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lum.blog.dao.pojo.SysUser;

public interface SysUserMapper extends BaseMapper<SysUser> {
   
     
     
}


*******************************************************************



package com.lum.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lum.blog.dao.pojo.Tag;

public interface TagMapper extends BaseMapper<Tag> {
   
     
     
}

1.5mapper创建好了,该Controller了

创建ArticleController,这个代表的是文章类的控制器
package com.lum.blog.controller;

import com.lum.blog.vo.Result;
import com.lum.blog.vo.params.PageParams;
import org.springframework.web.bind.annotation.*;

//使用json数据进行交互
@RestController
@RequestMapping("articles")
public class ArticleController {
   
     
     

        //为什么用post请求,因为前面的接口说明post请求
        //对于另外的参数,建立vo下面的PageParams类专门表示参数

        /**
         * 首页文章列表
         * @param pageParams
         * return 返回承担返回数据Result的类
         */
        @PostMapping
        public Result listArticle(@RequestBody PageParams pageParams){
   
     
     
            //对于接受的参数问题,这里是RequestBody接收
            return articleService.listArticle(pageParams);
        }

}

1)这里就有补充了,我们传入的参数,是PageParms类代表,在
com.lum.Blog下面创建vo的目录,然后创建PageParms的类

package com.lum.blog.vo.params;

import lombok.Data;

//承担着返回页数和数量的类
@Data
public class PageParams {
   
     
     
	private int page = 1;

	private int pageSize = 10;
}

2)返回的数据Result的类

package com.lum.blog.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

//承担着返回首页文章列表的类
@Data
@AllArgsConstructor
@NoArgsConstructor
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) {
   
     
     
        //没有数据可以返回,所有data是null
        return new Result(false , code, msg,null);
    }
}

1)首先我们编写的DAO层和数据库里面的表名字要对应起来。
2)然后就是编写的XXX-Mapper层层要继承BaseMapper<xxx类名>。
3)然后就是我们一般不再编写他的xxxMapper.xml而是要配置的是 xxxService+xxxServiceImpl,如果要编写xxxMapper.xml和Mybatis一样的配置

1.6 service
从上面中,我们就可以看出,我们已经开发差不多了,但是想返回数据了,但是listArticle这个方法却没有Service来读取数据,所以来开发Service和数据读取的方法

1)首先来编写这个Service层,在src/main/java下面建立service文件夹并且在下面ArticleService文件和Impl文件夹

package com.lum.blog.service;

import com.lum.blog.vo.Result;
import com.lum.blog.vo.params.PageParams;

public interface ArticleService {
   
     
     
    /**
     * 分页查询 文章列表
     * @param pageParams
     * @return
     */
    Result listArticle(PageParams pageParams);
}

2)再来编写他的Impl文件ArticleServiceImpl文件

package com.lum.blog.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lum.blog.dao.mapper.ArticleMapper;
import com.lum.blog.dao.pojo.Article;
import com.lum.blog.service.ArticleService;
import com.lum.blog.vo.ArticleVo;
import com.lum.blog.vo.Result;
import com.lum.blog.vo.params.PageParams;
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
    private ArticleMapper articleMapper;

    @Override
    public Result listArticle(PageParams pageParams) {
   
     
     
        //1. 这个是分页查询的类(代表着分离模式),要传入的是页面的页数和页面总数
        Page<Article> page = new Page<Article>(pageParams.getPage(),pageParams.getPageSize());

        //2. LambdaQueryWrapper是MybatisPlus提供的,需要就导入这个包就可以了
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();

        //3. 这里是根据字体排序
        //queryWrapper.orderByDesc(Article::getWeight);
        //4. 这里设置的是根据时间排序
        //queryWrapper.orderByDesc(Article::getCreateDate);
        //5. 这个方法    default Children orderByDesc(boolean condition, R column, R... columns) {是可变长参数的
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

        // 因为articleMapper继承了BaseMapper了的,所有设查询的参数和查询出来的排序方式
        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);

        //这个就是我们查询出来的数据的数组了
        List<Article> records = articlePage.getRecords();

        //因为页面展示出来的数据不一定和数据库一样,所有我们要做一个转换
       //将在查出数据库的数组复制到articleVo中实现解耦合,vo和页面数据交互
        List<ArticleVo> articleVoList = copyList(records);

        return Result.success(articleVoList);
    }

    private List<ArticleVo> copyList(List<Article> records) {
   
     
     
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
   
     
     
            articleVoList.add(copy(record));
        }
        return articleVoList;
    }

    //这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,想Article和articlevo相同属性的拷贝过来返回
    private ArticleVo copy(Article article) {
   
     
     
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);

        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        return articleVo;
    }
}

其中需要创建ArticleVo和TagVo

package com.lum.blog.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 List<CategoryVo> categories;
}
package com.lum.blog.vo;

import lombok.Data;

@Data
public class TagVo {
   
     
     

    private Long id;

    private String tagName;
}

vo和页面交互的数据不应该和数据库映射对象进行耦合,最好分开

这里补充一下这个articleVo,这个类,因为平时我们开发出来的东西,到时候要
到数据库里面找数据嘛,然后找出来不一定一样,要把它一样的拷贝,不一样的返回null

总结:有了以上的配置之后,页面的内容就可以展示了

pic_712f0584.png

2. 首页文章列表页----2

问题引入:在之前开发的首页内容显示中,文章下面是没有标签,作者的信息等内容的,要开发下面有内容
pic_9470360e.png

2.1在ArticleServiceImpl中实现
思考:并不是所有的接口都需要标签,作者信息

增加两个boolean isTag,isAuthor来进行判断

在copyList中增加代码

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

在copy中增加代码

//这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,想Article和articleVo相同属性的拷贝过来返回
    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor) {
   
     
     
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);

        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        /*
        并不是所有的接口都需要标签,作者信息
        增加两个参数boolean isTag,boolean isAuthor
         */
		//需要开发tagService
        if (isTag) {
   
     
     
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        //需要开发authorService
        if (isAuthor) {
   
     
     
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        return articleVo;
    }

2.2标签tag
编写TagService

package com.lum.blog.service;

import com.lum.blog.vo.TagVo;

import java.util.List;

public interface TagService {
   
     
     

    List<TagVo> findTagsByArticleId(Long articleId);
}

编写实现类TagServiceImpl

package com.lum.blog.service.Impl;

import com.lum.blog.dao.mapper.TagMapper;
import com.lum.blog.dao.pojo.Tag;
import com.lum.blog.service.TagService;
import com.lum.blog.vo.TagVo;
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 TagServiceImpl implements TagService {
   
     
     

    @Autowired
     private TagMapper tagMapper;

    /*copyList传递tag*/
    public TagVo copy(Tag tag){
   
     
     
        TagVo tagVo = new TagVo();
        BeanUtils.copyProperties(tag,tagVo);
        return tagVo;
    }

    public List<TagVo> copyList(List<Tag> tagList) {
   
     
     
        List<TagVo> tagVoList = new ArrayList<>();
        for (Tag tag : tagList) {
   
     
     
            tagVoList.add(copy(tag));
        }
        return tagVoList;
    }
/**********************************/
    @Override
    public List<TagVo> findTagsByArticleId(Long articleId) {
   
     
     
        /* MyBatisPlus无法实现多表查询 */
        List<Tag> tags=tagMapper.findTagsByArticleId(articleId);
        return copyList(tags);
    }
}

编写TagMapper

package com.lum.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lum.blog.dao.pojo.Tag;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface TagMapper extends BaseMapper<Tag> {
   
     
     
    /**
     * 根据文章id查询标签列表
     * @param articleId
     * @return
     */
    List<Tag> findTagsByArticleId(Long articleId);
}

在resourse下建立TagMapper.xml
路径与接口包一致(com.lum.blog.dao.mapper)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.lum.blog.dao.mapper.TagMapper">

<!--    List<Tag> findTagsByArticleId(Long articleId);-->
    <select id="findTagsByArticleId" parameterType="long" resultType="com.lum.blog.dao.pojo.Tag">
        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>

parameterType="long"对应List findTagsByArticleId(Long articleId)的articleId

select id,avatar,tag_name as tagName from ms_tag
where id in
(select tag_id from ms_article_tag where article_id=#{articleId})
在关联表中查询标签的id,auatar,tagName

可以在application.properties中mybatis-plus开启驼峰命名

mybatis-plus.configuration.map-underscore-to-camel-case=true

这样SQL语句就不需要as别名。


接下来将TagService注入到ArticleImpl中实现

if (isTag) {
   
     
     
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }

2.3作者author
建立接口SysUserService

package com.lum.blog.service;

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

public interface SysUserService {
   
     
     

    SysUser findUserById(Long id);


}

编写实现类SysUserServiceImpl

package com.lum.blog.service.Impl;

import com.lum.blog.dao.mapper.SysUserMapper;
import com.lum.blog.dao.pojo.SysUser;
import com.lum.blog.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SysUserServiceImpl implements SysUserService {
   
     
     

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public SysUser findUserById(Long id) {
   
     
     
        /*防止出现id为空的情况*/
        SysUser sysUser = sysUserMapper.selectById(id);
        if (sysUser == null) {
   
     
     
            sysUser = new SysUser();
            sysUser.setNickname("鹿鸣");

        }
        return sysUser;
    }

}

注入SysUserMapper,然后编写查询

将SysUserService注入到文章实现类ArticleServiceImpl中

if (isAuthor) {
   
     
     
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }

在SysUserImpl中编写如果出现空的情况处理办法

public SysUser findUserById(Long id) {
   
     
     
        /*防止出现id为空的情况*/
        SysUser sysUser = sysUserMapper.selectById(id);
        if (sysUser == null) {
   
     
     
            sysUser = new SysUser();
            sysUser.setNickname("lum");

        }
        return sysUser;
    }

在copyList中加入返回值istag,isauthor

List<ArticleVo> articleVoList = copyList(records,true,true);
for (Article record : records) {
   
     
     
            articleVoList.add(copy(record,isTag,isAuthor));
        }

在前方代码中已经有体现在哪加😁

进行测试
pic_30bffe65.png

3.首页-最热标签

1.标签所拥有的文章数量最多
    2.查询 根据tag_id分组,技术,从大到小排,取前limit个

3.1 接口说明
接口url:/tag/hot
请求方式:Get
请求参数:无
返回数据:

{
   
     
     
	"successs":true
	"code"200
	"msg":"success"
	"data"[
		{
   
     
     
		"id":1,
		"tagName":"最热"
		}
	]
}

3.2编码
先写controller
创建TagsController

package com.lum.blog.controller;

import com.lum.blog.service.TagService;
import com.lum.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @Autowired
    private TagService tagService;

    @GetMapping("hot")
    public Result hot() {
   
     
     
        int limit =6;  //最热6个
        return tagService.hots(limit);
    }
}

在tagService实现hots方法

package com.lum.blog.service;

import com.lum.blog.vo.Result;
import com.lum.blog.vo.TagVo;

import java.util.List;

public interface TagService {
   
     
     

    List<TagVo> findTagsByArticleId(Long articleId);

    Result hots(int limit);
}

首先分析SQL语句

select tag_id
from ms_article_tag
group by tag_id
order by count(*) limit 2

TagServiceImpl实现 Result hots(int limit)

@Override
    public Result hots(int limit) {
   
     
     
        /**
         * 1.标签所拥有的文章数量最多
         * 2.查询 根据tag_id分组,技术,从大到小排序,取前limit个
         */
        List<Long> tagIds= tagMapper.findHotsTagId(limit);
        return null;
    }

在TagMapper中创建findHostTagId(limit)方法

/**
     * 查询最热标签前limit条
     * @param limit
     * @return
     */
    List<Long> findHotsTagId(int limit);

在资源文件TagMapper中生成findHotsTagId并添加==parameterType=“int”==属性以及sql语句

<select id="findHotsTagId" parameterType="int" resultType="java.lang.Long">
        select tag_id
        from ms_article_tag
        group by tag_id
        order by count(*) limit 6
    </select>

这时查询出最热的tagId,需要根据tagid查询tagName,Tag
继续Result hot

@Override
    public Result hots(int limit) {
   
     
     
        /**
         * 1.标签所拥有的文章数量最多
         * 2.查询 根据tag_id分组,技术,从大到小排序,取前limit个
         */
        List<Long> tagIds= tagMapper.findHotsTagId(limit);

        /*判断tagIds是否为空*/
        if(CollectionUtils.isEmpty(tagIds)){
   
     
     
            return Result.success(Collections.emptyList());
        }
//        需求的是tagId和tagName tag对象
//        select * from tag where id in (1,2,3)
        List<Tag> tagList= tagMapper.findTagsByIds(tagIds);
        return Result.success(tagList);
    }

所以需要在TagMapper.java中添加方法

/**
     * 根据tagId查询Tag对象
     * @param tagIds
     * @return
     */
    List<Tag> findTagsByIds(List<Long> tagIds);

在资源TagMapper.xml中添加

<!--    List<Tag> findTagsByIds(List<Long> tagIds);-->
    <select id="findTagsByIds" parameterType="list" resultType="com.lum.blog.dao.pojo.Tag">
        select id,tag_name from ms_tag
        where id in
        <foreach collection="tagIds" item="tagId" separator="," open="(" close=")">
            #{tagId}
        </foreach>
    </select>

3.3进行测试
pic_f8657181.png

4.统一异常处理

创建handler(com.lum.blog下.)包并创建AllExceptionHandler类

package com.lum.blog.handler;


import com.lum.blog.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
//对加了@ControllerAdvice的方法进行拦截处理 AOP实现
public class AllExceptionHandler {
   
     
     

    @ExceptionHandler(Exception.class)   //进行异常处理,处理Exception.class异常
    @ResponseBody                        //返回Json数据
    public Result doExceptionHandler(Exception e){
   
     
     
        e.printStackTrace();
        return Result.fail(-999,"系统异常");
    }
}

在controller代码中加入错误代码,进行测试

pic_dfc3e9a0.png

5.首页-最热文章

5.1接口说明
接口url: /articles/hot
请求方式:POST
请求参数:

返回数据:

{
   
     
     
  "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
   
     
     
            "id": 1,
            "title": "springboot介绍以及入门案例",
         },
         {
   
     
     
           "id": 2,
            "title": "springboot介绍以及入门案例",
         }  
            ]
}

5.2 Controller
在ArticleController中增加PostMapper,返回最热的文章

/**
     * 首页最热文章
     * @return
     */
    @PostMapping("hot")
    public Result hotArticle(){
   
     
     
        int limit=5;
        return articleService.hotArticle(limit);
    }

5.3Service

在articleService创建对应的方法.hotArticle(limit);

Result hotArticle(int limit);

在articleServiceImpl中实现 Result hotArticle(int limit) 进行sql查询并copyList返回

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

        return Result.success(copyList(articles,false,false));
    }

5.4进行测试
pic_e9159279.png

6.首页-最新文章

6.1接口说明
接口url: /articles/new
请求方式:POST
请求参数:

返回数据:

{
   
     
     
  "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
   
     
     
            "id": 1,
            "title": "springboot介绍以及入门案例",
         },
         {
   
     
     
           "id": 2,
            "title": "springboot介绍以及入门案例",
         }  
            ]
}

6.2 Controller
在ArticleController中增加PostMapper,返回最热的文章

/**
     * 首页最热文章
     * @return
     */
    @PostMapping("new")
    public Result newArticle(){
   
     
     
        int limit=5;
        return articleService.newArticle(limit);
    }

6.3Service

在articleService创建对应的方法.hotArticle(limit);

Result newArticle(int limit);

在articleServiceImpl中实现 Result hotArticle(int limit) 进行sql查询并copyList返回

@Override
    public Result newArticle(int limit) {
   
     
     
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getCreateDate);
        queryWrapper.select(Article::getId,Article::getTitle);
        queryWrapper.last("limit"+limit);
        //select id,title from article order by CreateDate desc limit 5
        List<Article> articles = articleMapper.selectList(queryWrapper);

        return Result.success(copyList(articles,false,false));
    }

6.4 进行测试
pic_0b06c5ca.png

7.首页-文章归档

文章根据日期的年月进行归档处理
因为数据库的create_date为bigint类型需要/1000得到时间戳,再经过FROM_UNIXTIME进行格式转换
SQL语句为

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

7.1 接口说明
接口url: /articles/listArtchives
请求方式:POST
请求参数:

返回数据:

{
   
     
     
  "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
   
     
     
            "year": "2021",
            "mouth": "6",
            "count"2
         }
            ]
}

7.2 Controller
ArticleController类

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

7.3 Service

ArticleService

/**
     * 文章归档
     * @return
     */
    Result listArchives();

在ArticleServiceImpl中实现

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

7.4 mapper

因为文章归档返回的数据不是数据库的直接数据,临时使用,不属于pojo对象
所以创建dos包存放非持久化数据
pic_4436bebe.png
创建Archives类归档信息

package com.lum.blog.dao.dos;

import lombok.Data;

@Data
public class Archives {
   
     
     
    private Integer year;

    private Integer month;

    private Long count;
}

创建Articlemapper

package com.lum.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lum.blog.dao.dos.Archives;
import com.lum.blog.dao.pojo.Article;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
   
     
     

    List<Archives> listArchives();
}

在资源包mapper创建ArticleMapper.xml实现listArchives()

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

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

7.5 进行测试
pic_64db9baa.png

8 登录功能的实现(JWT)

8.1接口说明
接口url:/login

请求方式:POST

请求参数:

参数名称参数类型说明
accountstring账号
passwordstring密码

返回的数据

{
   
     
     
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

8.2JWT技术实现
JSON Web Token (JWT),它是目前最流行的跨域身份验证解决方案

JWT的精髓在于:“去中心化”,数据是保存在客户端的。

jwt可以生成一个加密token,作为用户登陆的令牌,当用户登陆成功后,发放给客户端.
请求需要登陆的资源和接口时,将token携带,后端验证token是否合法.

jwt有三部分组成:

  • Header,{“type”:“JWT”,“alg”:“HS256”}固定

  • playload,存放自定义信息 比如,用户id,过期时间等,可以被解密,不能存放敏感信息

  • 签证 前两点加上密钥加密组成,只要密钥不丢失,可以认为是安全的

    (alg:HS256是算法)
    

jwt验证,主要就是验证签证部分,是否合法


导入依赖包

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

创建工具类JWTUtils

package com.lum.blog.utils;

import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTUtils {
   
     
     
    private static final String jwtToken = "12345Lum!@#$%";			//密钥

    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  *1000));  //一天都有效时间
        return jwtBuilder.compact();
    }

    public static Map<String,Object> checkToken(String token){
   
     
     
        try {
   
     
     
            Jwt parser = Jwts.parser().setSigningKey(jwtToken).parse(token);  //解析jwtToken
            return (Map<String, Object>) parser.getBody();
        }catch ( Exception e) {
   
     
     
            e.printStackTrace();
        }
        return null;
    }


    /*测试jwt*/
    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"));
    }
}

8.3Controller
在controller层创建LoginController进行登陆控制

package com.lum.blog.controller;

import com.lum.blog.service.LoginService;
import com.lum.blog.vo.Result;
import com.lum.blog.vo.params.LoginParam;
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 SysUserService sysUserService;
//    不建议,每个Service都有单独的业务

    @Autowired
    private LoginService loginService;

    @PostMapping
    public Result login(@RequestBody LoginParam loginParam){
   
     
     
        //登陆验证用户 访问用户表
        return loginService.login(loginParam);

    }

}

需要在Service层编写业务

8.4Service

创建LoginService编写业务

package com.lum.blog.service;

import com.lum.blog.vo.Result;
import com.lum.blog.vo.params.LoginParam;

public interface LoginService {
   
     
     

    /**
     * 登陆功能
     * @param loginParam
     * @return
     */
    Result login(LoginParam loginParam);
}

在vo包的param参数包编写登录用到的参数登录参数

在LoginServiceImpl中实现login方法

package com.lum.blog.service.Impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.lum.blog.dao.pojo.SysUser;
import com.lum.blog.service.LoginService;
import com.lum.blog.service.SysUserService;
import com.lum.blog.utils.JWTUtils;
import com.lum.blog.vo.ErrorCode;
import com.lum.blog.vo.Result;
import com.lum.blog.vo.params.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class LoginServiceImpl implements LoginService {
   
     
     

    @Autowired
    private SysUserService sysUserService;//需要用到用户表

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    private static final String salt="lum!@#";


    @Override
    public Result login(LoginParam loginParam) {
   
     
     

        /**
         * 1.检查参数是否合法
         * 2.根据用户名和密码区user表中查询是否存在
         * 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());
        }
        password = DigestUtils.md5Hex(password + salt); //密码加盐
        SysUser sysUser = sysUserService.findUser(account,password);
        if (sysUser == null) {
   
     
     
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(), ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }
        String token = JWTUtils.createToken(sysUser.getId());
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS); //过期时间
        return Result.success(token);
    }
}

在这里先判断用户名或者密码是否为空,空的话返回统一错误码

if (StringUtils.isBlank(account)||StringUtils.isBlank(password)) {
   
     
     
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }

再对密码进行加盐处理

private static final String salt="lum!@#";

password = DigestUtils.md5Hex(password + salt); //密码加盐

需要用到redis来作缓存和数据库的中介
进行application.properties中redis配置
设置token和过期时间

redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);

8.5,登录参数,redis配置,统一错误码

LoginParam登录参数

package com.lum.blog.vo.params;

import lombok.Data;

@Data
public class LoginParam {
   
     
     

    private String account;

    private String password;
}

redis配置

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

统一错误码

在vo包下创建ErrorCode

package com.lum.blog.vo;

public enum ErrorCode {
   
     
     

    PARAMS_ERROR(10001,"参数有误"),
    ACCOUNT_PWD_NOT_EXIST(10002,"用户密码不存在喔!"),
    TOKEN_ERROR(10003,"Token不合法"),
    ACCOUNT_EXIST(10004,"账号已存在"),
    NO_PERMISSION(70001,"无访问权限"),
    SESSION_TIME_OUT(90001,"会话超时"),
    NO_LOGIN(90002,"未登录"),;


    private int code;
    private String msg;

    ErrorCode(int code, String msg) {
   
     
     
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
   
     
     
        return code;
    }

    public void setCode(int code) {
   
     
     
        this.code = code;
    }

    public String getMsg() {
   
     
     
        return msg;
    }

    public void setMsg(String msg) {
   
     
     
        this.msg = msg;
    }
}

8.6 进行测试

使用postman进行测试,因为登录后,需要跳转页面,进行token认证,有接口未完成,换端会出问题,
token前端获取到之后,会存储storage中b5,本地存储

pic_88a8b0da.png

redis-ci中使用 key * 也可查询token

pic_c54f3748.png

9.登录后获取用户信息

9.1接口说明
接口url:/users/currentUser
请求方式:Get
请求参数:

参数名称参数类型说明
AuthorZationstring头部信息(Token)

9.2 Controller
创建UserController进行用户信息返回

package com.lum.blog.controller;


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

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

    @Autowired
    private SysUserService sysUserService;

    //users/currentUser
    @GetMapping("currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token) {
   
     
       //请求头

        return sysUserService.findUserByToken(token);
    }


}

9.3 Service
在SysUserService接口添加findUserByToken(String token)方法

/*根据token查询用户信息*/
    Result findUserByToken(String token);

在实现类中编写findUserByToken(String token),需要在vo包创建LoginUserVo

@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());
        }
        //理论上应该写TokenService处理,此处简单处理
        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setId(sysUser.getId());
        loginUserVo.setNickname(sysUser.getNickname());
        loginUserVo.setAccount(sysUser.getAccount());
        loginUserVo.setAvatar(sysUser.getAvatar());
        return Result.success(loginUserVo);
    }

LoginUserVo

package com.lum.blog.vo;

import lombok.Data;

@Data
public class LoginUserVo {
   
     
     

    private  Long id;

    private String account;

    private String nickname;

    private String avatar; //头像
}

编写LoginService接口添加checkToken检查Token是否为空,不为空继续判断redis中userJson是否为空,不为空返回user对象

SysUser checkToken(String token);

在LoginServiceImpl中实现方法checkToken

@Override
    public SysUser checkToken(String token) {
   
     
     
        if (StringUtils.isBlank(token)){
   
     
     
            return null;
        }
        Map<String, Object> checkToken = JWTUtils.checkToken(token);
        if (checkToken == 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;
    }

在SysUserServiceImpl中添加查找用户

@Override
    public SysUser findUser(String account, String password) {
   
     
     
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getAccount,account);
        queryWrapper.eq(SysUser::getPassword,password);//进行加密
        queryWrapper.select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname);//查询需要的
        queryWrapper.last("limit 1");//为了加快速度,只查一个

        return sysUserMapper.selectOne(queryWrapper);
    }

9.4进行测试
pic_b7e1c7fa.png

10 退出登录

前端清除token,后端清除redis中数据
10.1接口说明
接口url:/logout
请求方式:Get
请求参数:

参数名称参数类型说明
AuthorZationstring头部信息(Token)

10.2 Controller
创建LogoutController

package com.lum.blog.controller;

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

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

    @Autowired
    private LoginService loginService;

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

    }

}

10.3Service
在LoginService接口中创建logout方法

/**
     * 退出登录
     * @param token
     * @return
     */
    Result logout(String token);

在实现类中完成方法
将redis中的token删除即可

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

11.注册

sso(single sign on) 单点登录,后期把登录注册提出去,单独服务,可以独立提供接口

11.1接口说明
接口url:/register

请求方式:POST

请求参数:

参数名称参数类型说明
accountstring账号
passwordstring密码
nicknamestring昵称

返回的数据

{
   
     
     
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

11.2 Controller
创建RegisterController

package com.lum.blog.controller;

import com.lum.blog.service.LoginService;
import com.lum.blog.vo.Result;
import com.lum.blog.vo.params.LoginParam;
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("register")
public class RegisterController {
   
     
     


    @Autowired
    private LoginService loginService;

    @PostMapping
    public Result register(@RequestBody LoginParam loginParam) {
   
     
     

        return loginService.register(loginParam);
    }
}

11.3 Service
LoginParam增加属性nickname


在LoginService接口中增加方法

private String nickname;

  /**
     * 注册
     * @param loginParam
     * @return
     */
    Result register(LoginParam loginParam)

在LoginServiceImpl中实现register方法

@Override
    public Result register(LoginParam loginParam) {
   
     
     
        /**
         * 1.判断参数是否合法
         * 2.判断账户是否存在
         * 3.账户不存在注册用户
         * 4.生成token
         * 5.存入redis并返回
         * 6.注意  加上事务,一旦中间出现任何问题,注册用户需要回滚
         */
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        String nickname = loginParam.getNickname();
        if (StringUtils.isBlank(account)
                ||StringUtils.isBlank(nickname)
                ||StringUtils.isBlank(password)
        ){
   
     
     
            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(), "账号已经被用了欧");
        }

        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.createToken(sysUser.getId());
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
        return Result.success(token);

    }

因为需要查找账户和将账户写入数据库,需要在SysUserService新增方法

/**
     * 根据账户名查询用户
     * @param account
     * @return
     */
    SysUser findUserByAccount(String account);

    /**
     * 保存用户
     * @param sysUser
     */
    void save(SysUser sysUser);

实现findUserByAccount(String account)

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

实现save(SysUser sysUser)

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

    }

开启数据库事务
在SysUserServiceImpl开启 @Transactional

11.4进行测试
pic_93c38715.png
pic_07768804.png

pic_9c692400.png

12 登录拦截器

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

1. 拦截器的实现有多种方式(SpringSecurity,继承HandlerInterceptor,还有shiro
	都可以实现),这里我们选择的是继承HandlerInterceptor
2. 这种方式有两步,第一编写继承类,第二步在webMVC里面配置即可

12.1 拦截器实现

package com.lum.blog.handler;

import com.alibaba.fastjson.JSON;
import com.lum.blog.dao.pojo.SysUser;
import com.lum.blog.service.LoginService;
import com.lum.blog.vo.ErrorCode;
import com.lum.blog.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
    private LoginService loginService;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
     
     
        //在执行Controller方法(Handle)前进行执行
        // pre代表什么什么之前的
        /**
         * 1.需要判断,请求的接口路径是否为HandleMethod(controller方法)
         * 2.判断token是否为空,为空未登录
         * 3.不为空,登录验证 loginService  checkToken
         * 4.如果认证成功放行
         */

        if (!(handler instanceof HandlerMethod)) {
   
     
     
            //说简单的就是Handler是controller里面的某一个方法
            //handle 可能是  RequestResourceHandle(访问资源handle)  springboot程序访问静态资源  默认去classpath下的static目录查询
            return true;
        }
        得去拿Token,为什么这样呢,因为前端传东西过来的时候是,我们用@RequestHeader("Authorization") 传过来的
        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");
            response.getWriter().print(JSON.toJSONString(result)); //返回json信息(fastjson进行转化)
            return false;
        }
        /*用户为空,拦截*/
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
   
     
     
            //以下是错误返回的信息
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(),ErrorCode.NO_LOGIN.getMsg());
            //要告诉浏览器我们要返回的是这种类型
            response.setContentType("application/json;charset=utf8");
            //返回的东西是result类型,要转换为JSON类型才行
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }

        //登录验证成功,放行
        return true;
    }
}

12.2 配置拦截路径
WebMvcConfig中配置拦截路径

//拦截器注入
@Autowired
    private LoginInterceptor loginInterceptor;
@Override
    public void addInterceptors(InterceptorRegistry registry) {
   
     
     
        //配置拦截接口
//        除了登录注册的所有接口
//        registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login").excludePathPatterns("/register");
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test");
    }

pic_6f2e7233.png

13. ThreadLocal保存用户信息

想在controller中直接获取用户信息怎么获取?

什么是ThreadLocal

**ThreadLocal是什么**

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富:

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离

3、进行事务操作,用于存储线程事务信息。

4、数据库连接,Session会话管理。

13.1使用ThreadLocal
现在utils包下创建UserThreadLocal类

package com.lum.blog.utils;

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

public class UserThreadLocal {
   
     
     
    //这句话的意思是声明为私有
    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();
    }

}

既然是保存用户信息
对LoginIntercept修改,既然在这里验证,就在这里进行添加
pic_09637127.png

package com.lum.blog.handler;

import com.alibaba.fastjson.JSON;
import com.lum.blog.dao.pojo.SysUser;
import com.lum.blog.service.LoginService;
import com.lum.blog.utils.UserThreadLocal;
import com.lum.blog.vo.ErrorCode;
import com.lum.blog.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
    private LoginService loginService;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
     
     
        //在执行Controller方法(Handle)前进行执行
        // pre代表什么什么之前的
        /**
         * 1.需要判断,请求的接口路径是否为HandleMethod(controller方法)
         * 2.判断token是否为空,为空未登录
         * 3.不为空,登录验证 loginService  checkToken
         * 4.如果认证成功放行
         */

        if (!(handler instanceof HandlerMethod)) {
   
     
     
            //说简单的就是Handler是controller里面的某一个方法
            //handle 可能是  RequestResourceHandle(访问资源handle)  springboot程序访问静态资源  默认去classpath下的static目录查询
            return true;
        }
        得去拿Token,为什么这样呢,因为前端传东西过来的时候是,我们用@RequestHeader("Authorization") 传过来的
        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===================");


        /*token为空,拦截*/
        if(StringUtils.isBlank(token)){
   
     
     
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(),ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf8");
            response.getWriter().print(JSON.toJSONString(result)); //返回json信息(fastjson进行转化)
            return false;
        }
        /*用户为空,拦截*/
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
   
     
     
            //以下是错误返回的信息
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(),ErrorCode.NO_LOGIN.getMsg());
            //要告诉浏览器我们要返回的是这种类型
            response.setContentType("application/json;charset=utf8");
            //返回的东西是result类型,要转换为JSON类型才行
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }

        //登录验证成功,放行
        //在controller直接获取用户信息
        UserThreadLocal.put(sysUser);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   
     
     
        //如果不删除ThreadLocal中的信息,会有内训泄露的风险
        UserThreadLocal.remove();
    }
}

记得在最后清除信息以免内存泄漏
pic_135217ba.png

@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   
     
     
        //如果不删除ThreadLocal中的信息,会有内训泄露的风险
        UserThreadLocal.remove();
    }

13.2 进行测试
在TsetCroller中进行测试

package com.lum.blog.controller;

import com.lum.blog.dao.pojo.SysUser;
import com.lum.blog.utils.UserThreadLocal;
import com.lum.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {
   
     
     
    /**
     * 用来测试拦截器
     */
    @RequestMapping
    public Result test(){
   
     
     
        SysUser sysUser = UserThreadLocal.get();
        System.out.println(sysUser);
        return Result.success(null);
    }
}

pic_6ef9988a.png

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

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

pic_ccf532cd.png

这要知道的是一个线程可以存在多个ThreadLocal


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

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

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

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

	上面的那个 key为使用**弱引用**ThreadLocal实例,当我们的线程中的那个
ThreadLocal被垃圾回收机制干掉之后,是不是这个弱引用的Key不存在了,但是这个是
Map集合呀,Value会永远的存在,所有要手动的删除

14.文章详情

14.1 接口说明
接口url:/articles/view/(id)
请求方式:POST
请求参数:

参数名称参数类型说明
idlong文章id(路径参数)

返回的数据

{
   
     
     
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

14.2 涉及的表及对应的pojo

CREATE TABLE `blog`.`ms_article_body`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  #这个是文章内容
  `content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  #文章内容页面
  `content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `article_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8 COLLATE = utf8_general_ci 
ROW_FORMAT = Dynamic;

ArticleBody

package com.lum.blog.dao.pojo;

import lombok.Data;

@Data
public class ArticleBody {
   
     
     

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

类别表

//类别表
CREATE TABLE `blog`.`ms_category`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  #分类的图标
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  #分类图标的名字
  `category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

Category

package com.lum.blog.dao.pojo;

import lombok.Data;

@Data
public class Category {
   
     
     

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

14.3Controller
在ArticleConller中添加方法查找文章

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

    }

14.4 Service
返回文章的哪些内容需要用到articleVo中添加ArticleBodyVo和CategoryVo

package com.lum.blog.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;
}
package com.lum.blog.vo;

import lombok.Data;

@Data
public class CategoryVo {
   
     
     

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

在articleService中添加查找文章方法接口

/**
     * 查询文章详情
     * @param articleId
     * @return
     */
    Result findArticleById(Long articleId);
以下全在ArticleServiceImpl中编写

在articleServiceImpl中实现方法

/*文章详情*/
    @Override
    public Result findArticleById(Long articleId) {
   
     
     
        /**
         * 1.根据id查询文章信息
         * 2.根据bodyId和categoryId 去做关联查询
         */

        Article article = this.articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article, true, true,true,true);

        return Result.success(articleVo);
    }
/*文章体显示*/
    private ArticleBodyVo findArticleBodyById(Long bodyId) {
   
     
     
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());

        return articleBodyVo;
    }

想要显示文章部分内容就要根据文章找到详情

想要显示内容需要ArticleVo
重载cpoyList可以根据参数显示不同文章内容

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

    }


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

文章articleVo显示 copyList重载后的copy方法

private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory) {
   
     
     
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);

        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));

        if (isTag) {
   
     
     
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if (isAuthor) {
   
     
     
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        if(isBody){
   
     
     
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if(isCategory){
   
     
     
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }

        return articleVo;
    }

需要创建CategoryService

package com.lum.blog.service;

import com.lum.blog.vo.CategoryVo;
import com.lum.blog.vo.Result;


public interface CategoryService {
   
     
     


    CategoryVo findCategoryById(Long categoryId);

    Result findAll();

    /**
     * 文章分类
     * @return
     */
    Result findAllDetail();


    Result categoryDetailById(Long id);
}

实现CategoryServiceImpl

package com.lum.blog.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.lum.blog.dao.mapper.CategoryMapper;
import com.lum.blog.dao.pojo.Category;
import com.lum.blog.service.CategoryService;
import com.lum.blog.vo.CategoryVo;
import com.lum.blog.vo.Result;
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 CategoryServiceImpl implements CategoryService {
   
     
     

    @Autowired
    private CategoryMapper categoryMapper;

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

        return categoryVo;
    }

不要忘记注入关系

@Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private TagService tagService;

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private ArticleBodyMapper articleBodyMapper;

想要文章体需要添加articleBodyMapper

package com.lum.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lum.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
   
     
     
}

14.5 进行测试

pic_2729ac9a.png

15. 阅读数的更新

线程池的使用
问题介绍

@Override
    public ArticleVo findArticleById(Long id) {
   
     
     
        Article article = articleMapper.selectById(id);

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

线程池的配置
com.lum.blog.config下新建ThreadPoolConfig开启线程池并进行相关配置

package com.lum.blog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

/**
 * @author lum
 * @date 2021/9/3
 */
@Configuration
@EnableAsync
//开启多线程

public class ThreadPoolConfig {
   
     
     
    @Bean("taskExecutor")
    public Executor asyncServiceExecutor() {
   
     
     
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置线程核心数
        executor.setCorePoolSize(5);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        // 配置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间
        executor.setKeepAliveSeconds(60);
        // 设置线程名称
        executor.setThreadNamePrefix("Lum博客");
        //等待所有任务及结束后关闭线程
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();

        return executor;
    }
}

15.1 Controller
属于文章业务,无controller
15.2 Service

在com.lum.blog.service.Impl下新建ThreadService完成线程池的使用

package com.lum.blog.service.Impl;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.lum.blog.dao.mapper.ArticleMapper;
import com.lum.blog.dao.pojo.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * @author lum
 * @date 2021/9/3
 */

@Component
public class ThreadService {
   
     
     


    //期望此操作在线程池中 执行 不会影响主线程


    @Async("taskExecutor")
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
   
     
     

        int viewCounts = article.getViewCounts();
        Article articleUpdate = new Article();
        articleUpdate.setViewCounts(viewCounts+1);

        LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(Article::getId,article.getId());
        // 设置一个为了在多线程的条件下 线程安全
        updateWrapper.eq(Article::getViewCounts,viewCounts);
        // update article set view_count = 100 where view_count =99 and id = 1
        articleMapper.update(articleUpdate, updateWrapper);

        try {
   
     
     
            Thread.sleep(2000);
            System.out.println("更新完成!");
        } catch (InterruptedException e) {
   
     
     
            e.printStackTrace();
        }

    }
}

这是会出现一个bug
更新阅次数时会吧评论数更新为0
这是因为在Article的pojo与数据库的关系映中,int基本类型的默认值为0,会影响sql语句
所以在pojo中不要使用基本类型

package com.lum.blog.dao.pojo;

import lombok.Data;
/*
博客管理
 */
@Data
public class Article {
   
     
     

    public static final Integer Article_TOP = 1;

    public static final Integer Article_Common = 0;

    private Long id;

    private String title;

    private String summary;

    private Integer commentCounts;

    private Integer viewCounts;

    /**
     * 作者id
     */
    private Long authorId;
    /**
     * 内容id
     */
    private Long bodyId;
    /**
     *类别id
     */
    private Long categoryId;

    /**
     * 置顶
     */
    private Integer weight;


    /**
     * 创建时间
     */
    private Long createDate;
}

16 评论列表

16.1 接口说明
接口url:/comments/article/(id)
请求方式:GET
请求参数:

参数名称参数类型说明
idlong文章id(路径参数)

返回的数据

{
   
     
     
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
    {
   
     
     
    	"id":12,
    	"author":{
   
     
     
    	"nickname":"赵云",
    	"avatar":"",
    	"id":1
    	},
    	"content":"111"
    	"childrens":[],
    	"createDate":"2021-9-1 08:35",
    	"level":2,
    	"toUser":{
   
     
     
    		"id":12,
    		"nickname":"赵云",
    		"id":1
    	}
    }
    ],
    "createDate":"2021-9-1 08:35",
    "level":1,
    "toUser":null
    }
  ]
    
}

新建pojo包下Comment映射关系

package com.lum.blog.dao.pojo;

import lombok.Data;

/**
 * @author lum
 * @date 2021/9/3
 */

@Data
public class Comment {
   
     
     
    private Long id;

    private String content;

    private Long createDate;

    private Long articleId;

    private Long authorId;

    private Long parentId;

    private Long toUid;

    private Integer level;
}

创建两个Vo用来显示

CommentVo

package com.lum.blog.vo;

import lombok.Data;

import java.util.List;

/**
 * @author lum
 * @date 2021/9/3
 */
@Data
public class CommentVo {
   
     
     
    private Long id;

    private UserVo author;

    private String content;

    private List<CommentVo> childrens;

    private String createDate;

    private Integer level;

    private UserVo toUser;
}

UserVo

package com.lum.blog.vo;

import lombok.Data;

/**
 * @author lum
 * @date 2021/9/3
 */
@Data
public class UserVo {
   
     
     

    private String nickname;

    private String avatar;

    private Long id;
}

16.2 Controller
创建CommentController

package com.lum.blog.controller;

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

/**
 * @author lum
 */
@RestController
@RequestMapping("comments")
public class CommentController {
   
     
     

    @Autowired
    private CommentsService commentsService;

    @GetMapping("article/{id}")
    public Result findArticleById(@PathVariable("id") Long articleId){
   
     
     
        return commentsService.commentsByArticleId(articleId);

    }
}

16.3 Service
创建接口CommentService

package com.lum.blog.service;

import com.lum.blog.vo.Result;

/**
 * @author lum
 * @date 2021/9/3
 */
public interface CommentsService {
   
     
     
    /**
     * 根据文章id查找评论
     * @param articleId
     * @return
     */
    Result commentsByArticleId(Long articleId);
}

CommentServiceImpl实现方法

package com.lum.blog.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.lum.blog.dao.mapper.CommentMapper;
import com.lum.blog.dao.pojo.Comment;
import com.lum.blog.service.CommentsService;
import com.lum.blog.service.SysUserService;
import com.lum.blog.vo.CommentVo;
import com.lum.blog.vo.Result;
import com.lum.blog.vo.UserVo;
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;

/**
 * @author lum
 * @date 2021/9/3
 */
@Service
public class CommentsServiceImpl implements CommentsService {
   
     
     

    @Autowired
    private CommentMapper commentMapper;

    @Autowired
    private SysUserService sysUserService;

    @Override
    public Result commentsByArticleId(Long articleId) {
   
     
     
        /*
          1.根据文章id查询评论列表,从 comment 中查询
          2.根据作者id查询作者信息
          3.如果 level=1,查询有没有子评论,\
          4.如果有  根据评论id进行查询

         */

        LambdaQueryWrapper<Comment> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getArticleId,articleId);
        queryWrapper.eq(Comment::getLevel,1);
        List<Comment> comments = commentMapper.selectList(queryWrapper);
        List<CommentVo> commentVoList= copyList(comments);
        return Result.success(commentVoList);
    }

    
    private List<CommentVo> copyList(List<Comment> comments) {
   
     
     
        List<CommentVo> commentVoList = new ArrayList<>();
        for (Comment comment : comments) {
   
     
     
            commentVoList.add(copy(comment));
        }
        return commentVoList;
    }

    private CommentVo copy(Comment comment) {
   
     
     
        CommentVo commentVo = new CommentVo();
        //将类型相同的copy到commentVo中
        BeanUtils.copyProperties(comment, commentVo);
        //时间格式化
        commentVo.setCreateDate(new DateTime(comment.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        //作者信息
        Long authorId = comment.getAuthorId();
        UserVo userVo = this.sysUserService.findUserVoById(authorId);
        commentVo.setAuthor(userVo);

        //子评论
        Integer level = comment.getLevel();
        if (level == 1){
   
     
     
            Long id = comment.getId();
            List<CommentVo> commentVoList = findCommentByParentId(id);
            commentVo.setChildrens(commentVoList);
        }

        //toUser向谁评论
        if (level > 1) {
   
     
     
            Long toUid = comment.getToUid();
            UserVo toUserVo = this.sysUserService.findUserVoById(toUid);
            commentVo.setToUser(toUserVo);

        }

        return commentVo;

    }

    //子评论查询
    private List<CommentVo> findCommentByParentId(Long id) {
   
     
     
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getParentId,id);
        queryWrapper.eq(Comment::getLevel,2);

        return copyList(commentMapper.selectList(queryWrapper));

    }

}

需要向SysUserServiceImpl中增加findUserVoById(Long id)方法

@Override
    public UserVo findUserVoById(Long id) {
   
     
     
        SysUser sysUser = sysUserMapper.selectById(id);
        if (sysUser == null) {
   
     
     
            sysUser = new SysUser();
            sysUser.setId(1L);
            sysUser.setAvatar("/static/img/logo.b3a48c0.png");
            sysUser.setNickname("空");

        }
        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(sysUser,userVo);
          //设置userVo的id
        userVo.setId(String.valueOf(sysUser.getId()));
        return userVo;
    }

进行测试

pic_69246593.png

17.评论功能

17.1 接口说明

接口url:/comments/create/change
请求方式: POST
请求参数:

参数名称参数类型说明
articleidlong文章id
contentlstring评论内容
parentlong父评论id
toUseridlong被评论用户id

创建CommentParam

package com.lum.blog.vo.params;

/**
 * @author lum
 * @date 2021/9/3
 */
@Data
public class CommentParam {
   
     
     
    
    private Long articleId;
    
    private String content;
    
    private Long parentId;
    
    private Long toUserId;
}

17.2 需要加入到登录拦截器中
登录后才可以评论
WebMvcConfig

@Override
    public void addInterceptors(InterceptorRegistry registry) {
   
     
     
        //配置拦截接口
//        除了登录注册的所有接口
//        registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login").excludePathPatterns("/register");
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/comments/create/change")
                .addPathPatterns("/test");
    }

因为分布式id 的Long过长前端解析精度损失,会将值改变,需要进行Json序列化
CommentVo

@JsonSerialize(using = ToStringSerializer.class)
    private Long id;

17.3 Controller
在commentController中添加接口

@PostMapping("create/change")
    public Result comment(@RequestBody CommentParam commentParam){
   
     
     
        return commentsService.comment(commentParam);
    }

17.4 Service
CommentService

/**
     * 评论
     * @param commentParam
     * @return
     */
    Result comment(CommentParam commentParam);

实现方法CommentServiceImpl

//发表评论
    
    @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.getParentId();
        //如果父id为空,则父评论,否则为子评论
        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);
		this.commentMapper.insert(comment);
		//插入到数据库
        return Result.success(null);
    }

18.写文章

需要三个接口:

  1. 获取所有文章类别
  2. 获取所有标签
  3. 发布文章

18.1 所有文章分类

接口说明
接口url:/categorys
请求方式: GET
请求参数:
|参数名称| 参数类型 |说明|

Controller

CategoryController

//categroys
    @GetMapping
    public Result categories(){
   
     
     
        return categoryService.findAll();
    }

Service

CategroyService

//查找分类
 Result findAll();

CategoryServiceImpl

@Override
    public Result findAll() {
   
     
     
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //queryWrapper.select(Category::getId,Category::getCategoryName);
        List<Category> categories = categoryMapper.selectList(queryWrapper);
        //页面交互的对象
        return Result.success(copyList(categories));
    }

    public CategoryVo copy(Category category){
   
     
     
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        //categoryVo.setId(String.valueOf(category.getId()));
        return categoryVo;
    }
    public List<CategoryVo> copyList(List<Category> categoryList){
   
     
     
        List<CategoryVo> categoryVoList = new ArrayList<>();
        for (Category category : categoryList) {
   
     
     
            categoryVoList.add(copy(category));
        }
        return categoryVoList;
    }

18.2 所有文章标签
接口说明
接口url:/tags
请求方式: GET
请求参数:
|参数名称| 参数类型 |说明|

Controller

TagsController

/**
     * 所有文章标签
     * @return
     */
    @GetMapping
    public Result findAll() {
   
     
     
        return tagService.findAll();
    }

TagService

/**
     * 查询所有文章标签
     * @return
     */
    Result findAll();

TagsServiceImpl

@Override
    public Result findAll() {
   
     
     
        List<Tag> tags = this.tagMapper.selectList(new LambdaQueryWrapper<>());
        return Result.success(copyList(tags));
    }

pic_4df8aebd.png

19.发布文章

接口说明

接口url:/articles/publish
请求方式: POST
请求参数:

参数名称参数类型说明
titlestring文章标题
idlong文章id(编辑有值)
bodyobject文章内容
categoryjson文章类别
summarystring文章概述
tagsjson文章标签

创建ArticleParam

@Data
public class ArticleParam {
   
     
     

    private Long id;

    private ArticleBodyParam body;

    private CategoryVo category;

    private String summary;

    private List<TagVo> tags;

    private String title;
}

创建ArticBodyParam

@Data
public class ArticleBodyParam {
   
     
     

    private String content;

    private String contentHtml;

}

创建pojo下article和tag关联表对象

@Data
public class ArticleTag {
   
     
     

    private Long id;

    private Long articleId;

    private Long tagId;

新建ArticleTagMapper接口

package com.lum.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lum.blog.dao.pojo.ArticleTag;

/**
 * @author lum
 * @date 2021/9/4
 */
public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
   
     
     
}

将ArticleVo中id进行序列化,防止前段解析损失

@JsonSerialize(using = ToStringSerializer.class)
    private Long id;

ArticleController

@PostMapping("publish")
    public Result publish(@RequestBody ArticleParam articleParam){
   
     
     
        return articleService.publish(articleParam);
    }

ArticleService

/**
     * 文章发布
     * @param articleParam
     * @return
     */
    Result publish(ArticleParam articleParam);

ArticleServiceImpl

@Autowired
    private ArticleTagMapper articleTagMapper;


    /**
     * 1.发布文章 目的构建Article对象
     * 2. 作者id 当前登陆用户
     * 3. 标签 将标签加入关联表中
     * 4. body内容存储 article bodyId
     * @param articleParam
     * @return
     * 此接口要加入登陆拦截中
     */
    @Override
    public Result publish(ArticleParam articleParam) {
   
     
     
        SysUser sysUser = UserThreadLocal.get();

        Article article = new Article();
        article.setAuthorId(sysUser.getId());
        article.setWeight(Article.Article_Common);
        article.setViewCounts(0);
        article.setTitle(articleParam.getTitle());
        article.setSummary(articleParam.getSummary());
        article.setCommentCounts(0);
        article.setCreateDate(System.currentTimeMillis());
        article.setCategoryId((articleParam.getCategory().getId()));
        //插入之后 会生成一个文章id
        this.articleMapper.insert(article);
        //tag
        List<TagVo> tags = articleParam.getTags();
        if (tags != null){
   
     
     
            for (TagVo tag : tags) {
   
     
     
                Long articleId = article.getId();
                ArticleTag articleTag = new ArticleTag();
                articleTag.setTagId((tag.getId()));
                articleTag.setArticleId(articleId);
                articleTagMapper.insert(articleTag);
            }
        }
        //body
        ArticleBody articleBody  = new ArticleBody();
        articleBody.setArticleId(article.getId());
        articleBody.setContent(articleParam.getBody().getContent());
        articleBody.setContentHtml(articleParam.getBody().getContentHtml());
        articleBodyMapper.insert(articleBody);

        article.setBodyId(articleBody.getId());
        articleMapper.updateById(article);
        //将id转换成string放入map
        Map<String,String> map = new HashMap<>();
        map.put("id",article.getId().toString());
        return Result.success(map);
    }

pic_8f8425ab.png

20 AOP日志

创建commom.aop包
创建LogAnnotation自定义注解

package com.lum.blog.common.aop;

/**
 * @author lum
 * @date 2021/9/4
 */
import java.lang.annotation.*;
//Type 代表可以放在类上面 Method 代表可以放在方法上
@Target({
   
     
     ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
   
     
     

    String module() default "";

    String operator() default "";
}

同时创建LogAspect用来定义切入点及日志

package com.lum.blog.common.aop;

import com.alibaba.fastjson.JSON;
import com.lum.blog.utils.HttpContextUtils;
import com.lum.blog.utils.IpUtils;
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 javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * @author lum
 * @date 2021/9/4
 */

@Component
@Aspect //切面 定义了通知和切点的关系
@Slf4j
public class LogAspect {
   
     
     

    @Pointcut("@annotation(com.lum.blog.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);

        //获取request 设置IP地址
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        log.info("ip:{}", IpUtils.getIpAddr(request));


        log.info("excute time : {} ms",time);
        log.info("=====================log end================================");
    }
}

接下来就可以到Controller任意方法中添加注解来进行记录

@PostMapping
    @LogAnnotation(module = "文章",operator = "获取文章列表")
    public Result listArticle(@RequestBody PageParams pageParams){
   
     
     
        //对于接受的参数问题,这里是RequestBody接收
//            int i=10/0; 测试异常
        return articleService.listArticle(pageParams);
    }

pic_4ba3e1ef.png

21 文章图片上传

接口名称
接口url:/upload
请求方式: POST
请求参数:

参数名称参数类型说明
imagefile上传的文件名称

Controller

新建UploadController

package com.lum.blog.controller;

import com.lum.blog.utils.QiniuUtils;
import com.lum.blog.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

/**
 * @author lum
 * @date 2021/9/4
 */

@RestController
@RequestMapping("upload")
public class UploadController {
   
     
     

    @Autowired
    private  QiniuUtils qiniuUtils;

    @PostMapping
    public Result upload(@RequestParam("image") MultipartFile file) {
   
     
     
        //原始文件名称 比如 aa.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,"上传失败");
    }
}

在appliation.properties中配置上传文件大小

#上传文件的最大值
spring.servlet.multipart.max-request-size=20MB
#单个文件的最大值
spring.servlet.multipart.max-file-size=2MB

使用七牛云存储
maven导包

<dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
            <version>[7.7.0, 7.7.99]</version>
        </dependency>

新建七牛Utils

package com.lum.blog.utils;

/**
 * @author lum
 * @date 2021/9/4
 */

import com.alibaba.fastjson.JSON;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Component
public class QiniuUtils {
   
     
     

    //配置自己的域名
    public static  final String url = "https://qywco434a.hd-bkt.clouddn.com/";


        //配置自己的密钥
    @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.huabei());
        //...其他参数参考类注释
        UploadManager uploadManager = new UploadManager(cfg);
        //...生成上传凭证,然后准备上传
        // 配置空间名称
        String bucket = "lumblog";
        //默认不指定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;
    }
}

22.导航-文章分类

接口名称
接口url:/categorys/detail
请求方式: GET
请求参数:

参数名称参数类型说明

CategoryController

@GetMapping("detail")
    public Result categoriesDetail(){
   
     
     
        return categoryService.findAllDetail();
    }

CategoryService

/**
     * 文章分类
     * @return
     */
    Result findAllDetail();

CategoryServiceImpl

@Override
    public Result findAllDetail() {
   
     
     
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        List<Category> categories = categoryMapper.selectList(queryWrapper);
        //页面交互的对象
        return Result.success(copyList(categories));
    }

与findAll区别在于
findAll只查询id和分类名
findAllDetail查询全部信息

pic_9ed90ebe.png

23 导航-标签

接口说明

接口url:/categorys/detail
请求方式: GET
请求参数:

参数名称参数类型说明

23.2
TagVo新增avatar属性

package com.lum.blog.vo;

import lombok.Data;

@Data
public class TagVo {
   
     
     

    private Long id;

    private String tagName;

    private String avatar;
}

CategroyVo

@Data
public class CategoryVo {
   
     
     

    private Long id;

    private String avatar;

    private String categoryName;
    
    private String description;

TagsController

/**
     * 导航栏所有标签
     * @return
     */
    @GetMapping("detail")
    public Result findAllDetail() {
   
     
     
        return tagService.findAllDetail();
    }

TagService

/**
     * 问导航栏标签
     * @return
     */
    Result findAllDetail();

TagServiceImpl
更改之前findAll方法,增加条件只查询id和tagName
需要设么查询什么

@Override
    public Result findAll() {
   
     
     
        LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.select(Tag::getId,Tag::getTagName);
        List<Tag> tags = this.tagMapper.selectList(queryWrapper);
        return Result.success(copyList(tags));
    }

    @Override
    public Result findAllDetail() {
   
     
     
        LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
        List<Tag> tags = this.tagMapper.selectList(queryWrapper);
        return Result.success(copyList(tags));
    }

24 分类文章列表

接口名称
接口url:/categorys/detail/{id}
请求方式: GET
请求参数:

参数名称参数类型说明
id分类id路径

CategoryController

@GetMapping("detail/{id}")
    public Result categoryDetailById(@PathVariable("id") Long id){
   
     
     
        return categoryService.categoryDetailById(id);
    }

CategoryService

Result categoryDetailById(Long id);

CategoryServiceImpl

@Override
    public Result categoryDetailById(Long id) {
   
     
     
        Category category = categoryMapper.selectById(id);
        return Result.success(copy(category));
    }

因为CategoryVo的tagId属性改为String
CategoryVo copy代码修改

public CategoryVo copy(Category category){
   
     
     
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        categoryVo.setId(String.valueOf(category.getId()));
        return categoryVo;
    }

需要增加ArticleServiceImpl中的ListArticle条件实现根据CategoryId分类

PageParam

package com.lum.blog.vo.params;

import lombok.Data;

//承担着返回页数和数量的类
@Data
public class PageParams {
   
     
     
	private int page = 1;

	private int pageSize = 10;

	private Long categoryId;

	private Long tagId;

}

ArticleServiceImpl

//and category_id = #{categoryId}
        if(pageParams.getCategoryId() != null){
   
     
     
            queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
        }

pic_725c19dd.png

pic_ea290d2e.png

25.标签文章列表

接口名称
接口url:/tags/detail/{id}
请求方式: GET
请求参数:

参数名称参数类型说明
id标签id路径

TagsController

/**
 * 标签对应文章
 * @return
 */
@GetMapping("detail/{id}")
public Result findDetailById(@PathVariable("id") Long id) {
   
     
     
    return tagService.findDetailById(id);
}

tagService

/**
     * 标签对应文章
     * @param id
     * @return
     */
    Result findDetailById(Long id);

tagServiceimpl

因为tagVo的tagId属性改为String
tagVo copy方法修改

public TagVo copy(Tag tag){
   
     
     
        TagVo tagVo = new TagVo();
        BeanUtils.copyProperties(tag,tagVo);
        tagVo.setId(String.valueOf(tag.getId()));
        return tagVo;
    }
@Override
    public Result findDetailById(Long id) {
   
     
     
        Tag tag = tagMapper.selectById(id);
        return Result.success(copy(tag));
    }

标签对应文章
在ArticleServiceImpl增加查询文章列表条件(listArticle)
pic_7961a0f7.png

List<Long> articleIdList = new ArrayList<>();
        if (pageParams.getTagId() !=null) {
   
     
     
            //加入标签  条件查询
            //article表中 并没有tag字段一篇文章有多个标签
            //article_tag 中一个articleId对应多个tagId
            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());
                //将文章标签id和标签id相等的放入数组
            }
            if (articleIdList.size() > 0){
   
     
     
                //如果有文章id则查询文章id是否在符合条件数组中
                queryWrapper.in(Article::getId,articleIdList);
            }
        }

pic_22162c39.png

26 导航栏-文章归档

在PageParam中添加属性

private String year;

    private String month;

    public String getMonth(){
   
     
     
        if (this.month != null && this.month.length() == 1){
   
     
     
            return "0"+this.month;
        }
        return this.month;
    }

因为需要按时间查询文章列表,所以需要修改articleServiceImpl中的 listArticle 查询条件

@Override
    public Result listArticle(PageParams pageParams) {
   
     
     
            Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
            IPage<Article> articleIPage = articleMapper.listAticle(
                    page,
                    pageParams.getCategoryId(),
                    pageParams.getTagId(),
                    pageParams.getYear(),
                    pageParams.getMonth());
            List<Article> recordes = articleIPage.getRecords();
            return Result.success(copyList(recordes,true,true));
    }

将之前的注释掉

在articlemapper.java中增加mybatis-plus分页查询文章属性

//mybatis-plus分页
    IPage<Article> listAticle(Page<Article> page,
        Long categoryId,
        Long tagId,
        String year,
        String month);

修改articleMapper.xml中对应的sql语句
增加articleMap映射语句,属性和数据库对应

因为开启了驼峰命名映射ResultMap,所以可删除映射表,两种方法都可以
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.lum.blog.dao.mapper.ArticleMapper">

    <resultMap id="articleMap" type="com.lum.blog.dao.pojo.Article">
        <id column="id" property="id" />
        <result column="author_id" property="authorId"/>
        <result column="comment_counts" property="commentCounts"/>
        <result column="create_date" property="createDate"/>
        <result column="summary" property="summary"/>
        <result column="title" property="title"/>
        <result column="view_counts" property="viewCounts"/>
        <result column="weight" property="weight"/>
        <result column="body_id" property="bodyId"/>
        <result column="category_id" property="categoryId"/>
    </resultMap>

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

    </select>
    <select id="listAticle"  resultMap="articleMap" resultType="com.lum.blog.dao.pojo.Article">
        select * from ms_article
        <where>
            1=1
            <if test="categoryId != null">
                and category_id=#{categoryId}
            </if>
            <if test="tagId != null">
                and id in (select article_id from ms_article_tag where 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>

pic_f5dd06ea.png

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

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:17:32-

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