LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

Cloud分布式微服务打造大型自媒体3大业务平台

2024/5/12

自媒体项目

开发思维与企业一致
  • 三端融合:门户+媒体+运营
  • 4g自媒体辉煌时代
  • 5g科技互联网风口
  • 前后端分离式开发、代码动静分离、保证职能解耦、功能模块互相协调
如何整合分布式中间件到项目中
  • 整个流程会做到细致入微
  • 帮助迅速提升至少2年以上的项目经验
重点功能技术分析
  • Redis:分布式会话、session共享、单点登录、防刷、计数
  • Fastdfs+Nginx/OSS/GridFS:搭建分布式文件系统、单文件/批量上传、人脸隐私保护拦截
  • Maven:项目构建、聚合、分层、架构设计、面向对象
  • 阿里AI:人脸对比、文本/图片自动审核、短信
  • SpringCloud:业务分而治之、可伸缩、可扩展、接口服务化
  • Freemarker:构建模块页,实现页面静态化
前置技能必备
  • Java基础
  • 熟悉MySQL/MariaDB
  • 掌握Linux的基本命令
课程安排
  • 前端构建与运行
  • 后端手把手从0到1
  • 中间件手把手部署

前后端分离开发模式

传统JavaWeb开发 与 前后端页面交互

运行前端项目

前端代码在压缩包中 启动D:\apache-tomcat-8.5.93\bin\startup.bat
将里面的imooc-news放到D:\apache-tomcat-8.5.93\webapps中
去浏览器中启动 ttp://localhost:9090/imooc-news/portal/index.html
SwitchHosts

https://oldj.github.io/SwitchHosts/
在本地把域名和对应的IP給联系绑定起来 [相当于在云服务买了域名后绑定]

#imooc-news 127.0.0.1
[这东西需要关闭代理才可以用]

127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com

127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com

D:\apache-tomcat-8.5.93\webapps\imooc-news\portal\js\app.js
window.app = {
    /* 
    portalIndexUrl: "http://localhost:8080/imooc-news/portal/index.html",           // 门户首页地址
    writerIndexUrl: "http://localhost:8080/imooc-news/writer/contentMng.html",      // 作家中心首页
    writerInfoUrl: "http://localhost:8080/imooc-news/writer/accountInfo.html",     // 用户信息完善页面
    userServerUrl: "http://192.168.1.5:8003",   // 用户服务后端接口地址
    */

    portalIndexUrl: "http://www.imoocnews.com:9090/imooc-news/portal/index.html",           // 门户首页地址
    writerLoginUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/passport.html",      // 登录页面
    writerIndexUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/contentMng.html",      // 作家中心首页
    writerInfoUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html",     // 用户信息完善页面
    adminCenterUrl: "http://admin.imoocnews.com:9090/imooc-news/admin/contentReview.html",     // 运营管理平台主页

    userServerUrl: "http://user.imoocnews.com:8003",        // 用户服务后端接口地址
    fsServerUrl: "http://files.imoocnews.com:8004",         // 文件服务后端接口地址
    adminServerUrl: "http://admin.imoocnews.com:8005",      // 运营管理服务后端接口地址
    articleServerUrl: "http://article.imoocnews.com:8001",      // 文章服务后端接口地址

    /**
     * 如果本地使用localhost测试可以不使用,如果是ip或者域名测试,cookieDomain改为对应的ip或者域名
     * 例:
     *    ip:  192.168.1.111
     *    域名:   .imooc.com
     */
    cookieDomain: ".imoocnews.com",  
    ......
}

数据库选型与数据导入

  • MySql 5.6/5.7
  • MariaDB
  • Mysql 8.0
表名                             注释
admin_user                      运营管理平台的admin级别用户    
app_user                      网站用户
article                          文章资讯表
category                      新闻资讯文章的分类(或称之为领域)
comments                      评论表
fans                          粉丝表,用户与粉丝的关联关系,粉丝本质也是用户

构建Maven聚合工程

创建一个 imooc-news-dev 的Maven项目作为一个顶级工程项目

pom.xml
<?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.imooc</groupId>
    <artifactId>imooc-news-dev</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
<!--
    1.聚合工程可以分为顶级项目(顶级工程,父工程) 与子工程(子modele模块)
      这两者的关系其实就是父子继承关系, 子工程在maven中可以称为module,
      模块与模块之间是平级的,是可以相互依赖的
    2.子模块可以使用顶级工程中所有的资源(依赖), 子模块之间如果有要使用资源的话
      必须构建依赖(构建关系)
    3.一个顶级工程是可以由多个不同的子工程共同组合而成
-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath /> <!--SpringBoot是和后续的SpringCLoud版本联系的-->
    </parent>

    <properties> <!--属性文件参数 如果mysql是8以上 需要修改mysql的版本号-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <mysql-connector-java.version>8.0.33</mysql-connector-java.version>
        <mybatis-spring-boot-starter.version>2.1.0</mybatis-spring-boot-starter.version>
        <mapper-spring-boot-starter.version>2.1.5</mapper-spring-boot-starter.version>
        <pagehelper-spring-boot-starter.version>1.2.12</pagehelper-spring-boot-starter.version>

        <okhttp.version>4.2.2</okhttp.version>
        <jackson.version>2.10.2</jackson.version>

        <commons-codec.version>1.11</commons-codec.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <commons-fileupload.version>1.4</commons-fileupload.version>
        <google-guava.version>28.2-jre</google-guava.version>

        <springfox-swagger2.version>2.4.0</springfox-swagger2.version>
        <swagger-bootstrap-ui.version>1.6</swagger-bootstrap-ui.version>
        <fastdfs.version>1.27.2</fastdfs.version>

        <slf4j.version>1.7.21</slf4j.version>
        <joda-time.version>2.10.6</joda-time.version>
    </properties>

    <!--
        使用dependencyManagement的目的是为了保证父工程的干净,
        也就是说父工程他只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖。
        如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系,
        依赖的导入只有在各自的子工程中才会进行导入。
    -->
<!--  ↓ 管理依赖 不会从外网下载具体jar包 只有在后续子模块配置的时候才会去配置
      为了保证父工程的干净,夫工程中只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖
      如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系
      依赖的导入只有在各自的子工程中才会导入
 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringCloud 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 引入 mongodb 依赖 -->
            <dependency>
                <groupId>org.mongodb</groupId>
                <artifactId>mongodb-driver</artifactId>
                <!--mongodb-driver.version-->
                <version>3.11.1</version>
            </dependency>
            <!-- mysql 驱动  这样引用方便以后jar包依赖的升级-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
            <!-- 通用mapper逆向工具 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper-spring-boot-starter.version}</version>
            </dependency>
            <!--pagehelper -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper-spring-boot-starter.version}</version>
            </dependency>
            <!--服务和服务之间的请求-->
            <dependency>
                <groupId>com.squareup.okhttp3</groupId>
                <artifactId>okhttp</artifactId>
                <version>${okhttp.version}</version>
            </dependency>

            <!-- jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

            <!-- apache 工具类 -->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>${commons-codec.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>${commons-fileupload.version}</version>
            </dependency>

            <!-- google 工具类 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${google-guava.version}</version>
            </dependency>

            <!-- swagger2 配置 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>swagger-bootstrap-ui</artifactId>
                <version>${swagger-bootstrap-ui.version}</version>
            </dependency>

            <!-- 文件上传fdfs工具 -->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastdfs.version}</version>
            </dependency>

            <!-- joda-time 时间工具 -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${joda-time.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Java 编译 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
imooc-news-dev-common
<?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>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>
imooc-news-dev-model
<?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>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-model
    模型工程, 所有的子工程以及微服务中所涉及到的模型实体类都在此管理
    可以包含一些 *pojo,*Bean,*Entity,vo,bo,dto等
-->

    <artifactId>imooc-news-dev-model</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
<!--  子工程依赖common -->
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
imooc-news-dev-service-api
<?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>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!--    imooc-news-dev-service-api
            接口工程,集中管理所有的controller中的接口,为了更好的统一管理微服务
    -->
    <artifactId>imooc-news-dev-service-api</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
imooc-news-dev-service-user
<?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>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-user</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
    </dependencies>
</project>
imooc-news-dev [父]
<?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.imooc</groupId>
    <artifactId>imooc-news-dev</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
    <!-- 这里是子模块[自动创建] 如果物理删除了子工程 这个不会自动删除的要手动删除-->
        <module>imooc-news-dev-common</module>
        <module>imooc-news-dev-model</module>
        <module>imooc-news-dev-service-api</module>
        <module>imooc-news-dev-service-user</module>
        <module>imooc-news-dev-service-user</module>
    </modules>
    <!--
        1.聚合工程可以分为顶级项目(顶级工程,父工程) 与子工程(子modele模块)
          这两者的关系其实就是父子继承关系, 子工程在maven中可以称为module,
          模块与模块之间是平级的,是可以相互依赖的
        2.子模块可以使用顶级工程中所有的资源(依赖), 子模块之间如果有要使用资源的话
          必须构建依赖(构建关系)
        3.一个顶级工程是可以由多个不同的子工程共同组合而成
    -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath /> <!--SpringBoot是和后续的SpringCLoud版本联系的-->
    </parent>

    <properties> <!--属性文件参数 如果mysql是8以上 需要修改mysql的版本号-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <mysql-connector-java.version>8.0.33</mysql-connector-java.version>
        <mybatis-spring-boot-starter.version>2.1.0</mybatis-spring-boot-starter.version>
        <mapper-spring-boot-starter.version>2.1.5</mapper-spring-boot-starter.version>
        <pagehelper-spring-boot-starter.version>1.2.12</pagehelper-spring-boot-starter.version>

        <okhttp.version>4.2.2</okhttp.version>
        <jackson.version>2.10.2</jackson.version>

        <commons-codec.version>1.11</commons-codec.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <commons-fileupload.version>1.4</commons-fileupload.version>
        <google-guava.version>28.2-jre</google-guava.version>

        <springfox-swagger2.version>2.4.0</springfox-swagger2.version>
        <swagger-bootstrap-ui.version>1.6</swagger-bootstrap-ui.version>
        <fastdfs.version>1.27.2</fastdfs.version>

        <slf4j.version>1.7.21</slf4j.version>
        <joda-time.version>2.10.6</joda-time.version>
    </properties>

    <!--
        使用dependencyManagement的目的是为了保证父工程的干净,
        也就是说父工程他只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖。
        如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系,
        依赖的导入只有在各自的子工程中才会进行导入。
    -->
<!--  ↓ 管理依赖 不会从外网下载具体jar包 只有在后续子模块配置的时候才会去配置
      为了保证父工程的干净,夫工程中只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖
      如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系
      依赖的导入只有在各自的子工程中才会导入
 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringCloud 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 引入 mongodb 依赖 -->
            <dependency>
                <groupId>org.mongodb</groupId>
                <artifactId>mongodb-driver</artifactId>
                <!--mongodb-driver.version-->
                <version>3.11.1</version>
            </dependency>

            <!-- mysql 驱动  这样引用方便以后jar包依赖的升级-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
            <!-- 通用mapper逆向工具 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper-spring-boot-starter.version}</version>
            </dependency>
            <!--pagehelper -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper-spring-boot-starter.version}</version>
            </dependency>
            <!--服务和服务之间的请求-->
            <dependency>
                <groupId>com.squareup.okhttp3</groupId>
                <artifactId>okhttp</artifactId>
                <version>${okhttp.version}</version>
            </dependency>

            <!-- jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

            <!-- apache 工具类 -->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>${commons-codec.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>${commons-fileupload.version}</version>
            </dependency>

            <!-- google 工具类 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${google-guava.version}</version>
            </dependency>

            <!-- swagger2 配置 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>swagger-bootstrap-ui</artifactId>
                <version>${swagger-bootstrap-ui.version}</version>
            </dependency>

            <!-- 文件上传fdfs工具 -->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastdfs.version}</version>
            </dependency>

            <!-- joda-time 时间工具 -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${joda-time.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Java 编译 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Object hello(){
        return "hello";
    }
}
com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
  port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: service-user
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
SwitchHosts【一定要先关闭代理 因为它会固定静态ip】
127.0.0.1 user.imoocnews.com

http://user.imoocnews.com:8003/hello

api接口暴露

所有业务下不同的controller都要交给api统一接口去处理,把实现所对应的接口写进API内
把imooc-news-dev-service-user中的pom.xml中的关于SpringBoot的依赖
全部放入imooc-news-dev-service-api中
并且在imooc-news-dev-service-user中写入引用依赖
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
【服务层依赖api层】
更改把user项目的HelloController 复制到dev-service-api 创建一个接口

...api
com/imooc/api/controller/user/HelloControllerApi.java
package com.imooc.api.controller.user;
import org.springframework.web.bind.annotation.GetMapping;

public interface HelloControllerApi {
    /**
     * api的作用:
     * api就相当于企业的领导,老板,部门经理
     * 其他的服务层都是实现,他们就相当于员工,只做事情
     * 老板(开发人员)来看一下每个人(服务)的进度,做什么事
     * 老板不会去问员工,他只会对接部门经理
     * 这里所有的api接口就是统一在这里管理和调度的,微服务也如此
     */

    /**
     * 运作:
     * 现在的所有接口都在此暴露,实现都是在各自的微服务中
     * 本项目只写项目,不写实现,实现在各自的微服务工程中,因为以业务来划分的微服务有很多
     * Controller也会分散在各个微服务工程中,一旦多了就很难统一管理和查看
     *
     * 其次,微服务之间的调用都是基于接口的
     * 如果不这样做,微服务之间的调用就需要互相依赖了
     * 耦合对也就很高,接口的目的是为了能够提供解耦
     *
     * 此外,本项目的接口其实就是一套规范.实现都是由各自的工程去做的处理
     * 目前我们使用springboot作为接口的实现的
     * 如果未来以后出现新的java web框架,那么我们不需要修改接口
     * 只需要去修改对应的实现就可以了,这其实也是解耦的一个体现
     *
     * Swagger2, 基于接口的自动文档生成
     * 所有的配置文件只需要一份,就能再当前项目中去构建了
     * 管理起来很方便
     * 
     * 综上所述,这样做法可以提高多服务的项目可扩展性
     */
    @GetMapping("/hello")
    public Object hello();
}


...user
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    public Object hello(){
        return "hello";
    }
}

配置logback日志与多环境profile

imooc-news-dev-service-user

先添加一个logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-admin"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-dev"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/service-user.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

        return "hello";
    }
}
//重新启动后去页面刷新一下
......
29:22.373 [main] INFO  com.imooc.user.Application - Started Application in 1.061 seconds (JVM running for 1.543)
29:40.693 [http-nio-8003-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
29:40.694 [http-nio-8003-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
29:40.696 [http-nio-8003-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 2 ms
29:40.707 [http-nio-8003-exec-1] INFO  c.i.user.controller.HelloController - info: hello~
29:40.707 [http-nio-8003-exec-1] WARN  c.i.user.controller.HelloController - warn: hello~
29:40.707 [http-nio-8003-exec-1] ERROR c.i.user.controller.HelloController - error: hello~
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-user
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
application-dev.yml
server:
  port: 8003


application-prod.yml
server:
  port: 8130

优雅的返回封装结果

com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.IMOOCJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

//        return "hello";
//        return IMOOCJSONResult.ok();
//        return IMOOCJSONResult.ok("hello!");
        return IMOOCJSONResult.errorMsg("您的信息有误");
    }
}
com/imooc/grace/result/IMOOCJSONResult.java
package com.imooc.grace.result;

/**
 * 
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 * 
 *                 200:表示成功
 *                 500:表示错误,错误信息在msg字段中
 *                 501:bean验证错误,不管多少个错误都以map形式返回
 *                 502:拦截器拦截到用户token出错
 *                 555:异常抛出信息
 *                 556: 用户qq校验异常
 *                 557: 校验用户是否在CAS登录,用户门票的校验
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V1.0
 * 这样太麻烦了 直接用枚举类
 */
public class IMOOCJSONResult {

    // 响应业务状态
    private Integer status;

    // 响应消息
    private String msg;

    // 响应中的数据
    private Object data;
    
    private String ok;    // 不使用

    public static IMOOCJSONResult build(Integer status, String msg, Object data) {
        return new IMOOCJSONResult(status, msg, data);
    }

    public static IMOOCJSONResult build(Integer status, String msg, Object data, String ok) {
        return new IMOOCJSONResult(status, msg, data, ok);
    }
    
    public static IMOOCJSONResult ok(Object data) {
        return new IMOOCJSONResult(data);
    }

    public static IMOOCJSONResult ok() {
        return new IMOOCJSONResult(null);
    }
    
    public static IMOOCJSONResult errorMsg(String msg) {
        return new IMOOCJSONResult(500, msg, null);
    }

    public static IMOOCJSONResult errorUserTicket(String msg) {
        return new IMOOCJSONResult(557, msg, null);
    }
    
    public static IMOOCJSONResult errorMap(Object data) {
        return new IMOOCJSONResult(501, "error", data);
    }
    
    public static IMOOCJSONResult errorTokenMsg(String msg) {
        return new IMOOCJSONResult(502, msg, null);
    }
    
    public static IMOOCJSONResult errorException(String msg) {
        return new IMOOCJSONResult(555, msg, null);
    }
    
    public static IMOOCJSONResult errorUserQQ(String msg) {
        return new IMOOCJSONResult(556, msg, null);
    }

    public IMOOCJSONResult() {

    }

    public IMOOCJSONResult(Integer status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }
    
    public IMOOCJSONResult(Integer status, String msg, Object data, String ok) {
        this.status = status;
        this.msg = msg;
        this.data = data;
        this.ok = ok;
    }

    public IMOOCJSONResult(Object data) {
        this.status = 200;
        this.msg = "OK";
        this.data = data;
    } Getter+Setter
这样太麻烦而且观察起来不方便 升级一下变成枚举类 更加优雅!
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.IMOOCJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

//        return "hello";
//        return IMOOCJSONResult.ok();
//        return IMOOCJSONResult.ok("hello!");
//        return IMOOCJSONResult.errorMsg("您的信息有误");
        return GraceJSONResult.errorCustom(ResponseStatusEnum.NO_AUTH);
    }
}
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.IMOOCJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

//        return "hello";
//        return IMOOCJSONResult.ok();
//        return IMOOCJSONResult.ok("hello!");
//        return IMOOCJSONResult.errorMsg("您的信息有误");
        return GraceJSONResult.errorCustom(ResponseStatusEnum.NO_AUTH);
    }
}
com/imooc/grace/result/GraceJSONResult.java
package com.imooc.grace.result;

import java.util.Map;

/**
 * 自定义响应数据类型枚举升级版本
 *
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 *
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V2.0
 */
public class GraceJSONResult {

    // 响应业务状态码
    private Integer status;

    // 响应消息
    private String msg;

    // 是否成功
    private Boolean success;

    // 响应数据,可以是Object,也可以是List或Map等
    private Object data;

    /**
     * 成功返回,带有数据的,直接往OK方法丢data数据即可
     * @param data
     * @return
     */
    public static GraceJSONResult ok(Object data) {
        return new GraceJSONResult(data);
    }
    /**
     * 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
     * @return
     */
    public static GraceJSONResult ok() {
        return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
    }
    public GraceJSONResult(Object data) {
        this.status = ResponseStatusEnum.SUCCESS.status();
        this.msg = ResponseStatusEnum.SUCCESS.msg();
        this.success = ResponseStatusEnum.SUCCESS.success();
        this.data = data;
    }


    /**
     * 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
     * @return
     */
    public static GraceJSONResult error() {
        return new GraceJSONResult(ResponseStatusEnum.FAILED);
    }

    /**
     * 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
     * @param map
     * @return
     */
    public static GraceJSONResult errorMap(Map map) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
    }

    /**
     * 错误返回,直接返回错误的消息
     * @param msg
     * @return
     */
    public static GraceJSONResult errorMsg(String msg) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
    }

    /**
     * 错误返回,token异常,一些通用的可以在这里统一定义
     * @return
     */
    public static GraceJSONResult errorTicket() {
        return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
    }

    /**
     * 自定义错误范围,需要传入一个自定义的枚举,可以到[ResponseStatusEnum.java[中自定义后再传入
     * @param responseStatus
     * @return
     */
    public static GraceJSONResult errorCustom(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }
    public static GraceJSONResult exception(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }

    public GraceJSONResult(ResponseStatusEnum responseStatus) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
        this.data = data;
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
        this.status = responseStatus.status();
        this.msg = msg;
        this.success = responseStatus.success();
    }Getter+Setter
}
com/imooc/grace/result/ResponseStatusEnum.java
package com.imooc.grace.result;

/**
 * 响应结果枚举,用于提供给GraceJSONResult返回给前端的
 * 本枚举类中包含了很多的不同的状态码供使用,可以自定义
 * 便于更优雅的对状态码进行管理,一目了然
 */
public enum ResponseStatusEnum {

    SUCCESS(200, true, "操作成功!"),
    FAILED(500, false, "操作失败!"),

    // 50x
    UN_LOGIN(501,false,"请登录后再继续操作!"),
    TICKET_INVALID(502,false,"会话失效,请重新登录!"),
    NO_AUTH(503,false,"您的权限不足,无法继续操作!"),
    MOBILE_ERROR(504,false,"短信发送失败,请稍后重试!"),
    SMS_NEED_WAIT_ERROR(505,false,"短信发送太快啦~请稍后再试!"),
    SMS_CODE_ERROR(506,false,"验证码过期或不匹配,请稍后再试!"),
    USER_FROZEN(507,false,"用户已被冻结,请联系管理员!"),
    USER_UPDATE_ERROR(508,false,"用户信息更新失败,请联系管理员!"),
    USER_INACTIVE_ERROR(509,false,"请前往[账号设置]修改信息激活后再进行后续操作!"),
    FILE_UPLOAD_NULL_ERROR(510,false,"文件不能为空,请选择一个文件再上传!"),
    FILE_UPLOAD_FAILD(511,false,"文件上传失败!"),
    FILE_FORMATTER_FAILD(512,false,"文件图片格式不支持!"),
    FILE_MAX_SIZE_ERROR(513,false,"仅支持500kb大小以下的图片上传!"),
    FILE_NOT_EXIST_ERROR(514,false,"你所查看的文件不存在!"),
    USER_STATUS_ERROR(515,false,"用户状态参数出错!"),
    USER_NOT_EXIST_ERROR(516,false,"用户不存在!"),

    // 自定义系统级别异常 54x
    SYSTEM_INDEX_OUT_OF_BOUNDS(541, false, "系统错误,数组越界!"),
    SYSTEM_ARITHMETIC_BY_ZERO(542, false, "系统错误,无法除零!"),
    SYSTEM_NULL_POINTER(543, false, "系统错误,空指针!"),
    SYSTEM_NUMBER_FORMAT(544, false, "系统错误,数字转换异常!"),
    SYSTEM_PARSE(545, false, "系统错误,解析异常!"),
    SYSTEM_IO(546, false, "系统错误,IO输入输出异常!"),
    SYSTEM_FILE_NOT_FOUND(547, false, "系统错误,文件未找到!"),
    SYSTEM_CLASS_CAST(548, false, "系统错误,类型强制转换错误!"),
    SYSTEM_PARSER_ERROR(549, false, "系统错误,解析出错!"),
    SYSTEM_DATE_PARSER_ERROR(550, false, "系统错误,日期解析出错!"),

    // admin 管理系统 56x
    ADMIN_USERNAME_NULL_ERROR(561, false, "管理员登录名不能为空!"),
    ADMIN_USERNAME_EXIST_ERROR(562, false, "管理员登录名已存在!"),
    ADMIN_NAME_NULL_ERROR(563, false, "管理员负责人不能为空!"),
    ADMIN_PASSWORD_ERROR(564, false, "密码不能为空后者两次输入不一致!"),
    ADMIN_CREATE_ERROR(565, false, "添加管理员失败!"),
    ADMIN_PASSWORD_NULL_ERROR(566, false, "密码不能为空!"),
    ADMIN_NOT_EXIT_ERROR(567, false, "管理员不存在或密码错误!"),
    ADMIN_FACE_NULL_ERROR(568, false, "人脸信息不能为空!"),
    ADMIN_FACE_LOGIN_ERROR(569, false, "人脸识别失败,请重试!"),
    CATEGORY_EXIST_ERROR(570, false, "文章分类已存在,请换一个分类名!"),

    // 媒体中心 相关错误 58x
    ARTICLE_COVER_NOT_EXIST_ERROR(580, false, "文章封面不存在,请选择一个!"),
    ARTICLE_CATEGORY_NOT_EXIST_ERROR(581, false, "请选择正确的文章领域!"),
    ARTICLE_CREATE_ERROR(582, false, "创建文章失败,请重试或联系管理员!"),
    ARTICLE_QUERY_PARAMS_ERROR(583, false, "文章列表查询参数错误!"),
    ARTICLE_DELETE_ERROR(584, false, "文章删除失败!"),
    ARTICLE_WITHDRAW_ERROR(585, false, "文章撤回失败!"),
    ARTICLE_REVIEW_ERROR(585, false, "文章审核出错!"),
    ARTICLE_ALREADY_READ_ERROR(586, false, "文章重复阅读!"),

    // 人脸识别错误代码
    FACE_VERIFY_TYPE_ERROR(600, false, "人脸比对验证类型不正确!"),
    FACE_VERIFY_LOGIN_ERROR(601, false, "人脸登录失败!"),

    // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, "");


    // 响应业务状态
    private Integer status;
    // 调用是否成功
    private Boolean success;
    // 响应消息,可以为成功或者失败的消息
    private String msg;

    ResponseStatusEnum(Integer status, Boolean success, String msg) {
        this.status = status;
        this.success = success;
        this.msg = msg;
    }

    public Integer status() {
        return status;
    }
    public Boolean success() {
        return success;
    }
    public String msg() {
        return msg;
    }
}
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": null
}

配置数据库逆向生成实体类

引入mybatis-generator-database[新建工程项目]
pom.xml
<?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.imooc</groupId>
    <artifactId>mybatis-generator-database</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>

        <!-- 引入log4j日志依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.3.8.RELEASE</version>
        </dependency>

        <!-- 阿里开源数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.41</version>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!--mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>1.2.4</version>
        </dependency>
        <!--pagehelper-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!-- mybatis 逆向生成工具  -->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.2</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

整合Mybatis

dependencymanagement 无法引入包依赖-CSDN博客
关于Maven依赖dependency无法引入的问题_ 无法引入包-CSDN博客
Maven配置仓库、阿里云镜像、环境变量(史上最全最详细)_maven配置阿里云镜像-CSDN博客

将mybatis-generator-database中的pojo[AppUser+Fans]复制转移到imooc-news-dev-model com/imooc/pojo中
将mybatis-generator-database中的com/imooc/user/mapper[AppUserMapper、FansMapper]复制转移到imooc-news-service-user com/imooc/user/mapper中
所有的服务都是要实现API的
将mybatis-generator-database中的com/imooc/my/mapper[MyMapper]复制转移到imooc-news-dev-service-api com/imooc/my/mapper中
将..database中的resources的mapper[AppUserMapper.xml、FansMapper.xml]转移到...service-user的resources的mapper中
imooc-news-dev-service-user中的resources的application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-user
  datasource: # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource          # 数据源类型:HikariCP
    driver-class-name: com.mysql.jdbc.Driver          # mysql驱动
    url: jdbc:mysql://localhost:3306/imooc-news-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    username: root
    password: root
    hikari:
      connection-timeout: 30000       # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
      minimum-idle: 5                 # 最小连接数
      maximum-pool-size: 20           # 最大连接数
      auto-commit: true               # 自动提交
      idle-timeout: 600000            # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
      pool-name: DateSourceHikariCP     # 连接池名字
      max-lifetime: 1800000           # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms
      connection-test-query: SELECT 1
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
############################################################
#
# mybatis 配置
#
############################################################
mybatis:
  type-aliases-package: com.imooc.pojo          # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件

############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
  mappers: com.imooc.my.mapper.MyMapper
  not-empty: false    # 在进行数据库操作的的时候,判断表达式 username != null, 是否追加 username != ''
  identity: MYSQL
# 分页插件配置
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true

【解决】SLF4J: Class path contains multiple SLF4J bindings._启动metastore时slf4j: class path contains multiple sl-CSDN博客
整合MongoDB踩坑记录及解决方法 - 会飞的猪仔 - 博客园 (cnblogs.com)

com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan("com.imooc")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Swagger2接口文檔工具的使用

imooc-news-dev-service-api
<!-- swagger2 配置 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${springfox-swagger2.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${springfox-swagger2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>${swagger-bootstrap-ui.version}</version>
        </dependency>
com/imooc/api/config/Swagger2.java
package com.imooc.api.config;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration //Springboot啓動的時候會被掃描到并且加載
@EnableSwagger2
public class Swagger2 {

    //    http://localhost:8088/swagger-ui.html     原路径
//http://user.imoocnews.com:8003/swagger-ui.htm
    //    http://localhost:8088/doc.html            新路径
//http://user.imoocnews.com:8003/doc.html

    // 配置swagger2核心配置 docket
    @Bean
    public Docket createRestApi() {
        Predicate<RequestHandler> adminPredicate = RequestHandlerSelectors.basePackage("com.imooc.admin.controller");
//        Predicate<RequestHandler> articlePredicate = RequestHandlerSelectors.basePackage("com.imooc.article.controller");
        Predicate<RequestHandler> userPredicate = RequestHandlerSelectors.basePackage("com.imooc.user.controller");
        Predicate<RequestHandler> filesPredicate = RequestHandlerSelectors.basePackage("com.imooc.files.controller");

        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                 // 用于定义api文档汇总信息
                .select()
                .apis(Predicates.or(userPredicate, adminPredicate, filesPredicate))
//                .apis(Predicates.or(adminPredicate, articlePredicate, userPredicate, filesPredicate))
                .paths(PathSelectors.any())         // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕课新闻·自媒体接口api")                       // 文档页标题
                .contact(new Contact("imooc",
                        "https://www.imooc.com",
                        "abc@imooc.com"))                   // 联系人信息
                .description("专为慕课新闻·自媒体平台提供的api文档")      // 详细信息
                .version("1.0.1")                               // 文档版本号
                .termsOfServiceUrl("https://www.imooc.com")     // 网站地址
                .build();
    }
}
com/imooc/api/controller/user/HelloControllerApi.java
package com.imooc.api.controller.user;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

@Api(value = "controller的標題",tags = {"xx功能的Controller"})
public interface HelloControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "GET")
    @GetMapping("/hello")
    public Object hello();
}

[maven-之Lifecycle详解_maven lifecycle-CSDN博客](https://blog.csdn.net/qq_39505065/article/details/102915403#第三部分 PS)

Maven中的Lifecycle的install是什麽 ?
将包安装到本地存储库中,作为本地其他项目的依赖项

梳理短信登錄注冊流程

  • 短信登录注册
  • 短信验证码发送与限制
  • 分布式会话
  • 用户信息完善,OSS/FastDFS文件上传
  • AOP日志监控

aliyun.properties
#这里需要去阿里云上购买短信验证
aliyun.accessKeyID=
aliyun.accessKeySecret=
【imooc-news-dev-common】
pom.xml
加入springboot依赖包
<?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>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <!--  第三方云厂商相关依赖 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>
</project>
com/imooc/utils/extend/AliyunResource.java
package com.imooc.utils.extend;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun") //这里是前缀
public class AliyunResource {
    private String accessKeyID;
    private String accessKeySecret;

    public String getAccessKeyID() {
        return accessKeyID;
    }

    public void setAccessKeyID(String accessKeyID) {
        this.accessKeyID = accessKeyID;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }
}
com/imooc/utils/SMSUtils.java
package com.imooc.utils;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.imooc.utils.extend.AliyunResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component //工具类 可以作为组件
public class SMSUtils {
    @Autowired
    public AliyunResource aliyunResource;
    public void sendSMS(String mobile, String code) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou",
                aliyunResource.getAccessKeyID(),
                aliyunResource.getAccessKeySecret());
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("RegionId", "cn-hangzhou");
        //给对方发送的手机号
        request.putQueryParameter("PhoneNumbers", mobile);
        request.putQueryParameter("SignName", "风间影月");//控制台可以添加签名
        request.putQueryParameter("TemplateCode", "SMS_183761535");
        request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");//JSON对象字符串

        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
    }
}
com/imooc/api/controller/user/PassportControllerApi.java[接口]
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode();
}
com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PassportController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;

    @Override
    public GraceJSONResult getSMSCode(){
        String random = "123456789";
        smsUtils.sendSMS("15027597319",random);
        return GraceJSONResult.ok();
    }
}
com/imooc/grace/result/GraceJSONResult.java
package com.imooc.grace.result;

import java.util.Map;

/**
 * 自定义响应数据类型枚举升级版本
 *
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 *
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V2.0
 */
public class GraceJSONResult {

    // 响应业务状态码
    private Integer status;

    // 响应消息
    private String msg;

    // 是否成功
    private Boolean success;

    // 响应数据,可以是Object,也可以是List或Map等
    private Object data;

    /**
     * 成功返回,带有数据的,直接往OK方法丢data数据即可
     * @param data
     * @return
     */
    public static GraceJSONResult ok(Object data) {
        return new GraceJSONResult(data);
    }
    /**
     * 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
     * @return
     */
    public static GraceJSONResult ok() {
        return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
    }
    public GraceJSONResult(Object data) {
        this.status = ResponseStatusEnum.SUCCESS.status();
        this.msg = ResponseStatusEnum.SUCCESS.msg();
        this.success = ResponseStatusEnum.SUCCESS.success();
        this.data = data;
    }


    /**
     * 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
     * @return
     */
    public static GraceJSONResult error() {
        return new GraceJSONResult(ResponseStatusEnum.FAILED);
    }

    /**
     * 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
     * @param map
     * @return
     */
    public static GraceJSONResult errorMap(Map map) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
    }

    /**
     * 错误返回,直接返回错误的消息
     * @param msg
     * @return
     */
    public static GraceJSONResult errorMsg(String msg) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
    }

    /**
     * 错误返回,token异常,一些通用的可以在这里统一定义
     * @return
     */
    public static GraceJSONResult errorTicket() {
        return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
    }

    /**
     * 自定义错误范围,需要传入一个自定义的枚举,可以到[ResponseStatusEnum.java[中自定义后再传入
     * @param responseStatus
     * @return
     */
    public static GraceJSONResult errorCustom(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }
    public static GraceJSONResult exception(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }

    public GraceJSONResult(ResponseStatusEnum responseStatus) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
        this.data = data;
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
        this.status = responseStatus.status();
        this.msg = msg;
        this.success = responseStatus.success();
    }

    public GraceJSONResult() {
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

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

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }
}

安装配置整合Redis

通过Xftp7把redis-5.0.7.tar.gz传入到服务器
[root@iZbp1dssknxftmjczbtpndZ ~]# tar -zxvf redis-5.0.7.tar.gz 
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
apache-zookeeper-3.6.0-bin         rabbitmq-server-3.8.2-1.el7.noarch.rpm
apache-zookeeper-3.6.0-bin.tar.gz  redis-5.0.7
erlang-22.3-1.el7.x86_64.rpm       redis-5.0.7.tar.gz
[root@iZbp1dssknxftmjczbtpndZ ~]# cd redis-5.0.7
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# ll
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# yum install gcc-c++
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# make
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cd /usr/local/
[root@iZbp1dssknxftmjczbtpndZ local]# ll
[root@iZbp1dssknxftmjczbtpndZ local]# pwd
/usr/local
[root@iZbp1dssknxftmjczbtpndZ local]# cd redis/
[root@iZbp1dssknxftmjczbtpndZ redis]# ll
total 4
drwxr-xr-x 2 root root 4096 May 15 11:19 bin
[root@iZbp1dssknxftmjczbtpndZ redis]# cd bin
[root@iZbp1dssknxftmjczbtpndZ bin]# ll
total 32772
-rwxr-xr-x 1 root root 4366880 May 15 11:04 redis-benchmark
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-aof
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-rdb
-rwxr-xr-x 1 root root 4807952 May 15 11:04 redis-cli
-rw-r--r-- 1 root root       0 May 15 11:19 redis.conf
lrwxrwxrwx 1 root root      12 May 15 11:04 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-server
[root@iZbp1dssknxftmjczbtpndZ redis]# cd
[root@iZbp1dssknxftmjczbtpndZ ~]# cd redis-5.0.7
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# ls
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cp redis.conf /usr/local/redis/bin/
cp: overwrite ‘/usr/local/redis/bin/redis.conf’? y
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cd /usr/local/redis/bin/
[root@iZbp1dssknxftmjczbtpndZ bin]# ll
total 32836
-rwxr-xr-x 1 root root 4366880 May 15 11:04 redis-benchmark
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-aof
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-rdb
-rwxr-xr-x 1 root root 4807952 May 15 11:04 redis-cli
-rw-r--r-- 1 root root   61797 May 15 11:28 redis.conf
lrwxrwxrwx 1 root root      12 May 15 11:04 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-server
[root@iZbp1dssknxftmjczbtpndZ bin]# vim redis.conf
在里面 /bind 直接搜索
把bind 127.0.0.1修改成 0.0.0.0在任何地方都可以进行操作修改
在里面 /dae
把daemonize no 改成 daemonize yes[后台启动]
在里面 /require
把requirepass foobared 这里是设置密码 requirepass XXXX
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-server redis.conf
32421:C 15 May 2024 11:35:36.687 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
32421:C 15 May 2024 11:35:36.687 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=32421, just started
32421:C 15 May 2024 11:35:36.687 # Configuration loaded
[root@iZbp1dssknxftmjczbtpndZ bin]# ps -ef|grep redis
root     32422     1  0 11:35 ?        00:00:00 ./redis-server 0.0.0.0:6379
root     32456 25226  0 11:35 pts/0    00:00:00 grep --color=auto redis
【此时说明已经成功启动Redis】
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name imooc
OK
127.0.0.1:6379> get name
"imooc"
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli -p 6379 shutdown

3-5

阅读全文

Zookeeper+Dubbo应用与面试

2024/5/4

Zookeeper+Dubbo与面试周介绍

  • Zookeeper的下载、配置与运行
  • 数据结构node与常用命令
  • Watcher机制和权限cal介绍
  • 使用Zookeeper的Java原生客户端和Curator进行开发
  • RPC调用,在Dubbo架构下各服务的关系
  • 整合Dubbo和Zookeeper
  • 完成Dubbo开发案例
  • 线程进阶面试
  • 分布式、微服务面试题
  • Spring Cloud、Zookeeper的理解

Zookeeper

  • 理解Zookeeper
  • 安装、配置
  • 节点znode
  • 常用命令
  • Watcher机制
  • ACL权限控制
  • 代码实操

理解Zookeeper

  • 5大特点
  • 集群架构
  • Zookeeper和CAP关系
  • Zookeeper作用

为什么需要Zookeeper

  • 用起来像单机但是又比单机更可靠

  • leader在团队里的协调作用

  • 内存、单机

  • 集群、可靠

  • 当信息还没同步完成时,不对外提供服务

  • 同步的时间压缩的更短

Zookeeper诞生历史

雅虎研究室

  • 无单点问题的分布式协调架构,精力集中在处理业务逻辑
  • 内部很多项目都是使用动物的名字来命名
  • 大型动物园

Zookeeper是什么 [底层是Java]

  • Zookeeper是开源的高性能的分布式应用协调系统,一个高性能的分布式数据一致性解决方案

5大特点

  • 顺序一致性
  • 原子性 [全部成功或者全部不成功]
  • 单一视图 [无论连接哪个 都是一致的信息]
  • 可靠性
  • 及时性 [一定时间内能从服务器读到状态]
架构图、集群、工作过程

Zookeeper和CAP的关系

  • CP:一致性+分区容错性
  • 得到一致的数据结果,同时系统对网络具备容错性
  • 但是它不能保证每次服务请求的可用性
作用
  • 分布式服务注册与订阅
  • 统一配置文件
  • 生成分布式唯一ID [/order-0000001、/order-0000002]
  • Master节点选举

针对不能同时进行写数据,保证互斥同步 → 分布式锁

Zookeeper的安装、配置

  • 寻找教辅里的apache-zookeeper-3.6.0-bin.tar
  • 解压压缩包:tar zxvf apache-zookeeper-3.6.0-bin.tar.gz
  • 进入压缩包:cd apache-zookeeper-3.6.0-bin
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ls
bin  conf  docs  lib  LICENSE.txt  NOTICE.txt  README.md README_packaging.md
  • 配置[进入文件]:cp conf/zoo_sample.cfg conf/zoo
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# cp conf/zoo_sample.cfg conf/zoo
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ls conf/
configuration.xsl  log4j.properties  zoo  zoo.cfg  zoo_sample.cfg
  • 打开文件, 进行修改:vi conf/zoo.cfg
找到 dataDir=/tmp/zookeeper
修改成 dataDir=/tmp/lib/zookeeper 以免被自动清除
  • 启动:./bin/zkServer.sh start
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 1877.
======================================================================
[root@iZbp1dssknxftmjczbtpndZ bin]# ./zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... ^[[ASTARTED
[root@iZbp1dssknxftmjczbtpndZ bin]# ./zkServer.sh status
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: standalone
======================================================================
  • 停止:./bin/zkServer.sh stop
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh stop
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED

znode节点 [基本数据模型]

  • 树结构

节点性质
  • 树形结构,也可以理解为linux的文件目录
  • 每一个节点都是znode,里面可以包含数据,也可以有子节点
  • 点分为永久节点临时节点(与客户端绑定) [session失效,也就是客户端断开过,临时节点消失]
  • 每个znode都有版本号,每当数据变化,版本号都会累加(乐观锁)
  • 删除或修改节点,版本号不匹配的话(版本号已超时), 会报错)
  • 每个节点存储的数据不宜过大,几k即可 [保存路径再去查询]
  • 节点可以设置权限,来限制用户的访问
  • Zookeeper保证读和写都是原子操作,且每次读写操作都是对数据的完整读取或完整写入
节点类型
  • 持久节点
  • 临时节点
  • 顺序节点
节点属性
  • dataVersion
  • cversion [child]
  • aclVersion [权限]

常用命令

  • 启动:./bin/zkServer.sh start

  • 连接到Zookeeper

    [root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
    /usr/bin/java
    ZooKeeper JMX enabled by default
    Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
    Starting zookeeper ... STARTED
    
    [root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkCli.sh -server 127.0.0.1:2181
    
  • 查看节点

    [zk: 127.0.0.1:2181(CONNECTED) 3] ls 
    ls [-s] [-w] [-R] path
    [zk: 127.0.0.1:2181(CONNECTED) 4] ls /
    [zookeeper]
    [zk: 127.0.0.1:2181(CONNECTED) 5] ls /zookeeper
    [config, quota]
    [zk: 127.0.0.1:2181(CONNECTED) 6] 
    
    • 查看节点状态:stat /
    [zk: 127.0.0.1:2181(CONNECTED) 6] stat /
    cZxid = 0x0
    ctime = Thu Jan 01 08:00:00 CST 1970
    mZxid = 0x0
    mtime = Thu Jan 01 08:00:00 CST 1970
    pZxid = 0x0
    cversion = -1    //子节点更改的次数
    dataVersion = 0  //数据更改的情况
    aclVersion = 0   //权限修改的情况
    ephemeralOwner = 0x0  //[0是永久节点 其他的是临时节点]
    dataLength = 0
    numChildren = 1  //有几个子节点
    
    • 查看节点的数据和状态:get
    [zk: 127.0.0.1:2181(CONNECTED) 7] get /45
    jj
    
    • 创建、修改、删除节点
    [zk: 127.0.0.1:2181(CONNECTED) 8] create
    create [-s] [-e] [-c] [-t ttl] path [data] [acl]
    
    //创建
    [zk: 127.0.0.1:2181(CONNECTED) 9] create /imooc2
    Created /imooc2
    [zk: 127.0.0.1:2181(CONNECTED) 10] create /imooc3 123
    Created /imooc3
    [zk: 127.0.0.1:2181(CONNECTED) 11] get /imooc3
    123
    
    [zk: 127.0.0.1:2181(CONNECTED) 12] stat /imooc3
    cZxid = 0x5
    ctime = Sun May 05 01:41:21 CST 2024
    mZxid = 0x5
    mtime = Sun May 05 01:41:21 CST 2024
    pZxid = 0x5
    cversion = 0
    dataVersion = 0
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 3
    numChildren = 0
    
    //修改
    [zk: 127.0.0.1:2181(CONNECTED) 13] set /imooc3 456
    [zk: 127.0.0.1:2181(CONNECTED) 14] get /imooc3
    456
    [zk: 127.0.0.1:2181(CONNECTED) 15] stat /imooc3
    cZxid = 0x5
    ctime = Sun May 05 01:41:21 CST 2024
    mZxid = 0x6
    mtime = Sun May 05 01:42:16 CST 2024
    pZxid = 0x5
    cversion = 0
    dataVersion = 1
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 3
    numChildren = 0
    

高级命令

创建节点的高级功能

  • 创建顺序节点 [-s] (会戴上序号)
[zk: 127.0.0.1:2181(CONNECTED) 16] create /imooc4
Created /imooc4
[zk: 127.0.0.1:2181(CONNECTED) 17] create -s /imooc4 /s
Created /imooc40000000003
[zk: 127.0.0.1:2181(CONNECTED) 18] create -s /imooc4 /s
Created /imooc40000000004
  • 临时节点

    ephemeralOwner = 0x0 [0是永久节点 其他的是临时节点]

[zk: 127.0.0.1:2181(CONNECTED) 22] create /imooc
Created /imooc
[zk: 127.0.0.1:2181(CONNECTED) 23] create -e /imooc/tmp 123
Created /imooc/tmp
[zk: 127.0.0.1:2181(CONNECTED) 24] stat /imooc/tmp
cZxid = 0xd
ctime = Sun May 05 01:48:50 CST 2024
mZxid = 0xd
mtime = Sun May 05 01:48:50 CST 2024
pZxid = 0xd
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x10085ad02c90001  //[0x0是永久节点 其他的是临时节点]
dataLength = 3
numChildren = 0
  • 乐观锁
[zk: 127.0.0.1:2181(CONNECTED) 27] set /imooc 6
[zk: 127.0.0.1:2181(CONNECTED) 28] get /imooc
6
[zk: 127.0.0.1:2181(CONNECTED) 29] stat /imooc
cZxid = 0xc
ctime = Sun May 05 01:48:46 CST 2024
mZxid = 0xe
mtime = Sun May 05 01:51:15 CST 2024
pZxid = 0xd
cversion = 1
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 1
numChildren = 1 
//set -v 1 /imooc 9 是因为上面 dataVersion = 1  指定条件版本更新
[zk: 127.0.0.1:2181(CONNECTED) 35] set -v 1 /imooc 9  
[zk: 127.0.0.1:2181(CONNECTED) 36] get /imooc
9
  • 删除命令
[zk: 127.0.0.1:2181(CONNECTED) 38] delete
delete [-v version] path //也可以按照版本号去删除
[zk: 127.0.0.1:2181(CONNECTED) 42] ls /
[imooc, imooc2, imooc3, imooc4, imooc40000000003, imooc40000000004, imooc40000000005, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 43] delete /imooc40000000003
[zk: 127.0.0.1:2181(CONNECTED) 44] ls /
[imooc, imooc2, imooc3, imooc4, imooc40000000004, imooc40000000005, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 45] 

Watcher机制

  • 触发器、监督者
  • 使用场景:统一资源配置 [发生变化时 会給所有监听客户端发送信息]
Watcher事件类型
EventType 触发条件
NodeCreated (节点创建) Watcher监听的对应数据节点被创建
NodeDeleted (节点删除) Watcher监听的对应数据节点被删除
NodeDataChanged(节点数据修改) Watcher监听的对应数据节点的数据内容发生变更
NodeChildrenChanged(子节点变更) Watcher监听的对应数据节点的子节点列表发生变更
ACL
  • access control list 权限控制
  • 它使用权限位来允许/禁止对话节点及其所作用域的各种操作
  • ACL仅与特定的znode有关,与子节点无关
Scheme
  • ACL:[scheme采用的权限机制:id用户:permissions权限组合字符串]
  • world
  • auth [认证登录]
  • digest [密文加密]
  • ip [只允许特定ip访问]
  • super [超级权限]
权限字符串crdwa
  • Create
  • Read
  • Delete
  • Write
  • Admin
[ACL]权限使用场景
  • 区分开发/测试/运维环境,防止误操作
  • 可以针对不同IP而产生具体的配置,更安全

Java原生客户端连接到zookeeper [ZK]

  • 利用ZK原生的Java的API
  • 利用Apache Curator作为客户端来操作ZK
pom.xml
<?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.imooc</groupId>
    <artifactId>zk-practicer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.0</version>
        </dependency>
    </dependencies>

</project>
com/imooc/zkjavaapi/ZKConnect.java
package com.imooc.zkjavaapi;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.omg.CORBA.TIMEOUT;

import java.io.IOException;

/**
 * 连接到ZK服务端,打印连接状态
 */
public class ZKConnect implements Watcher {
    public static final String SERVER_PATH="47.98.225.105:2181";
    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args) throws IOException, InterruptedException {
        //后面new的相当于把这个作为参数传递进去
        //客户端和服务端是异步连接,连接成功之后,客户端会收到watcher通知
        //connectString:服务器的IP+端口号
        //sessionTImeout:超时时间
        //watcher:接收通知事件
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKConnect());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);
        System.out.println(zk.getState());
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        System.out.println("收到了通知" + watchedEvent);
    }
}
===========================================================
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.free=466MB
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.max=7209MB
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.total=487MB
17:35:46 INFO zookeeper.ZooKeeper: Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=5000 watcher=com.imooc.zkjavaapi.ZKConnect@7591083d
17:35:46 INFO common.X509Util: Setting -D jdk.tls.rejectClientInitiatedRenegotiation=true to disable client-initiated TLS renegotiation
17:35:46 INFO zookeeper.ClientCnxnSocket: jute.maxbuffer value is 1048575 Bytes
17:35:46 INFO zookeeper.ClientCnxn: zookeeper.request.timeout value is 0. feature enabled=false
客户端开始连接ZK服务器了
CONNECTING
17:35:46 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:35:46 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:35:46 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:50517, server: 127.0.0.1/127.0.0.1:2181
17:35:46 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710001, negotiated timeout = 5000
收到了通知WatchedEvent state:SyncConnected type:None path:null
CONNECTED
log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} %p %c{2}: %m%n
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 4147.

windows环境下安装zookeeper教程详解(单机版)_windows zooke-CSDN博客

用代码对节点进行操作

com/imooc/zkjavaapi/ZKOperator.java
package com.imooc.zkjavaapi;

import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

/**
 * 描述:     演示对节点的操作,包含创建、读取、删除等。
 */
public class ZKOperator implements Watcher {

    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args)
            throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKOperator());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);

        /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
        System.out.println(zk.create("/imooc-create-node2", "imooc2".getBytes(), Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT));
    }

    @Override
    public void process(WatchedEvent event) {
    }
}
==========================================================================
客户端开始连接ZK服务器了
CONNECTING
17:57:14 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:57:14 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:57:14 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:57443, server: 127.0.0.1/127.0.0.1:2181
17:57:14 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710008, negotiated timeout = 5000
/imooc-create-node2
==========================================================================
==========================================================================
 /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
//        System.out.println(zk.create("/imooc-create-node2", "imooc2".getBytes(), Ids.OPEN_ACL_UNSAFE,
//                CreateMode.PERSISTENT));
//        zk.setData("/imooc-create-node", "imooc3".getBytes(), 1);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data));
    }
==========================================================================
客户端开始连接ZK服务器了
CONNECTING
17:58:09 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:58:09 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:58:09 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:57766, server: 127.0.0.1/127.0.0.1:2181
17:58:09 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710009, negotiated timeout = 5000
imooc2
version版本不一样 保证不做修改
com/imooc/zkjavaapi/ZKOperator.java
修改值 让版本变成1
 zk.setData("/imooc-create-node2", "imooc3".getBytes(), 1);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data)); 
----------------------------------------------------------------------------
Exception in thread "main" org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for /imooc-create-node2
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:122)
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:54)
    at org.apache.zookeeper.ZooKeeper.setData(ZooKeeper.java:2551)
    at com.imooc.zkjavaapi.ZKOperator.main(ZKOperator.java:41)

报错=>版本不一致
===========================================================================
[修改]
 zk.setData("/imooc-create-node2", "imooc3".getBytes(), 0);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data));
----------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:01:35 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:01:35 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:01:35 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:58870, server: 127.0.0.1/127.0.0.1:2181
18:01:35 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec0871000d, negotiated timeout = 5000
imooc3 [修改成功]
删除节点 [引入回调函数+休眠]
com/imooc/zkjavaapi/ZKOperator.java
package com.imooc.zkjavaapi;

import java.io.IOException;

import com.imooc.zkjavaapi.callback.DeleteCallBack;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

/**
 * 描述:     演示对节点的操作,包含创建、读取、删除等。
 */
public class ZKOperator implements Watcher {

    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args)
            throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKOperator());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);

        /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
        zk.create("/imooc-create-node3", "imooc3".getBytes(), Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
//        zk.setData("/imooc-create-node2", "imooc3".getBytes(), 0);
//        byte[] data = zk.getData("/imooc-create-node2", null, null);

        String ctx = "删除成功"; //把ctx的内容代入到DeleteCallBack()里面去运行
        zk.delete("/imooc-create-node3",0,new DeleteCallBack(),ctx);
        Thread.sleep(2000);
//        System.out.println(new String(data));
    }

    @Override
    public void process(WatchedEvent event) {
    }
}
--------------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:10:01 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:10:01 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:10:01 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:2600, server: 127.0.0.1/127.0.0.1:2181
18:10:01 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec0871000f, negotiated timeout = 5000
删除节点/imooc-create-node3
删除成功
com/imooc/zkjavaapi/callback/DeleteCallBack.java
package com.imooc.zkjavaapi.callback;

import org.apache.zookeeper.AsyncCallback;

/**
 * 删除后运行的方法
 */
public class DeleteCallBack implements AsyncCallback.VoidCallback {
    @Override
    public void processResult(int rc, String path, Object ctx) {
        System.out.println("删除节点" + path);
        System.out.println((String)ctx);
    }
}

处理Watcher事件

com/imooc/zkjavaapi/ZKGetNode.java
package com.imooc.zkjavaapi;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

/**
 * 和节点相关:是否存在,获取数据,加上Watch
 */
public class ZKGetNode implements Watcher {
    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    //这个是门闩
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKGetNode());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);
        System.out.println(zk.getState());

//        Stat exists = zk.exists("/imooc-create-node", false);//不需要额外监听
//        if (exists != null){
//            System.out.println("节点的版本为: "+exists.getVersion());
//        }else{
//            System.out.println("该节点不存在");
//        }
        zk.getData("/imooc-create-node", true, null);
        countDownLatch.await();
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeChildrenChanged){
            System.out.println("数据被改变");
            countDownLatch.countDown();
        }
        System.out.println("收到了通知" + event);
    }
}
--------------------------------------------------------------------------------
在运行的情况下 去cmd中 修改
[zk: localhost:2181(CONNECTED) 1] set /imooc-create-node 11
--------------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:52:14 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:52:14 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:52:14 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:16204, server: 127.0.0.1/127.0.0.1:2181
18:52:14 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x1000534afde0001, negotiated timeout = 5000
收到了通知WatchedEvent state:SyncConnected type:None path:null
CONNECTED
数据被改变
收到了通知WatchedEvent state:SyncConnected type:NodeDataChanged path:/imooc-create-node

用Curator操作ZK

原生的Java的API的缺点
  • 不支持连接超时后的自动连接
  • Watcher注册一次后会失效
  • 不支持递归创建节点
利用Apache Curator
  • 解决了Watcher注册一次后会失效的问题
  • API更加简单易用,提供了工具类
com/imooc/curator/CuratorTests.java
package com.imooc.curator;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

/**
 * 用Curator来操作ZK
 */
public class CuratorTests {
    public static void main(String[] args) throws Exception {
        String connectString = "127.0.0.1:2181";
        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retry);
        client.start();
        String path = "/curator";
        String data = "test";
        client.create().withMode(CreateMode.PERSISTENT).forPath(path,data.getBytes());
        byte[] bytes = client.getData().forPath(path);
        System.out.println(new String(bytes));
    }
}
更改高级一点!!!【添加+修改+删除】
com/imooc/curator/CuratorTests.java
package com.imooc.curator;

import com.sun.net.httpserver.Authenticator.Retry;
import java.text.MessageFormat;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher.Event.EventType;

/**
 * 描述:     用Curator来操作ZK
 */
public class CuratorTests {

    public static void main(String[] args) throws Exception {
        String connectString = "127.0.0.1:2181";
        String path = "/curator2";

        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retry);
        client.start();
        client.getCuratorListenable().addListener((CuratorFramework c, CuratorEvent event) -> {
            switch (event.getType()) {
                case WATCHED:
                    WatchedEvent watchedEvent = event.getWatchedEvent();
                    if (watchedEvent.getType() == EventType.NodeDataChanged) {
                        System.out.println(new String(c.getData().forPath(path)));
                    }
            }
        });
        String data = "test";
        String data2 = "test2";
        //添加
        client.create().withMode(CreateMode.PERSISTENT).forPath(path, data.getBytes());

        byte[] bytes = client.getData().watched().forPath(path);
        System.out.println(new String(bytes));
        //更改
        client.setData().forPath(path, data2.getBytes());
        //删除
        client.delete().forPath(path);
        Thread.sleep(2000); //保证足够时间运行成功
    }
}
-------------------------------------------------------------------------------------
19:44:41 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
19:44:41 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
19:44:41 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:32984, server: 127.0.0.1/127.0.0.1:2181
19:44:41 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x1000534afde0005, negotiated timeout = 40000
19:44:41 INFO state.ConnectionStateManager: State change: CONNECTED
test
test2

Dubbo [RPC远程过程调用]

  • 初识Dubbo
  • RPC介绍
  • Dubbo工作原理
  • 案例实操:项目编写
  • 整合Dubbo和Zookeeper
  • 实现服务间调用

初始Dubbo

Dubbo是什么
  • 轻量级、高性能的RPC框架
  • 并不是要成为一个微服务的全面解决方案
  • 以Java语言而出名
Dubbo现状
  • 全称是Apache Dubbo
  • 微店、网易云音乐、滴滴、中国电信、中国人寿
  • star有30K+个,fork有20K+个
Dubbo的故事
  • 09年开始做,做的第一个版本
  • 10年初的时候,架构升级,Dubbo2.0
  • 开源
  • one company战略
  • 合到HSF去
  • 第3节点,捐给Apache
开源的理解
  • 共同成长、巨人的肩膀上
  • 演化慢、不断革新、很强大的生命力
  • 突破任何的束缚,突破任何的常规,包容和开放

RPC介绍

  • RPC ——远程过程调用
  • 早期单机时代:IPC
  • 网络时代:把IPC扩展到网络上,这就是RPC
  • 实现RPC很头疼,于是有了RPC框架
  • 调用其他机器上的程序和调用本地的程序一样方便
常见的RPC框架
  • 阿里的Dubbo
  • 新浪的Montan
  • Facebook的Thrift
  • 各个框架都有其各自的优缺点
HTTP和RPC对比
  • 普通话[通用] 与 方言[企业内部]
  • 普通话本质上也是一种方言,只不过它是官方的方言
  • 传输效率
    • RPC定制自己传输请求让传输效率更高
    • HTTP会包含一些无用的内容效率较低
  • 性能消耗,主要在于序列化和反序列化的耗时
  • 负载均衡

Dubbo工作原理

一旦注册中心的信息有变化的时候会主动推送信息
  • 服务容器负责启动,加载,运行服务提供者
  • 服务提供者在启动时,向注册中心注册自己提供的服务
  • 服务提供者在启动时,向注册中心订阅自己所需的服务
  • 注册中心返回服务提供者地址列表给消费者
  • 从提供者地址列表中,选一台提供者进行调用
  • 定期发送一次统计数据到监控中心

模块 说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的控制中心
Container 服务运行容器

服务提供者开发

案例实操
  • 引入依赖
  • 添加注解
  • 整合Dubbo和Zookeeper
pom.xml[dubbo-practice]
<?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>
    <packaging>pom</packaging>
    <modules>
        <module>producer</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>dubbo-practice</artifactId>
    <version>0.0.1</version>
    <name>dubbo-practice</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.1.12.RELEASE</spring-boot.version>
        <dubbo.version>2.7.4.1</dubbo.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- Apache Dubbo  -->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-dependencies-bom</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>javax.servlet</groupId>
                        <artifactId>servlet-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>log4j</groupId>
                        <artifactId>log4j</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>
pom.xml[dubbo-practice-producer]
<?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>dubbo-practice</artifactId>
        <groupId>com.imooc</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

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

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

        <!-- Dubbo Spring Boot Starter -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <!-- Zookeeper dependencies -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-dependencies-zookeeper</artifactId>
            <version>${dubbo.version}</version>
            <type>pom</type>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Web 功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- MySQL connector, 需要与 MySQL 版本对应 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- MyBatis依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
    </dependencies>
</project>
com/imooc/producer/service/CourseListService.java
package com.imooc.producer.service;

import com.imooc.producer.entity.Course;

import java.util.List;

/**
 * 课程列表服务
 */
public interface CourseListService {
    List<Course> getCourseList();
}
com/imooc/producer/entity/Course.java
package com.imooc.producer.entity;

import java.io.Serializable;

/**
 * 描述:     Course实体类
 */
public class Course implements Serializable {

    Integer id;
    Integer courseId;
    String name;
    //1上架,0下架
    Integer valid;

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", name='" + name + '\'' +
                ", valid=" + valid +
                '}';
    } Getter+Setter
}
com/imooc/producer/service/impl/CourseListServiceImpl.java
package com.imooc.producer.service.impl;

import com.imooc.producer.entity.Course;
import com.imooc.producer.mapper.CourseMapper;
import com.imooc.producer.service.CourseListService;
import java.util.List;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 描述:     课程列表服务实现类
 */
@Service(version = "${demo.service.version}")
public class CourseListServiceImpl implements CourseListService {

    @Autowired
    CourseMapper courseMapper;

    public List<Course> getCourseList() {
        return courseMapper.findValidCourses();
    }
}
com/imooc/producer/mapper/CourseMapper.java
package com.imooc.producer.mapper;

import com.imooc.producer.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Mapper类
 */
@Mapper
@Repository
public interface CourseMapper {
    @Select("SELECT * FORM course WHERE valid = 1")
    List<Course> findValidCourses();
}
application.properties
demo.service.version=1.0.0

#server.port=8081

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_prepare?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}


spring.application.name=course-list

#dubbo协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
#dubbo注册
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.file=${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache

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

dubbo.scan.base-packages=com.imooc.producer.service.impl
com/imooc/producer/DubboProducerApplication.java
package com.imooc.producer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 描述:     Spring Boot启动类
 */
@EnableAutoConfiguration
public class DubboProducerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboProducerApplication.class, args);
    }
}

服务消费方开发

查看PID为8080:netstat -ano|findstr 8080
杀死进程:taskkill /pid 查询的PID /f

pom.xml[dubbo-practice-consumer]
<?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>dubbo-practice</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

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

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

    <!-- Dubbo Spring Boot Starter -->
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.7.4.1</version>
    </dependency>

    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo</artifactId>
    </dependency>
    <!-- Zookeeper dependencies -->
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-dependencies-zookeeper</artifactId>
      <version>${dubbo.version}</version>
      <type>pom</type>
      <exclusions>
        <exclusion>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <!-- Web 功能 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MySQL connector, 需要与 MySQL 版本对应 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- MyBatis依赖-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
    <dependency>
      <groupId>com.imooc</groupId>
      <artifactId>producer</artifactId>
      <version>0.0.1</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>
com/imooc/consumer/service/CoursePriceService.java
package com.imooc.consumer.service;

import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import java.util.List;

/**
 * 描述:     课程价格服务
 */
public interface CoursePriceService {

    CoursePrice getCoursePrice(Integer courseId);

    List<CourseAndPrice> getCoursesAndPrice();
}
com/imooc/consumer/service/impl/CoursePriceServiceImpl.java
package com.imooc.consumer.service.impl;

import com.imooc.consumer.dao.CoursePriceMapper;
import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import com.imooc.consumer.service.CoursePriceService;
import com.imooc.producer.entity.Course;
import com.imooc.producer.service.CourseListService;
import java.util.ArrayList;
import java.util.List;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 描述:     课程 价格服务
 */
@Service
public class CoursePriceServiceImpl implements CoursePriceService {

    @Autowired
    CoursePriceMapper coursePriceMapper;

    @Reference(version = "${demo.service.version}")
    CourseListService courseListService;

    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrices(courseId);
    }

    @Override
    public List<CourseAndPrice> getCoursesAndPrice() {
        List<CourseAndPrice> courseAndPriceList = new ArrayList<>();
        List<Course> courseList = courseListService.getCourseList();
        for (int i = 0; i < courseList.size(); i++) {
            Course course = courseList.get(i);
            if (course != null) {
                CoursePrice price = getCoursePrice(course.getCourseId());
                if (price != null && price.getPrice() > 0) {
                    CourseAndPrice courseAndPrice = new CourseAndPrice();
                    courseAndPrice.setId(course.getId());
                    courseAndPrice.setCourseId(course.getCourseId());
                    courseAndPrice.setName(course.getName());
                    courseAndPrice.setPrice(price.getPrice());
                    courseAndPriceList.add(courseAndPrice);
                }
            }
        }
        return courseAndPriceList;
    }
}
com/imooc/consumer/dao/CoursePriceMapper.java
package com.imooc.consumer.dao;

import com.imooc.consumer.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 描述:     Mapper类
 */
@Mapper
@Repository
public interface CoursePriceMapper {

    @Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
    CoursePrice findCoursePrices(Integer courseId);
}
com/imooc/consumer/entity/CourseAndPrice.java
package com.imooc.consumer.entity;

import java.io.Serializable;

/**
 * 描述:     CoursePrice实体类
 */
public class CourseAndPrice implements Serializable {

    Integer id;
    Integer courseId;
    String name;
    Integer price;

    @Override
    public String toString() {
        return "CourseAndPrice{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", name='" + name + '\'' +
                ", price=" + price +
                '}';
    } Getter+Setter
}
com/imooc/consumer/entity/CoursePrice.java
package com.imooc.consumer.entity;


import java.io.Serializable;

/**
 * 描述:     CoursePrice实体类
 */
public class CoursePrice implements Serializable {

    Integer id;
    Integer courseId;
    Integer price;
} Getter+Setter
com/imooc/consumer/controller/CoursePriceController.java
package com.imooc.consumer.controller;

import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import com.imooc.consumer.service.CoursePriceService;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:CoursePriceController
 */
@RestController
public class CoursePriceController {

    @Autowired
    CoursePriceService coursePriceService;


    @GetMapping({"/price"})
    public Integer getCoursePrice(Integer courseId) {
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        if (coursePrice != null) {
            return coursePrice.getPrice();
        } else {
            return -1;
        }
    }


    @GetMapping({"/coursesAndPrice"})
    public List<CourseAndPrice> getcoursesAndPrice() {
        return coursePriceService.getCoursesAndPrice();
    }
}
application.properties
demo.service.version=1.0.0

server.port=8084

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}


spring.application.name=course-price

#dubbo协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
#dubbo注册
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.file=${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache
com/imooc/consumer/DubboConsumerApplication.java
package com.imooc.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 描述:     Spring Boot启动类
 */
@SpringBootApplication
public class DubboConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboConsumerApplication.class, args);
    }
}
案例实操总结
  • 自动检查zk和依赖的服务
  • dubbo.scan.base-packages配置
  • 实现服务间调用

面试课

  • Spring Boot常见面试题
  • 线程常见面试题
  • 分布式的面试题
  • Docker相关面试题
  • Nginx和Zookeeper相关面试题
  • RabbitMQ相关面试题
  • 微服务相关
  • 彩蛋:学习方法
  • 锁分类、死锁
  • HashMap和final
  • 单例模式
  • 面试避坑指南
  • 重要的软实力

Spring、Spring Boot和Spring Cloud的关系?

  • Spring最初利用IOCAOP解耦
  • 按照这种模式搞了MVC框架 [之后就配置太繁琐了]
  • 写了很多样板代码很麻烦,就有了Spring Boot
  • Spring Boot是在强大的Spring帝国发展起来的,发明Spring Boot是为了让人们更好更高效的使用Spring,Spring Boot理念是约定优于配置
  • Spring Cloud是在Spring Boot基础上诞生的 [一系列框架的有序集合]

Spring Boot如何配置多环境

  • 开发、测试、预发、生产

面试官你好,我这边平时是会使用多套环境,比如说”开发、测试、预发、生产”环境。
开发环境通常在本地,它所连接的数据库也是专门用于开发的,里面的数据也是一定情况下算出来的,因为并不需要在开发环境的情况下保证数据的完全精确,为了开发效率的提高,我们通常造一些模拟的数据,通常开发完后我们要把程序部署到测试环境,因为测试环境通常是公司所提供的服务器,而开发环境通常是我们本机,对于本机而言如果关闭或关机后别人就无法访问了,但是测试的同学工作时间不一定能和开发的同学一致,如果把程序关掉了他们就没办法测试了。我们需要给测试同学提供一套稳定的环境去测试。而且有的时候会同时开发多种功能,前一个功能开发完了需要去测试,这个时候就要去开发新的功能了,此时本地的代码已经发生了变化,如果把开发环境当成测试环境的话会发生很多问题,它实际测试的和我们想要测试的不是同一套代码,正是这个原因测试环境是必不可少的,需要用一台稳定的服务器把我们开发好的部署上测试环境中去,这样的话无论电脑是否关机都不会影响测试人员的进度。但是在测试环境的数据库往往可以和开发环境的保持一致可以允许公用同一个数据。
预发环境是预备发布,和真正的线上环境高度统一,和测试环境的区别:
1.网络隔离 为了保证线上环境的稳定会采取环境隔离,在本地或者测试环境下是没有办法访问到预发环境的机器,不可直接访问。在预发环境通常采用真实的数据库去测试。在测试环境并不能把所有问题都测试出来,所以在测试环境中无法测试到的问题在预发环境就可以暴露出来了,有时候在测试环境中模拟的数据不是准确,比如模拟一个商品详情,报的是50个字,最后发现真实情况是100个字,就能看到数据库大小不够,再次比如测试的是整数,到真实环境中发现是小数。隔离+数据验真
生产环境是真实对外的数据,也会有很多流量进来,直接面向所有用户,也有并发问题,要确保数据稳定

  • 提供多套配置文件

在发布到某个环境之前,不建议把配置文件全部删除替换,有可能漏了文件导致了错误的替换,如果发布环境是测试环境的数据库,有可能会产生对外暴露的是测试环境的情况,这是很严重的事故

  • 通过改变application里的profiles.active值来加载对应的环境
com/imooc/profiles/ProfilesApplication.java
package com.imooc.profiles;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProfilesApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProfilesApplication.class, args);
    }
}
application.properties

spring.profiles.active=prod
application-pre.properties

spring.profiles.active=test
server.port=8082
application-prod.properties

spring.profiles.active=test
server.port=8083
application-test.properties

spring.profiles.active=test
server.port=8081

实际工作中,如何全局处理异常?

  • 为什么异常需要全局处理?不处理行不行?

如果我们不进行处理的话,异常可能会把整个堆栈抛出去,一旦发生异常,用户或者别用用心的黑客可以看到详细的异常发生情况,包含详细的错误信息和代码的行数,这样的话对方可以利用一个漏洞进行不同的尝试,而且可以顺藤摸瓜分析出更多潜在的风险,最终把系统攻击破,异常是必须处理的。
但为什么要全局处理呢?电商项目→exception→GlobalExceptionHandler

  • GlobalExceptionHandler [使用全局处理]
    识别到什么异常,调用什么其处理器。写了全局异常处理器,轻松的针对不同的异常做出定制化的解决方案,不但增加了安全性,对用户也是友好的
package com.imooc.mall.exception;

import com.imooc.mall.common.ApiRestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

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

/**
 * 19.处理统一异常的handler 业务异常 处理不同逻辑异常  20对密码进行MD5加密UserServiceImpl 先创建MD5Utils
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //   统一处理Exception.class异常 所有异常的父类
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e) {
        log.error("Default Exception: ", e);
        return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);
    }
    
    // 处理自己所定义的异常 用户/密码不能为空......
    @ExceptionHandler(ImoocMallException.class)
    @ResponseBody
    public Object handleImoocMallException(ImoocMallException e) {
        log.error("ImoocMallException: ", e); //传进来的是什么就传出去
        return ApiRestResponse.error(e.getCode(), e.getMessage());
    }

    //  39.处理方法参数不合规的情况
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ApiRestResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException: ", e);
        return handleBindingResult(e.getBindingResult());
    }
//  40.处理返回异常的ApiRespond 41去pom引入Swagger自动生成API文档
    private ApiRestResponse handleBindingResult(BindingResult result){
//  把异常处理为对外暴露的提示
        List<String> list = new ArrayList<>();
        if (result.hasErrors()){
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError objectError : allErrors) { //itli快速  对着for按alt+回车 改成增强for
                String message = objectError.getDefaultMessage();
                list.add(message);
            }
        }
        if (list.size() == 0){
            return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR);
        } //list.toString()生成所创建的异常描述信息
        return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR.getCode(), list.toString());
    }
}

线程如何启动?

Thread.start.run

  • 既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?

因为它只是一个普通的java代码,而不会真正的启动一个线程,调用一次run()方法只执行一次,而且是在主线程执行的,就没有起到任何创建线程的效果了。
如果选择start方法的话会在后台执行很多操作,比如去申请一个线程、让子方法去执行run()里的内容,而且还包括执行之后的对线程状态的调整。所以说表面上是相同都是执行一段代码,但是实际上是不同的。

  • 两次调用start()方法会出现什么情况?

两次调用start()方法会报异常,异常类型叫做IllegalThreadStateException,在start()的时候首先会进行线程状态的检测只有是new的时候才能正常启动,不允许启动两次

com/imooc/interniew/StartTwice.java
package com.imooc.interniew;

/**
 * 描述:     两次启动线程
 */
public class StartTwice {

    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();
        thread.start();
    }
}
======================== 报错信息  ==========================
Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Thread.java:705)
    at com.imooc.interniew.StartTwice.main(StartTwice.java:11)
Thread.java
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

实现多线程的方法有几种?

  • 方法一:实现Runnable接口
com/imooc/interniew/createthreads/RunnableStyle.java
package com.imooc.interniew.createthreads;

import java.util.concurrent.Callable;

/**
 * 描述:     用Runnable方式创建线程
 */
public class RunnableStyle implements Runnable {
//new里new的意思是 把这个Runnable类作为参数传进Thread里面
    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("用Runnable方法实现线程");
    }
}
  • 方法二:继承Thread类
package com.imooc.interniew.createthreads;

import java.util.Timer;
import java.util.TimerTask;

/**
 * 描述:     利用定时器新建线程
 */
public class TimerDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }, 1000, 1000);
    }
}

两种方式的对比

方法1(实现Runnable接口更好)

实现多线程——常见面试问题
  • 实现Runnable接口和继承Thread类哪种方式更好?

    • 从代码架构角度

    本意是想让我们的执行类和任务的具体内容解耦,关系不那么密切,从架构角度好
    ★ Runnable具体描述的是工作的内容和线程的启动没有什么关系
    ★ Thread是维护整个线程的: 线程的启动、线程状态更改、线程结束,这两个本身的任务很分明的,不应该过度耦合[未来会发生很难扩展的问题]

    • 新建线程损耗

    ★ Runnable 在线程池更高级的用法中,一定不是每个任务都去新建一个线程的,为了提高整体的效率会让有限数量的线程由我们自己来确定,10个线程可以运行成千上万个任务。减少了新建线程的损耗。
    可以把任务作为一个参数直接传递给线程池,线程池里面用固定的线程去执行任务不需要每次都新建和销毁线程,这样大大降低了线程的开销。

    ★ Runnable 如果用这个不得不去把线程损耗承担起来,有的时候run方法执行的比较少,开销的少比不上新建线程的开销[捡了芝麻丢了西瓜]。

    • Java不支持双继承

    public class ThreadStyle extends Thread,Date (×)
    Class cannot extend multiple classes

    从语法的角度不允许继承多个类,一旦选定了一个父类就无法更改了[一辈子就被定死了]。在创建线程起就限制了代码的可扩展性,如果实现Runnable接口就不会出现这个问题,实现接口并不仅仅只能实现一个,实现接口后还可以继承类

    public class RunnableStyle extends Thread implements Runnable,Callable

两种方法的本质对比

方法一:最终调用target.run();

此方法本质是传入类后调用!
Ctrl+F12可以精确查找方法
@Override
public void run(){
if(target != null){
target.run()
}
}
而target是什么呢?实际上就是我们写的
@Override
public void run() {
System.out.println(“用Runnable方法实现线程”);
}

方法二:run()整个都被重写

整个重写代码

若同时使用这两种方法会发生什么?

  • 从面向对象的思想去考虑
com/imooc/interniew/createthreads/BothRunnableThread.java
package com.imooc.interniew.createthreads;
/**
 * 描述:     同时使用RUNNABLE和Thread两种方式实现线程
 */
public class BothRunnableThread {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable接口的方式");
            }
        }) {
            @Override
            public void run() {
                System.out.println("我来自Thread");
            }
        };
        t1.start();
    }
}
=========================================================
我来自Thread
---------------------------------------------------------
因为 run重写会被覆盖!!子类覆盖父类时 实行子类方法
@Override
public void run(){
    if(target != null){
        target.run()
    }
}
其他观点分析
  • 线程池创建线程也算是一种新建线程的方式 [把那两种方式进行包装]

  • 通过Callable创建线程,也算是一种新建线程的方式

  • 定时器[方法二:extends Thread]

    package com.imooc.interniew.createthreads;
    
    import java.util.Timer;
    import java.util.TimerTask;
    
    /**
     * 描述:     利用定时器新建线程
     */
    public class TimerDemo {
    
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getName());
            Timer timer = new Timer();
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            }, 1000, 1000);
        }
    }
    =======================================================
    main     //两个不一致证明新建了一个线程
    Timer-0
    Timer-0
    ......
    
  • 匿名内部类

  • Lambda表达式

实现多线程——常见面试问题
  • 有多少种实现线程的方法?5点思路
    • 不同的角度看,会有不同的答案
    • 经典答案是
    • 我们看原理,两种本质都是一样的
    • 具体展开说其他方式
总结:最精准的描述

准确地讲:创建线程只有一种方法那就是构造Thread类,而实现线程的执行单元有两种方式
★ 方法一:实现Runnable接口的run方法,并把Runnable实例传給Thread类
★ 方法二:重写Thread的run方法(继承Thread类)
多线程的实现方法,在代码种写法千变万化,但其本质万变不离其宗

线程的生命周期是什么?

线程有几种状态?
  • 有哪6种状态
  • 每个状态是什么含义?
  • 状态间的转化?
  • 阻塞状态是什么?
每个状态是什么含义?
  • New
  • Runnable [从new到调用start方法]
  • Blocked [线程状态由sychronized修饰]
  • Waiting
  • Timed Waiting
  • Terminated

状态转换的注意点和阻塞

com/imooc/interniew/NewRunnableTerminated.java
package com.imooc.interniew;

/**
 * 描述:     演示New、Runnable、Terminated状态。
 */
public class NewRunnableTerminated {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread();
        //打印出NEW的状态 线程被创建但没有启动会打出new状态
        System.out.println(thread.getState());
        thread.start();
        //打印出Runnable状态 线程被启动后打印runnable状态
        System.out.println(thread.getState());
        Thread.sleep(100);
        //打印出TERMINATED状态 打印terminate状态
        System.out.println(thread.getState());
    }
}
=================================================================================
NEW
RUNNABLE
TERMINATED

Process finished with exit code 0
com/imooc/interniew/BlockedWaitingTimedWaiting.java
package com.imooc.interniew;

/**
 * 描述:     展示Blocked、Waiting、Timed_Waiting状态
 */
public class BlockedWaitingTimedWaiting implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new BlockedWaitingTimedWaiting();
        Thread t1 = new Thread(runnable);
        t1.start();
        Thread t2 = new Thread(runnable);
        t2.start();
        Thread.sleep(10);
        //打印Timed_Waiting状态,因为正在执行Thread.sleep(1000);
        System.out.println(t1.getState());
        //打印出BLOCKED状态,因为t2拿不到synchronized锁[线程1还在休眠]
        System.out.println(t2.getState());

        Thread.sleep(1300);
        //打印出WAITING状态,以为执行了wait()
        System.out.println(t1.getState());
    }

    @Override
    public void run() {
        syn();
    }

    private synchronized void syn() { //锁!!
        try {
            Thread.sleep(1000);
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
=================================================================================
TIMED_WAITING
BLOCKED
WAITING
阻塞状态
  • 一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态
  • 不仅仅是Blocked

分布式面试题

什么是分布式
  • 饭店厨师的例子
    • 一个厨师
    • 多个厨师
    • 术业有专攻:配菜师、洗菜工
  • 实际项目的演进过程
    • 一个项目,大而全
    • 多台机器,部署同样的应用
    • 分布式:权限系统、员工系统、请假系统
分布式和单体结构哪个更好?[脱离业务场景和发展阶段的空谈就是耍流氓]
传统单体架构 分布式架构
新人的学习成本 业务逻辑成本高 架构逻辑成本高
部署、运维 容易 发布频繁、发布顺序复杂、运维难
隔离性 一损俱损,殃及鱼池 故障影响范围小

CAP理论是什么?[只选其二 三者不可兼得]

  • C(Consistency, 一致性):读操作是否总能读到前一个写操作的结果
  • A(Availability, 可用性):非故障节点应该在合理的时间内做出合理的响应
  • P(Partition tolerance, 分区容错性):当出现网络分区现象后,系统能够继续运行
CAP怎么选?
  • CP或者AP
  • 在什么场合,可用性高于一致性?

为什么需要Docker?

  • Docker:用来装程序以及环境的容器
  • 环境配置的难题
  • 虚拟机
Docker的用途是什么?
  • 提供统一的环境
  • 提供快速拓展、弹性伸缩的云服务
  • 防止其他用户的进程把服务器资源占用过多

Docker的架构是什么样的?

Docker的网络模式有哪些?
  • Bridge [桥接 用外面主机的端口号映射到里面的端口号 实现了一座桥]
  • Host [里面的容器不会获得独立的网络配置 不会使用虚拟网卡ip 而是使用宿主机上的ip和端口号]
  • None [不需要网络模式]

Nginx的适用场景有哪些?

  • HTTP的反向代理服务器

  • 动态静态资源分离

    • 不分离会变慢
    • 静态资源无需经过Tomcat,Tomcat只负责处理动态请求
    • 后缀为gif的时候,Nginx会直接获取到当前请求的文件并返回
    • 静态资源服务器

Nginx常用命令有哪些?

/usr/sbin/nginx 启动
-h 帮助
-c 读取指定的配置文件
-t 测试
-v 版本
-s信号
  stop 立即停止(不再接收任何请求立刻停止)  
  quit 优雅停止(不接收了但目前的请求要作完)
  reload 重启
  reopen 更换日志文件

Zookeeper有哪些节点类型?

  • 持久节点
  • 临时节点
  • 顺序节点

为什么要用消息队列?什么场景用?

  • 系统解耦
  • 异步调用
  • 流量削峰
消息队列RabbitMQ核心概念?

同一个RabbitMQ的Server下建立不同的虚拟主机,他们之间是相互独立的,用于不同的业务线。

交换机工作模式有哪4种?
  • fanout:广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的

  • direct:根据RoutingKey匹配消息路由到指定队列 [消费者接收消息不一致]

  • topic:比如消息严重性怎么样、只想记录error模块的用户信息

    • ***** 可以替代一个单词

    • # 可以替代零个或多个单词

  • headers:根据发送消息内容中的headers属性来匹配

微服务面试题

微服务有哪两大门派?

  • Spring Cloud:众多子项目
  • dubbo:高性能、轻量级的开源RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • dubbo提供的能力只是SpringCloud的一部分子集
Spring Cloud核心组件有哪些?
核心组件 Spring Cloud
服务注册中心 Spring Cloud Netflix Eureka
服务调用 Spring Cloud Netflix Feign
服务网关 Spring Cloud Netflix Zuul
断路器 Spring Cloud Netflix Hystrix
画一下Eureka架构
  • EureKa Server 和 EureKa Client

  • 集群 [只要能获得一个Eureka Server 就能获得整个信息]

负载均衡的两种类型是什么?
  • 客户端负载均衡(Ribbon)
  • 服务端负载均衡(Nginx)
负载均衡有哪些策略?
  • RandomRule表示随机策略
  • RoundRobinRule表示轮询策略
  • ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
为什么需要断路器?

防止线程突然卡住,当发现某个模块不可用时,把它摘除不影响主要流程。

为什么需要网关?
  • 签名校验、登录校验冗余问题
  • 统一对外,安全 [对恶意IP进行拦截 打出日志]
Dubbo的工作流程是什么?

彩蛋:学习编程知识的优质路径

  • 宏观上
  1. 并不是靠工作年限,有的人工作5年技术却还是只懂皮毛
  2. 要有强大的责任心,不放过任何bug,找到原因并去解决,这就是提高
  3. 主动:永远不会觉得自己的时间多余,重构、优化、学习、总结等
  4. 敢于承担:虽然这个技术难题以前没有碰到过,但是在一定的了解调研后,敢于承担技术难题,让工作充满挑战,这一次次攻克难关的过程种,进步是飞速的
  5. 关心产品,关心业务,而不是只写代码
  • 微观上
  1. 系统化的学习 看经典的书籍
  2. 看官方文档
  3. 自己动手写代码,尝试应用到项目中
  4. 不理解的内容参考多个知识来源,综合判断
  5. 学习开源项目,总结代码

Synchronized和Lock

Lock简介、地址、作用
  • 锁是一种工具,用于控制对共享资源的访问
  • Lock和Synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
  • Lock并不是用来替代Synchronized的,而是当使用Synchronized不合适或不满足要求的时候,来提供高级功能
  • Lock接口最常见的实现类是ReentrantLock
  • lock()、tryLock()、tryLock(long time, TimeUnit unit)locakInterruptibly()
lock()
  • lock()就是最普通的获取锁。如果锁已经被其他线程获取,则进行等待
  • Lock不会像Synchronized一样在异常时自动释放锁
  • 因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
  • lock()方法不能被中断,这就会带来很大隐患:一旦陷入死锁,lock()就会陷入永久等待
tryLock()
  • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
  • 相对比lock,这样的方法显然功能更加强大了,我们可以根据是否能获取到锁来决定后续程序的行为
  • 该方法会立即返回,即便在拿不到锁时不会一直在那等
tryLock(long time, TimeUnit unit):超时就放弃

locakInterruptibly():相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断

unlock():解锁 [最应该写在try…finally里面]

Synchronized和Lock有什么不同?

相同点:
  • 保障资源线程的安全:目的和作用都是为了 保障资源线程的安全
    [使用Synchronized后被保护的代码块最多只有一个线程可以访问]
  • 可重入 [不然就必须在获得第二个锁前释放]
com/imooc/interniew/Reentrant.java
package com.imooc.interniew;

/**
 * 描述:     synchronized可重入
 */
public class Reentrant {

    public synchronized void f1() {
        System.out.println("f1方法被运行了");
        f2();
    }

    public synchronized void f2() {
        System.out.println("f2方法被运行了");
    }

    public static void main(String[] args) {
        Reentrant reentrant = new Reentrant();
        reentrant.f1();
    }
}
========================================================
f1方法被运行了
f2方法被运行了
  • ReentrantLock [实现了Lock接口]
不同点:
  • 用法
    • Synchronized用在方法上、用在同步代码块上 [隐式]
    • Lock必须使用lock方法加锁unlock方法解锁 [显式]
  • 加解锁顺序不同
    • Synchronized是java内部控制,自动加解锁
    • Lock可以手动调节
  • Synchronized锁不够灵活
    • Synchronized获得了一个锁 其他的只能等待
    • Lock获得锁很灵活 可以随时调整
  • 性能区别
    • Synchronized由差到好 目前同等程度的性能

你知道有几种锁?

  • 共享锁[读写锁] 和 独占锁[排他锁]

  • 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据

  • 共享锁和排他锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁

读写锁的作用
  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
读写锁的规则
  • 多个线程只申请读锁,都可以申请到
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
  • 一句话总结:要么是一个多个线程同时有读锁,要么一个线程有写锁,但是两者不会同时出现(要么多读,要麽一写)
公平锁 和 非公平锁
  • 公平指的是按照线程请求的顺序,来分配锁
  • 非公平指的是不完全按照请求的顺序,在一定情况下,可以插队
  • 注意:非公平也同样不提倡 ”插队“ 行为,这里的非公平,指的是”在合适的时机”插队,而不是盲目插队
  • 什么是合适的时机呢?
    • 买火车票被插队的例子,排队买的例子
  • 实际情况并不是这样的,java设计者这样设计的目的是为了提高效率
  • 避免唤醒带来的空档期,提升吞吐量
优势 劣势
公平锁 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 更慢,吞吐量更小
不公平锁 更快,吞吐量更大 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行
悲观锁 和 乐观锁
  • 是否锁住资源的角度分类
悲观锁
  • 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失
  • java中悲观锁的实现就是synchronizedLock相关类
乐观锁
  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过,如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现的
在数据库中
  • select for update就是悲观锁
  • version控制数据库就是乐观锁
经典例子
添加一个字段lock_version
先查询这个更新语句的vesion:SELECT * FROM table
然后
UPDATE SET num = 2, 
version = version + 1 WHERE version = 1 AND id = 5
如果version被更新了等于2,不一样就会更新出错,这就是乐观锁的原理

自旋锁 和 非自旋锁
  • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
  • 如果同步代码块的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复线程的花费可能会让系统得不偿失
  • 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
  • 而为了让当前线程“稍等一下”,我们需要让当前线程进行自旋,如果在自旋完成后前面锁定同步资源资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。
自旋锁的缺点
  • 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
  • 在自旋的过程中,一直消耗CPU,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的
可重入锁 和 非可重入锁
  • 什么是可重入 [摇一个号拿N个牌]

  • 好处 [避免死锁]

可中断锁 和 不可中断锁
  • 可中断锁 [可以随时中断]

死锁相关

写一个必然死锁的例子?
什么是死锁?
  • 发生在并发

  • 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁

  • 一图胜千言

  • 线程A持有锁1但试图获取锁2 线程B持有锁2但视图获取锁1

多个线程造成死锁的情况
  • 如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁
死锁的影响
  • 死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力
    • 数据库中:检测并放弃事务
    • JVM中:无法自动处理

几率不高但危害大
  • 不一定发生,但是遵守墨菲定律
  • 一旦发生,多是高并发场景,影响用户多
  • 整个系统崩溃、子系统崩溃、性能降低
  • 压力测试无法找出所有潜在的死锁
deadlock/DeadLock.java
package deadlock;

/**
 * 描述:     必然发生死锁
 */
public class DeadLock implements Runnable {

    public int flag;

    static Object o1 = new Object();
    static Object o2 = new Object();

    public void run() {
        System.out.println("开始执行");
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("成功获取到了两把锁");
                }
            }
        }
        if (flag == 2) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("成功获取到了两把锁");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock r1 = new DeadLock();
        DeadLock r2 = new DeadLock();
        r1.flag = 1;
        r2.flag = 2;
        new Thread(r1).start();
        new Thread(r2).start();
    }
}
====================================== 分析 =======================================
★ 当类的对象flag=1时(T1),先锁定O1,睡眠500毫秒,然后锁定O2;
★ 而T1在睡眠的时候另一个flag=2的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放O1;
★ T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定
★ T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定
★ T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁

哲学家就餐问题?

  • 先拿起左手的筷子
  • 然后拿起右手的筷子
  • 如果筷子被人使用了,那就等别人用完
while(true){ //伪代码
    think();
    pick_up_left_fork();
    pick_up_right_fork();
    eat();
    put_down_right_fork();
    put_down_left_fork();
}
package deadlock;

/**
 * 描述:     哲学家就餐问题导致的死锁
 */
public class DiningPhilosophers {

    public static class Philosopher implements Runnable {

        private Object leftChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        private Object rightChopstick;

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Picked up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {//五个哲学家方便管理
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i]; //从0开始 i为5 所以底下要加1 但是越界就取余
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            //领导调节(检测与恢复策略) [定期巡视命令哲学家] 让最后一个 跟别人不一样 
            //别人都是先左后右面 它是先后面再左边 避免了环路的形成
            //直接避免死锁发生!!!!!
            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}
=============================================================================
哲学家4号 Thinking
哲学家5号 Thinking
哲学家3号 Thinking
哲学家1号 Thinking
哲学家2号 Thinking
哲学家2号 Picked up left chopstick
哲学家3号 Picked up left chopstick
哲学家1号 Picked up left chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
哲学家4号 Put down right chopstick
哲学家4号 Put down left chopstick
哲学家4号 Thinking
哲学家3号 Picked up right chopstick - eating
哲学家3号 Put down right chopstick
哲学家3号 Put down left chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
哲学家4号 Put down right chopstick
哲学家4号 Put down left chopstick
哲学家3号 Thinking
哲学家2号 Picked up right chopstick - eating
哲学家4号 Thinking
哲学家2号 Put down right chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
.............................
发生死锁的时候哲学家都拿着左边的筷子
原理:Thread.sleep((long) (Math.random() * 10));
random到了一个更大的数
多种解决策略
  • 服务员检查(避免策略) [提前看一看是否发生死锁]
  • 改变一个哲学家拿叉子的顺序(避免策略)
  • 餐票(避免策略)
  • 领导调节(检测与恢复策略) [定期巡视命令哲学家]
实际工程中如何避免死锁
① 设置超时时间
  • Lock的tryLock(long timeout, TImeUnit unit)
  • synchronized不具备尝试锁的能力
  • 造成超时的可能性很多:发生了死锁、线程陷入死循环、线程执行很慢
  • 获取锁失败:打日志、发报警邮件、重启
  • 代码演示:退一步海阔天空
package deadlock;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     用tryLock来避免死锁
 */
public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程1获取到了锁1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程1获取到了锁2");
                            System.out.println("线程1成功获取到了两把锁");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println("线程1尝试获取锁2失败,已重试");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程2获取到了锁2");

                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程2获取到了锁1");
                            System.out.println("线程2成功获取到了两把锁");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println("线程2尝试获取锁1失败,已重试");
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
====================================================================================
线程1获取到了锁1
线程2获取到了锁2
线程1尝试获取锁2失败,已重试 【正是有了线程1的释放 才让线程2拿到了锁1】
线程2获取到了锁1
线程2成功获取到了两把锁
线程1获取到了锁1
线程1获取到了锁2
线程1成功获取到了两把锁
② 多使用并发类而不是自己设置锁
③ 尽量降低锁的使用粒度:用不同的锁而不是一个锁
④ 如果能使用同步代码块,就不使用同步方法:自己锁定锁对象
⑤ 给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践

Hashmap为什么[线程]不安全?

  • i++

    • 第一个步骤是读取
    • 第二个步骤是增加
    • 第三个步骤是保存

    有可能会发生线程不安全的情况

  • 同时put碰撞导致数据丢失

  • 可见性问题无法保证

final的作用是什么?有哪些用法?

  • final修饰变量
  • final修饰方法
  • final修饰
final的作用
  • 早期
    • 锁定
    • final效率:早期的Java实现版本中,会将final方法转为内嵌调用
  • 现在
    • 类防止被继承、方法防止被重写、变量防止被修改
    • 天生是线程安全的,而不需要额外的同步开销
final的3种用法
final修饰变量:赋值时机
  • 属性被声明为final后,该变量则只能被赋值一次。且一旦被赋值,final的变量就不能再被改变,无论如何也不会改变

  • final修饰变量

    • final instance variable (类中的final属性)

      • 第一种是在声明变量的等号右边直接赋值
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public static int a = 5;
      
          public static void main(String[] args) {
              FinalVariable.a = 8;
          }
      }
      
      • 第二种就是构造函数中赋值
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public final int a;
      
          public testFinal(int a) {
              this.a = b;
          }
          public static void main(String[] args) {
          }
      }
      
      • 第三种就是在类的初始代码块种赋值(不常用)
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public static final int a;
      
          static {
              a = 9;
          }
      
          void testFinal() {
              final int b;
          }
          public static void main(String[] args) {
          }
      }
      
      • 如果不采用第一种赋值方法,那么就必须在第2、3种挑一个来赋值,而不能不赋值,这是final语法所 规定的
    • final static variable (类中的static final属性)

      • 两种赋值时机:除了在声明变量的等号右边直接赋值外,static final变量还可以用static初始代码块赋值,但是不能用普通的初始代码块赋值
    • final local variable (方法中的final变量)

      • 和前两种不同,由于这里的变量是在方法里的,所以没有构造函数,也不存在初始代码块
      • final local variable不规则赋值时机,只要求在使用前必须赋值,这和方法中的非final变量的要求也是一样的
    为什么要规定赋值时机?
    • 如果初始化不赋值,后续赋值,就是从null变成你的赋值,这就违反final不变的规则了
  • final修饰方法

    • 构造方法不允许final修饰
    • 不可被重写,也就是不能被override
    package com.imooc.interniew;
    
    /**
     * 描述:     final修饰方法
     */
    public class FinalMethodDemo{
    
        public void drink() {
        }
    
        public final void eat() {
        }
    }
    
    class SubClass extends FinalMethodDemo {
    
        @Override
        public void drink() {
            super.drink();
        }
    
    //    @Override
    //    public void eat() {
    //        super.drink();
    //    }
    }
    
  • final修饰

    • 不可被继承
    • 例如经典的String类就是final的,我们从见过哪个类是继承String类的

单例模式你会写吗?

  • 什么是单例模式

保证只有一个实例且提供只有一个全局入口

  • 为什么需要单例:节省内存和计算、保证结果正确、方便管理
  • 适用场景
    • 无状态的工具类
    • 全局信息类
  • 单例模式的8种写法
    • 饿汉式(静态常量) [可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     饿汉式(静态常量)(可用)
     */
    public class Singleton1 {
    
        private Singleton1() {
    
        }
    
        private final static Singleton1 INSTANCE = new Singleton1();
    // [未达到懒加载] 直接创建出来了
        public static Singleton1 getInstance() {
            return INSTANCE;
        }
    }
    
    • 饿汉式(静态代码块) [可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     饿汉式(静态代码块)(可用) // [未达到懒加载]
     */
    public class Singleton2 {
    
        private Singleton2() {
    
        }
    
        static {
            INSTANCE = new Singleton2();
        }
        private final static Singleton2 INSTANCE;
    
        public static Singleton2 getInstance() {
            return INSTANCE;
        }
    }
    
    • 懒汉式(线程不安全) [不可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程不安全)
     */
    public class Singleton3 {
    
        private Singleton3() {
    
        }
    
        private static Singleton3 INSTANCE;
    
        public static Singleton3 getInstance() {
            if (INSTANCE == null) {//第一次访问方法
                INSTANCE = new Singleton3();//初始化
            }
            //此时如果两个线程同时访问,都是null,就创造了两个初始化 违反单例模式
            return INSTANCE; //已经被初始化 就返回
        }
    }
    
    • 懒汉式(线程安全,同步方法) [不推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton4 {
    
        private Singleton4() {
    
        }
    
        private static Singleton4 INSTANCE;
        //synchronized同步关键字 最多一个线程访问
        //不推荐用的原因是因为一旦适用了synchronized同步关键字 线程就要排队 并发量大
        public synchronized static Singleton4 getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new Singleton4();
            }
            return INSTANCE;
        }
    }
    
    //方法上不进行同步了
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton5 {
    
        private Singleton5() {
    
        }
    
        private static Singleton5 INSTANCE;
    
        public static Singleton5 getInstance() {
            if (INSTANCE == null) { //此时不会存在两个线程同时出来了
                synchronized (Singleton5.class) {
                    //假如第一个执行完了 第二个进去执行 那么结果还是生成了两个 不符合单例
                    INSTANCE = new Singleton5();
                }
            }
            return INSTANCE;
        }
    }
    
    • 双重检查[推荐用]
      • 新建一个对象,但还未初始化
      • 调用构造函数等来初始化该对象
      • 把对象指向引用
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton6 {
    
        private Singleton6() {//2
    
        }
    
        private static volatile Singleton6 INSTANCE;
    
        public static Singleton6 getInstance() {
            if (INSTANCE == null) {
                synchronized (Singleton6.class) {
                    if (INSTANCE == null) { //3
                        //就不会出现多个结果了
                        INSTANCE = new Singleton6 //1
                    }
                }
            }
            return INSTANCE;
        }
    }
    
    • 静态内部类[推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     静态内部类写法(推荐用)
     */
    public class Singleton7 {
    
        private Singleton7() {
    
        }
    
        private static class SingletonInstance {
            private static Singleton7 INSTANCE = new Singleton7();
        }
    
        public static Singleton7 getInstance() {
            return SingletonInstance.INSTANCE;
        }
    }
    
    • 枚举[推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     枚举单例模式
     */
    public enum Singleton8 {
        //1.写法简洁
        //2.线程安全
        //3.防止反射
        
        INSTANCE;
    }
    
不同写法对比
  • 饿汉:简单,但是没有lazy loading
  • 懒汉:有线程安全问题
  • 静态内部类:可用
  • 双重检查:面试用
  • 枚举:最好
单例模式面试常见问题
  • 饿汉式的缺点?[没有懒加载]
  • 懒汉式的缺点?[不可以保证线程安全]
  • 为什么要用double-check?不用就不安全吗?
  • 为什么双重检查模式要用volatile
  • 应该如何选择,用哪种单例的实现方案最好
    • 单元素的枚举类型已经成为实现Singleton的最佳方法
    • 写法简单
    • 线程安全有保障
    • 避免反射破坏单例

面试避坑指南

  • 何时投简历 [Offer数量只会越来越少,越早越好] 秋招7-9月 社招金3银4
  • 信息尽量全面
  • 技术栈契合
  • 慎用”精通“ [对源码有很多熟悉] => 多写熟悉
  • 面试无处不在 [如果没时间可以申请换一个时间节点 并询问对方是否有时间]
  • 提前调试设备
  • 仪容仪表、提前到场 [提前5分钟左右联系面试官]
  • 确认问题 [实在不会可以说思路设想]
  • 问面试官的问题 [提前查公司信息 我了解到我们公司… 可不可以介绍一下… 未来规划… 技术栈…]

哪些软素质值得面试官认可?

  • 基本能力:聆听、沟通表达、学习能力
  • 工作能力:协作执行力、管理能力
  • 个人素质:技术自驱力、韧性、积极开放的心态

面试课总结

  • Spring Boot常见面试题
  • 线程常见面试题
  • 分布式的面试题
  • Docker相关面试题
  • Nginx和Zookeeper相关面试题
  • RabbitMQ相关面试题
  • 微服务相关
  • 彩蛋:学习方法
  • 锁分类、死锁
  • HashMap和final
  • 单例模式[高频考点]
  • 面试避坑指南
  • 重要的软实力

2024.5.9 14:34   在创新楼B105-JSP动态网页设计课上  完成了java+4399全部课程的学习

阅读全文

2024万能论据

2024/4/17

健康

1.sth contribute to one's health by giving sb some physical exercise.
  某物能有助于身体健康通过给某人一些身体锻炼
2.sth is harmful to/bad for/detrimental to health.
  某物对健康有害
3.sth exert great pressure on sb.
  某物给某人施加重压
4.sb may become more solitary and even suffer from certain mental illness.
  某人可能变得孤僻甚至产生某些心理疾病
5.Exercise can enhance immune function.
  锻炼可以增强免疫力
6.adopt a healthy and meaningful life style.
  采取一个健康和有意义的生活方式
7.release pressure.
  ★减压 

乐趣

1.sth will make ones's life more enjoyable. That is to say, sth can add color to the dull routine of everyday life.
  某物可以使某人的生活更加有乐趣,也就是说,某物为每天枯燥的生活增添了一些色彩
2.sth have become the foucus of one's life and the source of one's happiness and contentment.
  某物已成为某人生活的中心和快乐的来源

能力

1.equip sb with the ability to do sth.
  ★使某人拥有做某事的能力
2.be competent enough to do sth.
  ★有能力做某事
3.tell the useful information from the useless one.
  ★从无用信息中挑出有用信息
4.sth will provide sb with more opportunities to develop one’s interpersonal skills, which may put them in a favorable position in the future job markets.
  某事会使某人有更多机会发展人际交往能力,而这对他(们)未来找工作是非常有好处的
5.By doing sth, sb can not only improve one’s academic studies, but gain many abilities which he will never be able to get from the textbooks.
  通过做某事,某人不仅能够提高他们的专业能力,而且能获得从课本上得不到的能力
6.tap one's potential.
  ★开发潜能
7.sth will help people foster one's independence.
  某物帮助某人培养独立
8.help strengthen one's sense of responsibility.
  ★帮助加强某人的责任感
9.achieve one's goal.
  ★达成某人的目标
10.learn from mistake.
  ★吸取教训
11.surmount difficulities
  ★战胜困难

交流

1.develop and cultivate character and interpersonal skills.
  ★培养性格和人际交往的能力
2.make eye contact.
  ★做眼神交流
3.live only in the virtual world.
  ★仅仅生活在虚拟世界
4.hinder communication.
  ★阻碍沟通
5.The over-dependence on mobile phones can harm the relationship among friends and family.
  过度依赖手机会伤害朋友以及家人的关系
6.At dinner tables, instead of chatting and laughing with each other, many people choose to chat with other friends online.
  在饭桌前,人们不是相互聊天谈笑,而是选择和朋友在网上聊天
7.Many people have become over-dependent on the Internet and neglected face-to-face communication.
  很多人变得太依赖网络并且忽略了面对面交流。
8.The Internet enables more effective communication in some situations, but over-dependence on it actually pulls people apart.
  网络在一些情况下使交流更有效,但是过度依赖它实际是人们更疏远

知识

1.Little by little, our knowledge will be well enriched, and our horizons will be greatly broadened.
  逐渐的,我们的知识会得到增加,我们的视野将会大大的拓宽
2.reduce study load 
  ★减轻学习负担
3.sth can broaden people's minds to the extent which may not be reached previously.
  某物可以扩大人们的视野到一个之前不能到达的程度

方便

1.sth can save sb a great deal of time, so sb would be able to concentrate more time and energy on one's academic work.
  某物能节省大量时间,这会使某人有更多的时间和精力放在学习上
2.sth have brought people great convenience in doing sth.
  某物在人们做某事时带来了巨大的方便
3.There is no denying that sth have greatly enhanced work/study efficiency.
  毋庸置疑,某物很大的提升了工作/学习效率

经济

1.relieve one's families' financial burden to some extent.
  这在一定基础上可以缓解某人家庭的经济负担
2.The welfare and salary have been improved greatly.
  福利和收入得到了巨大的提升
3.Money is so indispensable in people’s lives that without it no material comforts or well-being can be guaranteed.
  钱在生活中必不可少,没有了钱,也就谈不上物质享受和幸福了
4.Money doesn't necessarily ensure happiness or well-being. 
  钱不一定能保证幸福
5.can afford the huge cost of sth.
  ★能够承担某物的高额
6.bring huge economic loss to sb.
  ★给某人带来巨大经济损失

安全

1.sth pose a great threat to one's safety.
  某物对某人的安全造成巨大威胁
2.ensure the safety of sb. 
  ★确保某人的安全
3.sth add to the safety of sth.
  某物增加了某事的安全
4.food safety.
  ★食品安全

环境

1.To balance economic growth with environmental protection is highly important.
  权衡经济发展和环境保护是高度重要的
2.We all should bear in mind that environmental protection is everybody's duty.
  我们都应该记住环境保护使每个人的职责
3.sth help to build harmonious cooperation and effective communication, which helps to create a pleasant working atmosphere.
  某事帮助建立和谐的合作关系和有效的沟通,这将帮助建立一个愉快的工作氛围
阅读全文

SpringCloud基础+电商实战

2024/4/11

Spring Cloud基础介绍

  • 微服务的基本概念、设计与拆分原则
  • 微服务和Spring Cloud的关系
  • 微服务常见的组件和功能
  • 课程查询案例基本介绍、系统架构设计和接口设计
  • 分模块构建Spring Cloud项目
  • 完成课程列表、课程价格服务开发
  • 课程服务整合,服务注册与发现
  • 整合Feign实现服务间调用
  • 网关的集成与开发,并接入服务
  • 引入服务的熔断与降级,并进行实操演练

微服务基础

  • 什么是微服务?
  • 微服务的特点
  • 微服务优缺点
  • 微服务的两大门派 [Spring Cloud 和 Dubbo]
  • 微服务拆分
  • 微服务扩展
  • 微服务重要模块

微服务热度

单体应用的痛点

什么是服务化
  • 把传统的单机应用中的本地方法调用,改造成通过RPC、HTTP产生的远程方法调用
  • 把模块从单体应用中拆分出来,独立成一个服务部署
什么是微服务
  • 一系列、一部分

  • 是一种架构风格

  • 开发单体应用作为一系列小型服务的套件,其中每个服务都运行再自己的进程中,并且通过轻量级的机制实现彼此间的通信,这通常是HTTP资源API

  • 这些服务是围绕着业务功能构建的,并且可以通过完全自动化的部署机制进行独立部署

  • 这些服务的集中式管理做到了最小化(例如docker相关技术),每一种服务都可以通过不同的编程语言进行编写,并且可以使用不同的数据存储技术

微服务的特点

  • 组件以服务形式来提供

  • 产品不是项目

  • 轻量级通信、独立进程

  • 分散治理、去中心化治理

  • 容错性设计

  • 会带来团队组织架构的调整

微服务优缺点

  • 服务简单、便于学习和上手,相对易于维护
  • 独立部署,灵活扩展
  • 技术栈丰富
微服务缺点
  • 运维成本过高 [磁盘满 CPU高]
  • 接口可能不匹配
  • 代码可能重复 [要非常明确的定义每个API]
  • 架构复杂度提高

微服务两大门派

  • Spring Cloud:众多子项目【最大供应者:Netflix内容付费知识】
  • dubbo:高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • dubbo提供的能力只是SpringCloud的一部分子集
[Dubbo虽然有很多不提供的组件 但是可以和其他供应商合作完成提供任务]
核心组件 Dubbo Spring Cloud
服务注册中心 Zookeeper Spring Cloud Netflix Eureka
服务调用方式 RPC REST API
服务网关 Spring Cloud Netflix Zuul
断路器 不完善 Spring Cloud Netflix Hystrix
分布式配置 Spring Cloud Config
服务跟踪 Spring Cloud Sleuth
消息总线 Spring Cloud Bus
数据流 Spring Cloud Stream
批量任务 Spring Cloud Task
通信协议对比
  • RPC vs REST
  • 服务提供方与调用方接口依赖方式太强
  • 服务对平台敏感,难以简单复用
文档质量对比
  • Dubbo的文档可以说再国内开源框架中算是一流的,提供了中文与英文两种版本
  • Spring Cloud文档体量大,更多的是偏向整合,更深的使用方法还是需要查看整合组件的详细文档
两大门派选型建议
  • Dubbo => 组装电脑 【中文文档多】
  • Spring Cloud => 品牌机【稳定可靠】

微服务拆分

什么时候进行服务化拆分
  • 第一阶段的主要目标是快速开发和验证想法
  • 进一步增加更多的新特性来吸引更多的目标用户
  • 同时进行开发的人员超过10人,这个时候就该考虑到服务化拆分了
不适合拆分的情况
  • 小团队,技术基础较薄弱
  • 流量不高,压力小,业务变化也不大
  • 对延迟很敏感的低延迟高并发系统
服务化拆分的两种方式
  • 纵向拆分[上方区别图]
  • 横向拆分
  • 结合业务综合分析

服务扩展

维度

自动按需扩展
  • 根据CPU负载程度、特定时间(比如周末)、消息中间件的队列长度、业务具体规则、预测等来决定是否扩展
  • 自动分配一个新的服务实例,提高可用性
  • 提高了可伸缩性(双11之后,自动减少服务器)
  • 具有最佳使用率,节约成本

微服务重要模块

  • 服务描述
  • 注册中心
  • 服务框架
  • 负载均衡
  • 熔断和降级
  • 网关

Spring Cloud课程查询

  • Spring Cloud简介
  • 项目整体设计
  • 课程列表模块开发
  • 课程价格模块开发 [模块间互相调用]
  • 服务注册与发现Eureka
  • 服务间调用Feign
  • 负载均衡Ribbon
  • 熔断器Hystrix [兜底界面 默认返回]
  • 网关Zuul
  • 整体测试

Spring Cloud简介

  • 成熟的微服务框架,定位为开发人员提供工具,以快速构建分布式系统
核心组件
核心组件 Spring Cloud
服务注册中心 Spring Cloud Netflix Eureka
服务调用方式 REST API、Feign、Ribbon
服务网关 Spring Cloud Netflix Zuul
熔断器 Spring Cloud Netflix Hystrix

项目整体设计

  • 项目介绍

  • 接口设计
    • 课程列表
    • 单个课程价格
    • 整合课程列表和价格
  • 表设计

系统数据流向

【课程列表数据】→ 【课程列表服务】

【课程价格数据】→ 【课程价格服务】→ 【整体列表+价格】

新建多模块项目

创建一个 spring-cloud-course-pracice 并删除src文件夹
新建New → module → Maven → course-service → 删除src
    新建New → module → New Module → Maven → course-list → Parent:course-service 
    新建New → module → New Module → Maven → course-price → Parent:course-service 
D:\Java+4399\阶段5:Java分布与微服务实战\第32周 Spring Cloud基础\第2节 Spring Cloud开发课程查询功能\辅助材料\SpringCloud课程查询源码【优质it资源微信it-wangke18】.zip\SpringCloud课程查询源码\课程价格模块开发后\spring-cloud-course-practice
course-service.course-list
pom.xml
    <?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>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-list</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CourseListController.java
package com.imooc.course.controller;

import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * CourseListController课程列表Controller
 */

@RestController
public class CourseListController {
    //提供课程列表服务
    @Autowired //引入课程服务
    CourseListService courseListService;


    @GetMapping("/courses")
    public List<Course> courseList() {
        //return → service
        return courseListService.getCourseList();
    }
}
com/imooc/course/service/CourseListService.java
package com.imooc.course.service;

import com.imooc.course.entity.Course;

import java.util.List;

/**
 * 课程列表服务
 */
public interface CourseListService {
    List<Course> getCourseList();
}
com/imooc/course/service/impl/CourseListServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CourseMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 课程服务实现类
 */
@Service
public class CourseListServiceImpl implements CourseListService {
    @Autowired
    CourseMapper courseMapper;
    @Override
    public List<Course> getCourseList() {
        return courseMapper.findValidCourses();
    }
}
com/imooc/course/entity/Course.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * Course的实体类
 */
public class Course implements Serializable {
    Integer id;
    Integer courseId;
    String courseName;
    Integer valid;

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", courseName='" + courseName + '\'' +
                ", valid=" + valid +
                '}';
    }
    Getter+Setter  
}
com/imooc/course/dao/CourseMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 描述课程的Mapper类
 */
@Mapper
@Repository
public interface CourseMapper {
    @Select("SELECT * FROM course WHERE valid = 1")
    List<Course> findValidCourses();
}
application.properties

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity的实体类与数据库名进行驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=====================================================================
数据库名:course_practice
   表名:course
   字段:id[自增]   course_id   course_name   valid
课程列表模块开发-总结 [注意点]
  • 多模块开发
  • 实体类实现Serializable接口、set方法
  • MyBatis的驼峰配置
course-list.course-price
pom.xml
<?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>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-price</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 课程价格控制器
 */

@RestController
public class CoursePriceController {
    @Autowired
    CoursePriceService coursePriceService;
    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId){
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }
}
com/imooc/course/service/CoursePriceService.java
package com.imooc.course.service;

import com.imooc.course.entity.CoursePrice;

import java.util.List;

/**
 * 课程价格服务
 */
public interface CoursePriceService {
    CoursePrice getCoursePrice(Integer courseId);
}
com/imooc/course/service/impl/CoursepriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 课程价格的服务实现类
 */
@Service
public class CoursepriceServiceImpl implements CoursePriceService {
    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }
}
com/imooc/course/entity/CoursePrice.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * CoursePrice的实体类
 */
public class CoursePrice implements Serializable {
    Integer id;
    Integer courseId;
    Integer price;

    @Override
    public String toString() {
        return "CoursePrice{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", price=" + price +
                '}';
    }
    Getter+Setter
}
com/imooc/course/dao/CoursePriceMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 课程价格Mapper类
 */
@Mapper
@Repository
public interface CoursePriceMapper {
    @Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
    CoursePrice findCoursePrice(Integer courseId);
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
==========================================================
http://127.0.0.1:8082/price?courseId=409
数据库名:course_practice
   表名:course_price
   字段:id[自增]   course_id    price

Eureka的作用和架构

Eureka
  • 用于定位服务,直接找到组件中的各个服务地址
  • 114[各种服务的提供者]、物业[维护各个住户的信息(注册中心)]
为什么需要服务注册与发现 [移除不影响 但会有很多麻烦]
  • IP变化
  • 难以维护
  • 改进
    • 节点变化[服务的提供者和消费者] [消费者需要调用提供者的API来获得服务] 若提供者修改了ip 此时应该上传到注册中心,消费者此时需要得到IP和API 无需直接找提供者 直接去注册中心调用
Eureka架构
  • EureKa Server 和 EureKa Client

  • 集群 [只要能获得一个Eureka Server 就能获得整个信息]

引入Eureka

  • 引入依赖
  • 配置文件
  • 启动注解
pom.xml(eureka-server)
<?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>spring-cloud-course-practice</artifactId>
        <groupId>com.imooc</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>course-eureka-server</name>
    <description>Spring Cloud Eureka</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

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

</project>
pom.xml(Spring-cloud-course-practice)
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <packaging>pom</packaging>
  <modules>
    <module>course-service</module>
    <module>eureka-server</module>
  </modules>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.imooc</groupId>
  <artifactId>spring-cloud-course-practice</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>spring-cloud-course-practice</name>
  <description>course project for Spring Cloud</description>

  <properties>
    <java.version>1.8</java.version>
  </properties>

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

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

  <!--  表示Spring Cloud的版本-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
application.properties(eureka-server)

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#fetch-registry???????????????????
eureka.client.fetch-registry=false
#register-with-eureka??????????Eureka Server????true?
eureka.client.register-with-eureka=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
application.properties(course-list)

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity?????????????????
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
#??????? eureka-server??application.properties?defaultZone????????????????
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
application.properties(course-price)

server.port=8082
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
spring.application.name=course-price
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/EurekaServerApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * Eureka的服务端
 */
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
========================================================
http://127.0.0.1:8000/

利用Feign实现服务间调用

Feign
  • 声明式、模板化的HTTP客户端,方便的调用远程的HTTP请求 [基于接口实现]
集成Feign
  • 引入依赖
  • 配置文件
  • 注解
pom.xml (course-price)
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

application.properties
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import java.util.List;
import javax.xml.ws.Action;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:     课程价格控制器
 */
@RestController
public class CoursePriceController {

    @Autowired
    CoursePriceService coursePriceService;

    @Autowired
    CourseListClient courseListClient;

    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId) {
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }

    @GetMapping("/coursesInPrice")
    public List<Course> getCourseListInPrice(Integer courseId) {
        List<Course> courses = courseListClient.courseList();
        return courses;
    }
}
com/imooc/course/client/CourseListClient.java[接口]
package com.imooc.course.client;

import com.imooc.course.entity.Course;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 描述:     课程列表的Feign客户端
 */
@FeignClient("course-list")
public interface CourseListClient {

    @GetMapping("/courses")
    List<Course> courseList();
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=============================================================
http://127.0.0.1:8082/coursesInPrice

[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]

负载均衡的两种类型

  • 客户端负载均衡(Ribbon)
  • 服务端负载均衡(Nginx)
负载均衡策略
  • RandomRule 表示随机策略
  • RoundRobinRule 表示轮询策略
  • ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
配置不同的负载均衡方式
  • Ribbon.NFLoadBalancerRuleClassName
application.properties(course-price)
course-list.ribbon.NFLoadBanlancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

为什么要断路器

Hystrix
pom.xml(course-price)   
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
application.properties(course-price)
feign.hystrix.enabled=true
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
================================================================
所有服务都打开时 http://127.0.0.1:8082/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
当把CouresListApplication服务关闭的时候
[
    {
        "id": 1,
        "courseId": 1,
        "courseName": "默认课程",
        "valid": 1
    }
]

整合两个服务

com/imooc/course/controller/CoursePriceController.java
    @GetMapping("/coursesAndPrice")
    public List<CourseAndPrice> getCoursesAndPrice(){
        List<CourseAndPrice> courseAndPrices = coursePriceService.getCourseAndPrice();
        return courseAndPrices;
    }
=====================================================
http://127.0.0.1:8082/coursesAndPrice
[
    {
        "id": 1,
        "courseId": 362,
        "name": "SpringCloud自学",
        "price": 348
    },
    {
        "id": 2,
        "courseId": 409,
        "name": "玩转Java并发工具",
        "price": 399
    }
]
com/imooc/course/service/CoursePriceService.java
public interface CoursePriceService {

    CoursePrice getCoursePrice(Integer courseId);
    List<CourseAndPrice> getCourseAndPrice();
}
com/imooc/course/service/impl/CoursePriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CourseAndPrice;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

/**
 * 描述:     课程价格的服务实现类
 */
@Service
public class CoursePriceServiceImpl implements CoursePriceService {

    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Autowired
    CourseListClient courseListClient;

    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }

    @Override
    public List<CourseAndPrice> getCourseAndPrice() {
        List<CourseAndPrice> courseAndPrices = new ArrayList<>();
        List<Course> courses = courseListClient.courseList();
        for (int i = 0; i < courses.size(); i++) {
            Course course = courses.get(i);
            //inn
            if (course != null) {
                CoursePrice coursePrice = getCoursePrice(course.getCourseId());
                CourseAndPrice courseAndPrice = new CourseAndPrice();
                courseAndPrice.setPrice(coursePrice.getPrice());
                courseAndPrice.setName(course.getCourseName());
                courseAndPrice.setId(coursePrice.getId());
                courseAndPrice.setCourseId(course.getCourseId());
                courseAndPrices.add(courseAndPrice);
            }
        }
        return courseAndPrices;
    }
}
com/imooc/course/entity/CourseAndPrice.java
package com.imooc.course.entity;

/**
 * 课程与价格的融合类
 */
public class CourseAndPrice {
    Integer id;
    Integer courseId;
    String name;
    Integer price;
}

网关Zuul

  • 为什么需要网关
  • 签名校验、登录校验冗余问题
  • Spring Cloud Zuul 与 Spring Cloud
  • API网关允许您将API请求(内部或外部)路由到正确的位置

集成Zuul [统一修改访问url地址]

  • 把自己注册到Eureka这个注册中心
  • 引入依赖
  • 配置路由地址
pom.xml(course-zuul)
     <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>
com/imooc/course/ZuulGatewayApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;

/**
 * 网关启动类
 */
@EnableZuulProxy
@SpringCloudApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
===============================================================
http://127.0.0.1:9000/imooc/price/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
application.properties

spring.application.name=course-gateway
server.port=9000
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

zuul.prefix=/imooc
zuul.routes.course-list.path=/list/**
zuul.routes.course-list.service-id=course-list
zuul.routes.course-price.path=/price/**
zuul.routes.course-price.service-id=course-price

利用网关实现过滤器

  • pre 过滤器在路由请求之前运行
  • route 过滤器可以处理请求的实际路由
  • post 路由请求后运行过滤器
  • error 如果在处理请求的过程中发生错误,则过滤器将运行
com/imooc/course/filter/PreRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 记录请求时间
 */
@Component
public class PreRequestFilter extends ZuulFilter {

    @Override
    public String filterType() {
        //过滤器的类型
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        //是否启用过滤器
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //通过时间戳 获取上下文
        RequestContext currentContext = RequestContext.getCurrentContext();
        currentContext.set("startTime",System.currentTimeMillis());
        System.out.println("过滤器已经记录时间");
        return null;
    }
}
com/imooc/course/filter/PostRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 请求处理后的过滤器
 */
@Component //有了才能被spring捕捉到
public class PostRequestFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() { //在其之前-1 先运行
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        Long startTime = (Long) currentContext.get("startTime");
        long duration = System.currentTimeMillis() - startTime;
        String requestURI = currentContext.getRequest().getRequestURI();
        System.out.println("uri:" + requestURI + ",处理时长:" + duration);
        return null;
    }
}


Spring Cloud电商实践

  • 服务拆分过程分析和经验分享
  • 完成用户、商品、购物车和订单等服务开发
  • 通用common模块的拆分和应用
  • Eureka server注册中心开发,用Feign完成服务之间的调用
  • 共享Session的处理方案
  • 统一网关的集成与开发

Spring Cloud电商项目

  • 项目介绍
    • 在Spring Boot的基础上升级为Spring Cloud
    • 从0到1 (Spring Boot)
    • 从1到多、微服务
  • 模块拆分
  • Eureka-server开发
  • 用户模块开发
  • 公共模块开发
  • 网关模块开发
  • 商品分类和商品模块开发
  • 购物车和订单模块开发
  • 总结

模块拆分

  • 粒度:过粗、适中、过细

  • 人员的角度

  • 业务的角度:相关、独立

  • Eureka-server模块

  • 网关模块

  • 公共模块 (md5、统一API返回、常量、异常、异常枚举、二维码、工具类模块……)

  • 用户模块 (相对来说比较独立,登录的时候无需获得商品信息)

  • 商品分类和商品模块

  • 购物车和订单模块

功能模块介绍

  • 项目功能:

    • 前台  {用户、商品分类、商品信息、购物车、订单}
      • 用户模块{注册、登录、更新签名、身份认证、登出}
      • 商品分类模块{多级目录、递归查询、缓存}
      • 商品模块{商品搜索、商品排序、商品列表、目录展示、商品详情}
      • 购物车模块{加入商品、列表显示、数量更改、删除商品、勾选反选、全选全不选}
      • 订单模块{下单、订单流程、订单详情、取消订单、支付二维码、扫码支付、个人订单、确认收货}
    • 后台  {用户、商品分类、商品信息、订单}
      • 管理员模块{登录登出、身份认证、安全限制}
      • 商品分类模块{分类列表、增加分类、修改分类、删除分类}
      • 商品模块{商品列表、新增商品、图片上传、更新删除、批量上下架}
      • 订单模块{订单列表、地址信息、发货、订单完结}

``

项目初始化

Eureka-server模块开发
  • 引入依赖
  • 配置文件
  • 启动注解

由于是多模块开发,创建了maven项目后把cloud-mall-practice的src删除

pom.xml(cloud-mall-practice)
<?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.imooc</groupId>
    <artifactId>cloud-mall-practice</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>cloud-mall-eureka-server</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
cloud-mall-eureka-server/com/imooc/cloud/mall/practice/eureka/EurekaServerApplication.java
package com.imooc.cloud.mall.practice.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * 1. Eureka Server的启动类,提供服务注册与发现
 * 2写其pom和resources的配置文件
 *
 */
@EnableEurekaServer //对外提供服务
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
pom.xml(cloud-mall-eureka-server)
<?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>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <name>
        cloud-mall-eureka-server
    </name>
    <description>Spring cloud Eureka Server</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

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

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#是否同步其他节点的信息
eureka.client.fetch-registry=false
#是否把自己作为服务注册在服务上
eureka.client.register-with-eureka=false
#eureka-server所在的地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

用户模块知识点

登录、注册[加盐md5]、重名校验[注册就不可注册]、密码加密存储、Session的使用、越权校验[不可编辑别人的签名]

用户模块
  • 表设计
  • 开发
  • 测试

idea中如何将包名折叠或者或如何将折叠的包名展开_idea包名折叠-CSDN博客

用户模块初始化

公共模块
  • 常量、异常、工具类
  • 自身不是Spring Boot项目
进行模块各层级的重构 + 用户模块的测试
一定要记得如果引用其他muder的时候 要在xml中假如其项目文件的依赖才可以跨项目引用
比如我这个项目是【一定要引用噢!! 不然在非其项目的时候找不到import导包】
pom.xml(cloud-mall-zuul)
           <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

网关模块开发

pom.xml(cloud-mall-zuul)
<?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>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-zuul</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties(cloud-mall-zuul)
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
com/imooc/cloud/mall/practice/zuul/filter/UserFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 用户鉴权过滤器
 */
@Component
public class UserFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("images") || requestURI.contains("pay")) {
            return false;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/filter/AdminFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import com.imooc.cloud.mall.practice.zuul.feign.UserFeignClient;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 管理员鉴权过滤器
 */
@Component
public class AdminFilter extends ZuulFilter {
    @Autowired
    UserFeignClient userFeignClient;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("adminLogin")){
            return false;
        }
        if (requestURI.contains("admin")){
            return true;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10010,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
            return null; //程序可以停止了
        }
        //进一步判断是否是管理员
        Boolean adminRole = userFeignClient.checkAdminRole(currentUser);
        if (!adminRole){
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10011,\n"
                    + "    \"msg\": \"NEED_ADMIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.zuul.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * UserFeignClient
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    @PostMapping("/checkAdminRole")
    public Boolean checkAdminRole(@RequestBody User user);
}
com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
网关模块加上SpringBoot启动类

package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
=================================================================
127.0.0.1:8083/user/login?userName=mumu9&password=lihao123
{
    "status": 10000,
    "msg": "SUCCESS",
    "data": {
        "id": 21,
        "username": "mumu9",
        "password": null,
        "personalizedSignature": "乘风破浪的姐姐",
        "role": 2,
        "createTime": "2024-04-17T18:08:15.000+0000",
        "updateTime": "2024-04-17T18:10:49.000+0000"
    }
}

POSTMAN中cloud-mall-practice的注册新用户、用户登录、管理员登录、登出模块都通过网关共用地址的调用,但是更新个性签名不可以,提示需要登录,因为更新签名需要提前登录。但是问题的最主要的点就是没有拿到session所以并没有把用户信息传到个性签名中 这是就需要Session共享机制

Session共享机制

登录功能分析
  • 登录状态需要保持
  • session的实现方案:登陆后,会保存用户信息到session
  • 之后的访问,先从session中获取用户信息,然后再执行业务逻辑
目前遇到的障碍 [记得去电脑端启动Redis]
  • session被网关过滤
  • 共享session [现在是多模块项目 其他项目保存了session 另一个项目无法获得]
  • EnableRedisHttpSession[需要通过中介Redis去调取]
在cloud-mall-user/application.properties中
server.port=8081

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-user

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
在cloud-mall-zuul/application.properties中
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=

#没有敏感的需要过滤
zuul.sensitive-headers=
zuul.host.connect-timeout-millis=15000
zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
之后在UserApplication和ZuulGatewayApplication前面加上@EnableRedisHttpSession

com/imooc/cloud/mall/practice/user/UserApplication.java
package com.imooc.cloud.mall.practice.user;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * 启动类
 */
@SpringBootApplication
@EnableSwagger2
@MapperScan(basePackages = "com.imooc.cloud.mall.practice.user.model.dao")
@EnableRedisHttpSession
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

-------------------------------------------------------------------------------------

com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
@EnableRedisHttpSession
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}

商品分类与商品模块初始化

什么是商品分类
  • 条理清楚,层次分明
  • 方便用户进行筛选和辨别
  • 可以通过分类的设置快速的进入对应的商品列表页面进行商品选择
分类层级
  • 在商品分类上需要继续做归类操作
  • 分类设置成三级
  • 层级太深的弊端:
    • 一是对用户不太友好,不利于寻找
    • 二是对于后台管理人员不友好,不方便管理

[cloud-mall-category-product]创造出来,将controller,model(dao,pojo),service,impi 里转入 Category来搞 因为在网关项目里写了关于管理员校验的方法 所以在此次处CategoryController把session里的校验删除

要让商品和商品目录用到User类 一定要去pom文件里添加依赖 不然idea找不到
pom.xml(cloud-mall-category-product)
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
com/imooc/cloud/mall/practice/categoryproduct/config/CachingConfig.java
创建处理缓存的配置类 configpackage com.imooc.cloud.mall.practice.categoryproduct.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.time.Duration;

*/**
* ** 57.缓存的配置类 想要运行成功保存序列化 要去弄个序列化接口
*   CategoryVO implements Serializable
* **/
*@Configuration
@EnableCaching
public class  CachingConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheWriter redisCacheWriter = RedisCacheWriter
                .lockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));

        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
                cacheConfiguration);
        return redisCacheManager;
    }
}
在网关中增加path和service-id
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user

zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
查端口  => netstat -ano | findstr :8083 [pid为7812]
杀死端口PID => taskkill /PID 7812 /f

商品模块

更新和新增商品
  • 合并写法不可取
  • 业务逻辑清晰、独立
批量上下架
  • MyBatis遍历List
  • where语句拼接
商品列表:搜索功能
入参判空 → 加%通配符 → like关键字
对于查询目录的in处理
  • 目录处理:如果查某个目录下的商品,不仅是需要查出来该目录的,还需要查出来子目录的所有商品
  • 这里要拿到某一个目录Id下的所有子目录id的List

前台:商品列表

  • 排序功能
  • Mybatis PageHelper
  • 枚举:order by
把所有product的java都移动过去 controller/service/impl/modal.dao/modal.pojo/query.ProductListQuery/request.AddProductReq、ProductListReq、UpdateProductReq/resourcs.mappers.ProductMapper.xml
将Constant里的@Value("${file.upload.dir}")的文件上传目录重构到category-product项目中
然后在其项目中的resources的application.properties中写一下路径
文件地址映射:config/imoocMallWebMvcConfig

图片端口的特殊处理

application.properties(cloud-mall-category-product)
file.upload.dir=/Users/Pluminary/Desktop/idea_Space/imooc-mall-prepare-static/

不能再通过获取getHost来配置了 根据实际情况去配置 没有办法获取到网关对外暴露的真正端口号
 private URI getHost(URI uri){
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),null,null,null);
        } catch (URISyntaxException e) {
            effectiveURI = null; //如果新建失败 就返回回去
        }
        return effectiveURI;
    }

要改为com/imooc/cloud/mall/practice/categoryproduct/controller/ProductAdminController.java
/**
 * 58.后台商品管理Controller  pojo的product复制一份到request变成AddProductReq  59.需要ProductService.java
 */
@RestController
public class ProductAdminController {

    @Autowired
    ProductService productService;

    @Value("${file.upload.ip}")
    String ip;

    @Value("${file.upload.port}")
    Integer port;

    @PostMapping("/admin/product/add")
    public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq) {
        productService.add(addProductReq);
        return ApiRestResponse.success();
    }

    @PostMapping("/admin/upload/file")
    public ApiRestResponse upload(HttpServletRequest httpServletRequest,
                                  @RequestParam("file") MultipartFile file) {
        String fileName = file.getOriginalFilename();
        String suffixName = fileName.substring(fileName.lastIndexOf("."));
        //生成文件名称UUID
        UUID uuid = UUID.randomUUID();
        String newFileName = uuid.toString() + suffixName;
        //创建文件
        File fileDirectory = new File(ProductConstant.FILE_UPLOAD_DIR);
        File destFile = new File(ProductConstant.FILE_UPLOAD_DIR + newFileName);
        if (!fileDirectory.exists()) {
            if (!fileDirectory.mkdir()) {
                throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
            }
        }
        try {
            file.transferTo(destFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            return ApiRestResponse
                    .success(getHost(new URI(httpServletRequest.getRequestURL() + "")) + "/category-product/images/"
                            + newFileName);
        } catch (URISyntaxException e) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.UPLOAD_FAILED);
        }
    }

    private URI getHost(URI uri) {
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), ip, port,
                    null, null, null);
        } catch (URISyntaxException e) {
            effectiveURI = null;
        }
        return effectiveURI;
    }

阶段性重难点和常见错误

阶段总结
  • 重难点:模块拆分设计、公共模块、Zuul(网关)过滤器、Session处理、Feign调用
    Session在微服务的情况下就不容易处理到了可以共享Session,把其放在Redis中实现共享
    Feign模块之间的接口调用[HTTP是手动调用]
  • 常见错误:模块粒度不合适、无公共模块、各接口独立校验、session无法共享、HTTP手动调用
模块拆分
  • 粒度:过粗、适中、过细
  • 人员的角度
  • 业务的角度:相关、独立

购物车与订单模块

购物车模块
  • 添加商品到购物车 → 商品是否在售、是否有库存

    • →[否] 提示用户

    • →[是] 该商品之前就在购物车里

      • →[否] 添加新商品
      • →[是] 原有基础上添加数量
创建一个module 加入关于cart的controller/model.dao.pojo.vo/service.impl
其中ProductMapper productMapper会爆红是理所应当的 这样证明耦合不是很严重
利用远程调用Feign进行调用
思路:通过调查发现productMapper只用于挑选ID
Product product = productMapper.selectByPrimaryKey(productId);
所以可以去重构一下代码 回到商品模块的地方cloud-mall-category-product
其中controller中的ProductController
//com/imooc/cloud/mall/practice/categoryproduct/controller/ProductController.java
//这个是服务与服务之间的内部调用 不需要层层包装 只需要返回就好
    @GetMapping("product/detailForFeign")
    public Product detailForFeign(@RequestParam Integer id){
       Product product = productService.detail(id);
       return product;
    }
//千万不要直接引用另一个项目的mapper 因为耦合有点严重 只要对面发生变化 就完蛋了
//com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.categoryproduct.model.pojo.Product;
import org.springframework.web.bind.annotation.RequestParam;

public interface ProductFeignClient {
    Product detailForFeign(@RequestParam Integer id);
}

用户模块提供获取当前用户接口

com/imooc/cloud/mall/practice/user/controller/UserController.java
 @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    } /**
     * 获取当前登录的User对象 为了避免暴露用户信息,可以再filter中对getUser进行拦截
     * @param session
     * @return
     */
    @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    }
com/imooc/cloud/mall/practice/cartorder/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * UserFeign客户端
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    /**
     * 获取当前登录的user对象
     */
    @GetMapping("/getUser")
    User getUser();
}
修改CaetMapper.xml的路径名
com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.cartorder.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * 描述:     商品FeignClient
 */
@FeignClient(value = "cloud-mall-category-product")
public interface ProductFeignClient {
    //不经过网关 只是内部调用
    @GetMapping("product/detailForFeign")
    Product detailForFeign(@RequestParam Integer id);

    @PostMapping("product/updateStock")
    void updateStock(@RequestParam Integer productId, @RequestParam Integer stock);

}
将新增模块加入到网关[前缀地址] zuul
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
zuul.routes.cloud-mall-cart-order.path=/cart-order/**
zuul.routes.cloud-mall-cart-order.service-id=cloud-mall-cart-order

让Feign携带Session信息

错误新消息:500 Internal Server Error
在购物车中去调用User的getUser方法 是经过Feign
但是Feign的调用不经过网关 它是一个HTTP的调用
需要携带网关的Session信息

  • FeignRequestInterceptor [对每一个发出的Feign进行拦截]
    把网关的所有信息都复制到Feign的请求上 就不会遗漏相关信息
com/imooc/cloud/mall/practice/cartorder/filter/FeignRequestInterceptor.java
package com.imooc.cloud.mall.practice.cartorder.filter;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 描述:     Feign请求拦截器
 */
@EnableFeignClients
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //通过RequestContextHolder获取到请求 拿到requestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return;
        }//类型转换 拿到Request
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        Enumeration<String> headerNames = request.getHeaderNames(); //拿到所有Header名字
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) { //如果还有新的元素 先获取
                String name = headerNames.nextElement(); //获取相关的值
                Enumeration<String> values = request.getHeaders(name);
                while (values.hasMoreElements()) {//获取到当前元素
                    String value = values.nextElement();
                    requestTemplate.header(name, value);
                }
            }
        }
    }
}

订单模块

  • 表设计和接口设计
"订单编号的名字也要改 不能是以前的自增id 要变成order_no"
order_no是每一种商品的id
否则黑客早上下一单 晚上下一单 相减 就可以估计客流量 
分成了很详细的字段名
product_id/name/img 都是之前购买过的订单产生的数据

生成订单–用户下单

  • 入参
  • 从购物车中查询已经勾选的商品
  • 判断商品是否正在售卖中
  • 判断库存,保证不超卖
  • 调用商品服务扣库存 [及时更新库存]
  • 删除购物车为中对应的商品
  • 生成订单
  • 订单号生成规则
  • 循环保存每一个商品到order_item表

图片路径配置

com/imooc/cloud/mall/practice/cartorder/config/ImoocMallWebMvcConfig.java
package com.imooc.cloud.mall.practice.cartorder.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 描述:     地址映射配置
 */
@Configuration
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {

    @Value("${file.upload.dir}")
    String FILE_UPLOAD_DIR;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + FILE_UPLOAD_DIR);
    }

购物车和订单模块-重难点

  • 订单表[多个关联(订单编号+状态)]、订单状态设计
  • 购物车流程
  • 下单流程
  • Feign调用的处理
购物车和订单模块-常见错误
  • Feign调用取不到User对象
  • URL错误拦截(图片url可以不拦截[订单+购物车])
  • 路由配置错误(二维码/图片不显示)
阅读全文

多线程与分布式

2024/4/4

多线程与分布式 [周介绍]

  • 线程池的基本使用、特点、注意点
  • ThreadLoacal的基本使用、原理和注意事项
  • 分布式基础、核心概念
  • docker的下载、安装和基本命令
  • 独立制作docker容器
  • Nginx的安装、基本使用和使用命令
  • 使用Nginx搭建文件服务
  • 消息队列RabbitMQ的核心概念queue、message和exchange
  • RabbitMQ的四种交换机模式
  • SpringBoot整合RabbitMQ案例

线程池 —— 治理线程的法宝

  • 线程池的自我介绍
  • 创建和停止线程池
  • 常见线程池的特点和用法
  • 任务太多,怎么拒绝
  • 钩子方法,给线程池加点料
  • 实现原理、源码分析
  • 使用线程池的注意点
线程池的自我介绍
  • 线程池的重要性 [可以复用我们的线程]
  • 什么是”池” – 软件中的”池”,可以理解为计划经济
  • 如果不适用线程池,每个任务都新开一个线程处理
    • 一个线程
    • for循环创建线程
    • 当任务数量上升到1000
      这样开销太大,我们希望有固定数量的线程,来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题[多了会报错 内存不足异常]
threadpool/ForLoop.java
package threadpool;
/**
 * 描述:     TODO
 */
public class ForLoop {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Task());
            thread.start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("执行了任务");
        }
    }
}
为什么要使用线程池
  • 问题一:反复创建线程开销大
  • 问题二:过多的线程会占用太多内存
  • 解决以上两个问题的思路
    • 用少量的线程——避免内存占用过多
    • 让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗
线程池的好处
  • 加快响应速度
  • 合理利用CPU和内存
  • 统一管理
线程池使用应用的场合
  • 服务器(Tomcat)接收到大量请求时,使用线程池技术是非常合适的,它可以大大减少现成的创建和销毁次数,提高服务器的工作效率
  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

线程增减的时机

创建和停止线程池
  • 线程池构造方法的参数
  • 线程池应该手动创建还是自动创建
  • 线程池里的线程数量设定为多少比较合适
  • 停止线程池的方法
线程池构造方法的参数
参数名 类型 含义
corePoolSize int 核心线程数
maxPoolSize int 最大线程数
keepAliveTime long 保持存活时间
workQueue BlockingQueue 任务存储队列
threadFactory ThreadFactory 当线程池需要新的线程的时候,会使用threadFactory来生成新的线程
Handler RejectedExecutionHandler 由于线程池无法接收你所提交的任务的拒绝策略
参数中的corePoolSize和maxPoolSize
  • corePoolSize指的是核心线程数
    线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新的线程去执行任务

  • 最大量maxPoolSize
    在核心线程数的基础上,格外增加的线程数的上限

添加线程规则
  1. 如果线程数小于corePoolSize,创建一个新线程来运行新任务
  2. 如果线程数等于(或大于) corePoolSize但少于maximumPoolSize,则将任务放入队列
  3. 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程
  4. 如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝

  • 是否需要增加线程的判断顺序是:
    • corePoolSize
    • workQueue
    • maxPoolSize
举个例子
  • 线程池:核心池大小为5,最大池大小为10,队列为100
  • 因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建新的线程maxPoolSize,最多到10个线程,如果再来任务,就拒绝
增减线程的特点
  • 通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池
  • 线程池希望保持较小的线程数,并且只有在负载变得很大时才增加它
  • 通过设置maximumPoolSize为很高的值,可以允许线程池容纳任意数量的并发任务
  • 只有在队列填满时才创建多于corePoolSize的线程,如果使用的是无界队列,那么线程数就不会超过corePoolSize

线程存活时间和工作队列

keepAliveTime
  • 如果线程池当前的线程多余corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,它们就会被终止
ThreadFactory 用来创建线程
  • 默认使用Executors.defaultThreadFactory()
  • 创建出来的线程都在同一个线程组
  • 如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等
工作队列
  • 有三种最常见的队列类型
    • 直接交接:SynchronousQueue
    • 无界队列:LinkedBlockingQueue
    • 有界队列:ArrayBlockingQueue

自动创建线程池的风险

线程池应该手动创建还是自动创建
  • 手动创建更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险
    自动创建线程池(即直接调用JDK封装好的构造方法) 可能会带来哪些问题?
  • newFixedThreadPool
    • 容易造成大量内存占用,可能会导致OOM
threadpool/FixedThreadPoolThread.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolThread {
    public static void main(String[] args) { //核心线程数量
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}
class Task implements Runnable{

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName());
    }
}
=======================================================================
pool-1-thread-3
pool-1-thread-4
pool-1-thread-1
pool-1-thread-2
    
pool-1-thread-3
pool-1-thread-1
pool-1-thread-4
pool-1-thread-2
因为核心线程数量只规定了4个 这有这四个线程跑程序
threadpool/FixedThreadPoolOOM.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示nexFixedThreadPool出错的情况
 */
public class FixedThreadPoolOOM {
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++){
            executorService.execute(new SubThread());
        }
    }
}
class SubThread implements Runnable{

    @Override
    public void run() {
        try{//一直睡觉 不让它结束
            Thread.sleep(1000000000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
=======================================================================
报错:java.lang.OutOfMemoryError:GC overhead limilt exceeded
  • newSingleThreadExecutor [单独的线程]
    • 当请求堆积的时候,可能会占用大量的内存
threadpool/SingleThreadExecutor.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}
=======================================================================
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
  • CachedThreadPool [可缓存线程池]
    • 特点:具有自动回收多余线程的功能
    • 弊端在于第二个参数maximumPoolSize被设置为了Integer.MAX_VALUE, 这可能会创建数量非常多的线程,甚至导致OOM
threadpool/CachedThreadPool.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}
  • newScheduledThreadPool [跟时间相关的]
threadpool/ScheduledThreadPool.java
package threadpool;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
        threadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
        threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);//每隔3s运行
    }
}
  • 正确的创建线程池的方法
    • 根据不同的业务场景,设置线程池参数
    • 比如:内存有多大,给线程取什么名字等等

线程池里的线程数量设定为多少比较合适?

  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右
  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于CPU核心数很多倍
    参考Brain Goetz推荐的计算方法:
    线程数 = CPU核心数 × (1 + 平均等待时间 / 平均工作时间)

对比各种线程池的特点

常见的线程池的特点

以上4种线程池的构造方法的参数
Parameter FixedThreadPool CachedThreadPool ScheduledThreadPool SingleThreaded
corePoolSize constructor-arg 0 constructor-arg 1
maxPoolSize same as corePoolSize Integer.MAX_VALUE Integer.MAX_VALUE 1
keepAliveTime 0 seconds 60 seconds 0 seconds 0 seconds

阻塞队列分析

  • FixedThreadPool和SingleThreadExecutor的Queue是LinedBlockingQueue
  • CachedThreadPool使用的是Queue是SynchronousQueue
  • ScheduledThreadPool使用延迟队列DelayedWorkQueue
workStealingPool是JDK1.8加入的
  • 这个线程池和之前的都有很大不同
  • 子任务
  • 窃取 [并行执行]

如何正确关闭线程池

停止线程池的正确方法
  • shutdown [再给就拒绝 新的任务不会增加了]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        executorService.shutdown();//绅士的暂停 再去提交的任务就不会增加了
        executorService.execute(new ShutDownTask());
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

=================================================================================
pool-1-thread-4
pool-1-thread-9
pool-1-thread-10
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task threadpool.ShutDownTask@61bbe9ba rejected from java.util.concurrent.ThreadPoolExecutor@610455d6[Shutting down, pool size = 10, active threads = 10, queued tasks = 970, completed tasks = 20]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
    at threadpool.Shutdown.main(Shutdown.java:18)
pool-1-thread-8
pool-1-thread-1
pool-1-thread-3
  • isShutdown [判断是否进入停止状态]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        System.out.println(executorService.isShutdown());//false
        executorService.shutdown();//绅士的暂停 再去提交的任务就不会增加了
        System.out.println(executorService.isShutdown());//true 已经结束了
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
  • isTerminated [线程停止返回 整个程序执行完毕]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        System.out.println(executorService.isShutdown());//false
        executorService.shutdown();//绅士的暂停 再去提交的任务就不会增加了
        System.out.println(executorService.isShutdown());//true 已经结束了
        System.out.println(executorService.isTerminated());
        Thread.sleep(10000);
        System.out.println(executorService.isTerminated());
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
==========================================================================
pool-1-thread-5
pool-1-thread-10
false
true
false
pool-1-thread-2
pool-1-thread-6
pool-1-thread-6
pool-1-thread-2
true
  • awaitTermination [测试一段时间内线程会不会完全停止的方法 等待的时间进程被打乱了 等待的时间到了]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500); //七秒钟之内是否完全运行完毕了
        executorService.shutdown();
        boolean b = executorService.awaitTermination(7L, TimeUnit.SECONDS);
        System.out.println(b);
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
  • shutdownNow [立刻关闭线程池]
///正在执行的和不要关闭 正在等待的内容直接返回 如何优雅编写?
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500); //七秒钟之内是否完全运行完毕了
        executorService.shutdownNow();//暴力关闭线程
        //这是正在队列中的数据 取了个List集合 都到runnableList里面了 要给它一个交代
        List<Runnable> runnableList = executorService.shutdownNow();
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断了");
        }
    }
}

=================================================================
pool-1-thread-6
pool-1-thread-4
pool-1-thread-10
pool-1-thread-8
pool-1-thread-3被中断了
pool-1-thread-1被中断了
pool-1-thread-8被中断了
pool-1-thread-2被中断了

暂停和恢复线程池

  • 拒绝时机
    • 当Executor关闭时,提交新任务被拒绝
    • 以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和
4种拒绝策略
  • AbortPolicy [直接抛出异常]
  • DiscardPolicy [默默的丢弃]
  • DiscardOldestPolicy [丢弃最老的]
  • CallerRunsPolicy [誰提交任务誰去跑(避免了业务损失 提交任务速度下降 给了线程池缓冲时间)]
钩子方法,给线程池加点料
  • 每个任务执行前后
  • 日志、统计
  • 代码演示
threadpool/PauseableThreadPool.java
package threadpool;

import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 演示每个任务执行前后放钩子函数
 */
public class PauseableThreadPool extends ThreadPoolExecutor {
    private final ReentrantLock lock = new ReentrantLock();
    private Condition unpaused = lock.newCondition();
    private boolean isPaused;
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override //在执行任务之前都会调用这个函数
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        lock.lock();
        //如果检测到就暂停休息
        try {
            while (isPaused) {
                unpaused.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void pause(){
        lock.lock();
        try{
            isPaused = true;
        }finally {
            lock.unlock();
        }
    }
    public void resume(){
        lock.lock();
        try{
            isPaused = false;
            unpaused.signalAll(); //唤醒全部
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 10l,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("我被执行");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10000; i++) {
            pauseableThreadPool.execute(runnable);
        }
            Thread.sleep(1500);
            pauseableThreadPool.pause();
            System.out.println("线程池被暂停了");
            Thread.sleep(1500);
            pauseableThreadPool.resume();
            System.out.println("线程池被恢复了");
    }
}

线程池实现复用的原因

  • 线程池组成部分
    • 线程池管理器
    • 工作线程
    • 任务队列
    • 任务接口(Task)
Executor家族
  • 线程池、ThreadPoolExecutor、ExecutorService、Executor、Executors等这么多和线程池相关的类,都是什么关系?

    Executor ← ExecutorService ← AbstractExecutorService ← ThreadPoolExecutor

线程池实现任务复用的原理
  • 相同线程执行不同任务

线程池状态和使用注意点

线程池状态
状态 含义
RUNNING 接收新任务并处理排队任务
SHUTDOWN 不接受新任务,但处理排队任务
STOP 不接受新任务,也不处理排队任务,并中断正在进行的任务
TIDYING 所有任务都已终止,workerCount为零时,线程会转换到TIDYING状态,并将运行terminate()钩子方法
TERMINATED terminate()运行完成
使用线程池的注意点
  • 避免任务堆积
  • 避免线程数过度增加
  • 排查线程泄露

ThreadLocal

ThreadLocal的两个作用
  • 让某个需要用到的对象在线程间隔离
  • 在任何方法中都可以轻松获取到该对象 .get

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问) 时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是ptivate static类型的,用于关联线程和线程上下文,它的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂性。

  • 定义:提供线程局部变量;一个线程局部变量在多个线程中,分别由独立的值(副本)
  • 特点:简单(开箱即用)、快速(无格外开销)、安全(线程安全)
  • 场景:多线程场景(资源持有、线程一致性、并发计算、线程安全等场景)
  • 实现原理:Java中使用哈希表实现
  • 应用范围:几乎所有提供多线程特征的语言
  • 设计者追求开箱即用的体验

ThreadLocal API

  • 构造函数 ThreadLocal()
  • 初始化 initialValue()
    • 该方法会返回当前线程对应的”初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
    • 当线程第一次使用get方法访问变量时,将调用此方法
    • 每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用此方法
    • 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来**重写initialValue()**方法
  • 访问器 T get():得到这个线程对应的value,如果是首次调用get()则会调用initialize来得到这个值 // void set(T t):为这个线程设置一个新值
  • 回收 void remove(): 删除这个线程
根据共享对象的生成时机不同,选择initialValue或set来保存对象
场景一:initialValue
  • 在ThreadLocal第一次get的时候把对象給初始化出来,对象的初始化时机可以由我们控制
场景二:set
  • 如果需要保存到ThreadLocal里的对象的生成时机不由我们随机控制,例如拦截器生成的用户信息
  • 用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用
使用ThreadLocal带来的好处
  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效地利用内存、节省开销
  • 避免传参的繁琐
threadpool/ThreadLocalAPI.java
package threadpool;

public class ThreadLocalAPI {
    public static ThreadLocal<Long> x = new ThreadLocal(){
        @Override
        protected Long initialValue(){
            System.out.println("Inital Value run...");
            return Thread.currentThread().getId();
        }
    };

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                System.out.println(x.get());
            }
        }.start(); //调用一次initialValue 每个线程单独拥有一个
        x.set(101l);
        x.remove();//清空线程 结果为1 因为 Thread.currentThread().getId();
        System.out.println(x.get());//发现x.remove被移除了 结果去重新触发initialValue
        System.out.println(x.get());
    }
}
========================================================================
Inital Value run...
Inital Value run...
20
1
1

若注释掉x.remove则会出现结果:
Inital Value run...
101
101
20

ThreadLocal的4种核心场景

  • 持有资源——持有线程资源供线程的各个部分使用,全局获取,减少编程难度
  • 线程一致——帮助需要保持线程一致的资源(如数据库事务) 维护一致性,降低编程难度
  • 线程安全——帮助只考虑了单线程的程序库,无缝向多线程场景迁移

ThreadLocal并发场景分析01

例1 200QPS压测统计接口
  • 观察200QPS下Spring框架的执行情况
  • 目标:理解并发、竞争条件、临界区等概念
  • 代表场景:交易场景
并发、竞争条件和临界区

ThreadLocal并发场景

  • 并发:多个程序同时执行
  • 竞争条件:多个进程(线程)同时访问同一个内存资源,最终的执行结果依赖于多个进程执行时的精确时序
  • 临界区:访问共享内存的程序片段
StatController.java
package com.imooc.springbootlearn;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller //并发可能导致同时 数据错误 需要加★后面的代码
public class StatController {
    //★★ 避免用锁 导致当数据多的话线程缓慢
    static ThreadLocal<Integer> c = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    //★ 请求并发也要排队synchronized(但是不要轻易使用锁) 应该怎么办 见★★
    synchronized void _add() throws InterruptedException {
        Thread.sleep(100);
        c.set(c.get() + 1);
    }

    @RequestMapping("/stat")
    public Integer stat() {
        return c.get();
    }

    @RequestMapping("/add")
    public Integer add() throws InterruptedException {
//        Thread.sleep(100l);
//        c++;
        _add();
        return 1;
    }

}
SpringBootlearnApplication.java
package com.imooc.springbootlearn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages="com.imooc")
public class SpringBootlearnApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootlearnApplication.class, args);
    }
}
总结
  • 基于线程池模型synchronize(排队操作很危险)
  • 用ThreadLocal收集数据很快速且安全
  • 思考:如何在多个ThreadLocal中收集数据?

ThreadLocal场景分析——减少同步

ThreadLocal< T >同步
StatController.java    高效解决高并发
package com.imooc.springbootlearn;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.HashSet;

@Controller //并发可能导致同时 数据错误 需要加★后面的代码
public class StatController {

    static HashSet<Val<Integer>> set = new HashSet<>();
    synchronized static void addSet(Val<Integer> v){
        set.add(v);
    }

    //★★ 避免用锁 导致当数据多的话线程缓慢
    static ThreadLocal<Val<Integer>> c = new ThreadLocal<Val<Integer>>() {
        @Override
        protected Val<Integer> initialValue() {
            Val<Integer> v = new Val<>();
            v.set(0);
            //set.add(v);//容易产生同步问题 小锁一下
            return v;
        }
    };

    //★ 请求并发也要排队synchronized(但是不要轻易使用锁) 应该怎么办 见★★
/*synchronized*/void _add() throws InterruptedException {
        Thread.sleep(100); //高并发
        Val<Integer> v = c.get();
        v.set(v.get() + 1);
    }

    @RequestMapping("/stat")
    public Integer stat() {
        return set.stream().map(x -> x.get()).reduce((a, x) -> a + x).get();
    }

    @RequestMapping("/add")
    public Integer add() throws InterruptedException {
//        Thread.sleep(100l);
//        c++;
        _add();
        return 1;
    }
}
Val.java
package com.imooc.springbootlearn;

public class Val<T>{
    T v;
    public void set(T _v){
        v = _v;
    }
    public T get(){
        return v;
    }
}
总结
  • 完全避免同步()
  • 缩小同步范围(简单) + ThreadLocal解决问题
  • 思考:还可以用在哪些场景?
源码分析1-Quartz SimpleSemaphore

  • Quartz的SimpleSemaphore提供资源隔离(上锁)
  • SimpleSemaphore中的lockOwners(ThreadLocal)为重度锁操作前置过滤
  • 思考:学易,用难!
源码分析2-Mybatis框架保持连接池线程一致
什么是本地事务
  • A(Atomic)原子性,操作不可分割
  • C(Consistency)一致性,任何时刻数据都能保持一致
  • I(Isolation)隔离性,多事务并发执行的时许不影响结果
  • D(Durability)持久性,对数据结构的存储是永久的

Begin → [本地事务:(更新订单状态 →<订单状态更新成功>→发放资源) → 资源发放成功(提交) → Commit →<持久化>→DB。**若资源发放失败(回滚)**→Rollback]

源码分析03 Spring框架对分布式事务的支持

技术选型——实现自己的ThreadLocal

MyThreadLocal.java
package com.imooc.springbootlearn;

import java.util.HashMap;
import java.util.Objects;

class MyThreadLocal<T> {
    static HashMap<Thread, HashMap<MyThreadLocal<?>, Objects>> threadLocalMap = new HashMap<>();//这里会产生临界区

    synchronized static HashMap<MyThreadLocal<?>, Objects> getMap() { //这是锁临界区
        var thread = Thread.currentThread();
        if (!threadLocalMap.containsKey(thread)) {
            threadLocalMap.put(thread, new HashMap<MyThreadLocal<?>, Objects>());
        }
        return threadLocalMap.get(thread);
    }

    T value;

    protected T initialValue() {
        return null;
    }

    public T get() {
        var map = getMap();
        if (!map.containsKey(this)) {
            map.put(this, initialValue());
        }
        return (T) map.get(this);
    }

    public void set(T v){
        var map = getMap();
        map.put(this, v);
    }
}
Test.java
package com.imooc.springbootlearn;

public class Test {
    static MyThreadLocal<Long> v = new MyThreadLocal<Long>(){
        @Override
        protected Long initialValue() {
         return Thread.currentThread().getId();
        }
    };

    public static void main(String[] args) {
        for (Integer i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(v.get());
            }).start();
        }
    }
}
问题
  • HashMap中直接存储了MyThreadLocal的引用,导致内存无法回收
  • 思考:可以用整数ID替代对MyThreadLocal的引用
MyThreadLocal.java
package com.imooc.springbootlearn;

import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

class MyThreadLocal<T> {
    static AtomicInteger atomic = new AtomicInteger(); //保证数据唯一性
    Integer threadLocalHash = atomic.addAndGet(0x61c88647); //这个值保证hash的更平均性
    static HashMap<Thread, HashMap<Integer, Objects>> threadLocalMap = new HashMap<>();//这里会产生临界区

    synchronized static HashMap<Integer, Objects> getMap() { //这是锁临界区
        var thread = Thread.currentThread();
        if (!threadLocalMap.containsKey(thread)) {
            threadLocalMap.put(thread, new HashMap<Integer, Objects>());
        }
        return threadLocalMap.get(thread);
    }

    T value;

    protected T initialValue() {
        return null;
    }

    public T get() {
        var map = getMap();
        if (!map.containsKey(this.threadLocalHash)) {
            map.put(this.threadLocalHash, initialValue());
        }
        return (T) map.get(this.threadLocalHash);
    }

    public void set(T v){
        var map = getMap();
        map.put(this.threadLocalHash, v);
    }
}
  • HashMap无限增加?初始空间分配是否合理?

HashTable

源码解读——哈希表实现ThreadLocal
什么是哈希表?

哈希表(散列 HashTable) 根据键(Key) 访问/设置内存中存储的位置的值

总结

  • 架构是严密且精确的东西(切记夸夸其谈)
  • 并发是一个很危险的场景,提高能力才会获得安全感
  • 保持怀疑,持续学习
  • 会用 → 场景查找 → 轻量实现 → 源码对照 → 场景沉淀
  • 程序架构:低耦合(独立)、高内聚(组合做到开箱即用)
  • 无论达到什么高度,要永远认为自己是个菜鸡

分布式章节

  • 什么是分布式
  • 分布式的作用
  • 分布式和单体结构的对比
  • CAP定理

  • 集群、分布式、微服务的区别

什么是分布式

  • 利用物理架构形成多个自治的处理元素,不共享主内存,但是通过发送信息合作。

  • 饭店初始的例子
    • 一个厨师 [单例]
    • 多个厨师
    • 术业有专攻:配菜师、洗菜工
  • 实际项目的演进过程

    • 一个项目,大而全
    • 多台机器,部署相同的应用
    • 分布式:权限系统、员工系统、请假系统

分布式的作用

为什么需要分布式
  • 实际工作中的痛点
    • 工程臃肿 [相互耦合 相互冲突]
    • 测试、上线繁琐
    • 开发效率低
单体应用的问题
  • 应用代码耦合严重,功能扩展难
  • 新需求开发交互周期长,测试工作量大
  • 新加入的开发同事需要很长时间才能熟悉系统
  • 升级维护也很困难(改动任何一点地方都要升级整个系统)
  • 系统性能提升艰难,可用性低,不稳定
分布式的好处
  • 增大系统容量
  • 加强系统可用[某个模块出bug 但不影响其他]
  • 因为模块化,所以系统模块重用度更高
  • 因为软件服务模块被拆分,开发和发布速度可以并行而变得更快
  • 系统扩展性更高
  • 团队协作流程也会得到改善
  • 技术升级

单体和分布式的对比

分布式和单体结构的对比
传统单体构架 分布式构架
新人的学习成本 业务逻辑成本高 架构逻辑成本高
部署、运维 容易 发布频繁,顺序复杂、运维难
隔离性 一损俱损,殃及鱼池 故障影响范围小
架构设计 难度低 难度指数级上升
系统性能 响应快、吞吐量小 响应慢、吞吐量大
测试成本 很高
技术多样性 技术单一且封闭 技术多样且开放
系统扩展性 扩展性差 扩展性很好
系统管理成本 成本低 成本高

CAP定理

CAP的重要性

分布式不可能同时满足三个条件

CAP理论是什么?
  • C(Consistency, 一致性):读操作是否总能读到前一个写操作的结果
  • A(Availability, 可用性):非故障节点应该在合理的时间内作出合理的响应(不是错误或超时的响应),但是可能不是最新的数据
  • P(Partition tolerance, 分区容错):当出现网络分区现象后,系统能够继续运行

CAP如何选择?
  • CP[支付宝]或者AP[超级跑跑系统维护]
  • 在什么场合,可用性高于一致性?
    • 网页必须要保障可用性(一定能看到最重要 是不是最新的不重要)和分区容错
    • 支付的时候一定要保障一致性(我可以保证不可用 但我不允许余额不一致)和分区容错
  • 合适的才是最好的

集群、分布式、微服务的区别

集群和分布式的区别
  • 分布式:一个业务分拆多个子业务,部署在不同的服务器上 [服务器之间要通信]
  • 集群:同一个业务,部署在多个服务器上 [五台机器可以不通信]
集群和微服务的区别
  • 集群:分散压力
  • 微服务:分散压力
微服务和分布式的区别
  • 微服务是架构设计方式 [逻辑架构]

  • 分布式是系统部署方式 [物理架构]

  • 微服务:是一种架构方式 [大的服务拆成小的服务 每个服务独立开发测试]

  • 分布式:主要强调部署的方式

Docker

  • 基本概念、用途、核心思想
  • Docker的组成、架构、重要概念
  • Docker的安装
  • 第一个Docker容器
  • 运用Nginx镜像,并访问到Docker容器内部 [从外部进行访问]
  • 制作自己的Docker容器,dockerfile实战

Docker的基本概念、用途、核心思想

  • Docker应用广泛
    京东618:15万个Docker实例,所有业务全部容器化
  • Docker是什么?
    [以前的图标是大鲸鱼拖着集装箱]
    • Docker是一个用来装程序及其环境的容器[类似于安装包.exe],属于Linux容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的Linux容器解决方案
    • 比喻:客车可以装人,衣柜可以放衣服 [Windows下写的小游戏也可以放在Docker中]

为什么需要Docker

  • 环境配置的难题
  • 虚拟机[资源占用很多 模拟一套完整系统但步骤多(有些步骤无法跳过) 启动慢]
  • Docker的基础——Linux容器 [体积小 速度快 轻量级虚拟机]

Docker的用途

  • 提供统一的环境
  • 提供快速拓展、弹性伸缩的云服务 [解决双十一淘宝+天猫 数据量剧增问题]
  • 防止其他用户的进程把服务器资源占用过多 [程序相互之间可以隔离]
  • 部署简单 运维简单 节省服务器资源

Docker的特点

标准化
  • 运输方式(把程序和环境从一个机器运送到另一个机器)

  • 存储方式(程序和环境的存储)

  • API接口(不需要Tomcat等应用的命令了,都标准化了)

  • 灵活:即使是最复杂的应用也可以集装箱化

  • 轻量级:容器利用并共享主机内核

  • 便携式:可以在本地构建,部署到云,并在任何地方运行

Docker带来的好处
  • 开发团队的好处 完全可以控制所有的环境[同一个镜像] 降低了风险

image镜像

  • 存储:联合文件系统,UnionFS

容器和仓库

  • 镜像类似于Java中的类,而容器就是实例化
  • 容器的这一层是可以修改的,而镜像是不可以修改的
  • 同一个镜像可以生成多个容器独立运行,而她们之间没有任何的干扰
仓库
client和deamon
  • client[客户端]:提供給用户一个终端,用户输入Docker提供的命令来管理本地或远程的服务器
  • deamon服务端守护进程,接收Client发送的命令并执行相应的操作

Docker的安装

更换系统镜像

云服务器管理控制台 (aliyun.com) → 实例 → 更多 → 更换操作系统 → CentOS 7.6 64位

C:\Users\Pluminary>ssh root@47.98.225.105
    
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:THXnakQ0Se5ee+d7oHO0NYKBTW7mhEKc426m9+rQgnk.
Please contact your system administrator.
Add correct host key in C:\\Users\\Pluminary/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in C:\\Users\\Pluminary/.ssh/known_hosts:7
Host key for 47.98.225.105 has changed and you have requested strict checking.
Host key verification failed.
    
//使用命令或手动删除 C:\\Users\\Pluminary/.ssh/known_hosts 防火墙拦截了
vim C:\\Users\\Pluminary/.ssh/known_hosts

C:\Users\Pluminary>ssh root@47.98.225.105
root@47.98.225.105's password:Panchunyao123!

[root@iZbp1dssknxftmjczbtpndZ ~]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)
//yum地址换成国内yum源
   //wget-O 这是大写的字母O 含义是放到指定目录下 替换本地文件为国内文件
[root@iZbp1dssknxftmjczbtpndZ ~]# wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
[root@iZbp1dssknxftmjczbtpndZ ~]# yum clean all
[root@iZbp1dssknxftmjczbtpndZ ~]# yum makecache //读取新的源

//较旧的Docker版本称为docker或docker-engine。如果已安装这些程序,请卸载它们以及相关的依赖项。
[root@iZbp1dssknxftmjczbtpndZ ~]# yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
//如果yum报告未安装这些软件包,也没问题。
    //更新yum 先查看需要更新哪些
[root@iZbp1dssknxftmjczbtpndZ ~]# yum check-update
[root@iZbp1dssknxftmjczbtpndZ ~]# yum update
// 安装所需的软件包
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install -y yum-utils \
                  device-mapper-persistent-data \
                  lvm2
// 使用以下命令来设置稳定的存储库 sudo是用超级管理员
[root@iZbp1dssknxftmjczbtpndZ ~]# sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

// 查看docker版本
[root@iZbp1dssknxftmjczbtpndZ ~]# yum list docker-ce --showduplicates | sort -r
// 安装指定的版本
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install docker-ce-18.09.0 docker-ce-cli-18.09.0 containerd.io
// Docker 是服务器----客户端架构
// 命令行运行docker命令的时候,需要本机有 Docker 服务。用下面的命令启动
[root@iZbp1dssknxftmjczbtpndZ ~]# systemctl start docker
// 安装完成后,运行下面的命令,验证是否安装成功。
[root@iZbp1dssknxftmjczbtpndZ ~]# docker version 或者 docker info

第一个Docker容器

  • 下载镜像
  • docker pull [OPTIONS] NAME[:TAG]
  • docker images [OPTIONS] [REPOSITORY[:TAG]] 查看本机有没有任何镜像
[root@iZbp1dssknxftmjczbtpndZ ~]# docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
c1ec31eb5944: Pull complete
Digest: sha256:53641cd209a4fecfc68e21a99871ce8c6920b2e7502df0a20671c6fccc73a7c6
Status: Downloaded newer image for hello-world:latest
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              d2c94e258dcb        11 months ago       13.3kB
  • 运行镜像
  • docker run [OPTIONS] IMAGE [COMMAND] [ARG…]
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal. 

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

后台运行容器

运行Nginx镜像,并访问到Docker容器内部
  • 前台、后台
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images //查看一下程序镜像是否还在
[root@iZbp1dssknxftmjczbtpndZ ~]# docker pull hub.c.163.com/library/nginx:1.13.0
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
hello-world                   latest              d2c94e258dcb        11 months ago       13.3kB
hub.c.163.com/library/nginx   1.13.0              46102226f2fd        6 years ago         109MB
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run hub.c.163.com/library/nginx:1.13.0
//上面的没反应是正常情况,再开一个cmd窗口


//第二个窗口:另一个cmd窗口 
C:\Users\Pluminary>ssh root@47.98.225.105
root@47.98.225.105's password:
Last login: Sun Apr  7 16:30:40 2024 from 182.102.75.173

Welcome to Alibaba Cloud Elastic Compute Service !
//展示当前容器列表
[root@iZbp1dssknxftmjczbtpndZ ~]# docker ps
CONTAINER ID        IMAGE                                COMMAND                  CREATED              STATUS              PORTS               NAMES
298f7e19f6a8        hub.c.163.com/library/nginx:1.13.0   "nginx -g 'daemon of…"   About a minute ago   Up About a minute   80/tcp              kind_taussig
//这时回到第一个窗口 ctrl+c 关掉运行 这时去第二个窗口docker ps的时候已经没有了
// ↓↓↓↓↓↓ 要把Nginx在后台运行才可以解决上述问题 ↓↓↓↓↓↓
[root@iZbp1dssknxftmjczbtpndZ ~]# docekr run -d hub.c.163.com/library/nginx:1.13.0
f124fc9171824e508639768b800efba5c780385dc1d135ab0ff70d80d3d75510 //返回容器ID
//此时再去第二个窗口 docker ps就会看到 CONTAINER ID 有f124fc917182
//第一个窗口 查看容器内部的风景 -i让容器输入有效 t是給我们分配一个终端 bash是启动终端
[root@iZbp1dssknxftmjczbtpndZ ~]# docker exec -it f124 bash //让docker明白我们让哪个启动
root@f124fc917182:/#  //已经进入到容器内部
root@f124fc917182:/# pwd
/
root@f124fc917182:/# touch 1 //创建了一个1
root@f124fc917182:/# ls
1  bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@f124fc917182:/# which nginx
/usr/sbin/nginx

Docker三种网络模式 [网络会隔离 需要配置进行端口映射]

  • Bridge 网卡网络独立的端口
  • Host 同步宿主机的端口
  • None
  • 端口映射技术
实操
  • 访问Docker内的Nginx
cmd窗口2
[root@iZbp1dssknxftmjczbtpndZ ~]# docker stop f12
f12
[root@iZbp1dssknxftmjczbtpndZ ~]# docker ps //目前没有容器在运行了
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
    //-p 本机的8080端口去映射内部的80端口 前面的是本机 后面的是容器内部的
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run -d -p 8080:80 hub.c.163.com/library/nginx:1.13.0
3906129ced72b668581a58dc36595b08734b82e247e2927eb2a8da6fbe7508ae
//拔出端口的信息
[root@iZbp1dssknxftmjczbtpndZ ~]# netstat -na|grep 8080
tcp6       0      0 :::8080                 :::*                    LISTEN

在网页上输入 http://47.98.225.105:8080/

Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.
For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.
Thank you for using nginx.

这个不是在宿主机启动的nginx 而是在容器中启动的【通过宿主机中转 并通过Bridge实现独立】
★★ 通过一台服务器上去布置多台Docker 而每台docker里面是独立的 并且可以通过一个端口 来实现和外界的映射关系 ★★
//这个与7行代码的区别 这里是大P 将这个容器的所有端口都进行映射
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run -d -P hub.c.163.com/library/nginx:1.13.0

制作自己的Docker容器,dockerfile实战

[把自己的软件程序打包好传給别人]

  • dockerfile作用
  • 使用dockerfile的好处[一目了然 哪个环境跑在什么环境下 非常方便知道如何配置]
  • 写一个自己的dockerfile

因此我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置,之后把dockerfile交给“编译器”docker进行“编译”,也就是docker build命令,生成的可执行程序就是image,之后就可以运行这个image了,这就是docker run命令,image运行起来后就是docker container。

FROM alpine:latest #继承父类 alpine及其小的环境
MAINTAINER imooc   #描述这个镜像由誰维护的
CMD echo 'hello my dockerfile'  #进入任何想要的dockerfile命令
[root@iZbp1dssknxftmjczbtpndZ ~]# touch Dockerfile
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
Dockerfile
[root@iZbp1dssknxftmjczbtpndZ ~]# vim Dockerfile
//按小写i进入编辑模式  按ESC退出编辑模式 :wq会保存
//取名字 .dockerfile现在的路径
[root@iZbp1dssknxftmjczbtpndZ ~]# docker build -t hello_docker .
    //先发送到服务器
Sending build context to Docker daemon  285.7kB
    //从父类中拉取下来
Step 1/3 : FROM alpine:latest
latest: Pulling from library/alpine
4abcf2066143: Pull complete
Digest: sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
Status: Downloaded newer image for alpine:latest
 ---> 05455a08881e
    //写定它的维护者
Step 2/3 : MAINTAINER imooc
 ---> Running in 38a73abca50e
Removing intermediate container 38a73abca50e
---> f44831f49afe
    //把这个语句写进去
Step 3/3 : CMD echo 'hello my dockerfile'
 ---> Running in 1ff431539208
Removing intermediate container 1ff431539208
 ---> f45b88d0cabc
Successfully built f45b88d0cabc
Successfully tagged hello_docker:latest

//运行自己的镜像
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
hello_docker                  latest              f45b88d0cabc        3 minutes ago       7.38MB
alpine                        latest              05455a08881e        2 months ago        7.38MB
hello-world                   latest              d2c94e258dcb        11 months ago       13.3kB
hub.c.163.com/library/nginx   1.13.0              46102226f2fd        6 years ago         109MB
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run hello_docker
hello my dockerfile

Nginx

  • Nginx介绍
  • Nginx的安装
  • 常用命令讲解和演示
  • 配置文件讲解
  • 场景实战:搭建一个静态文件的Nginx服务

Nginx介绍

  • Nginx是什么、适用场景
  • Nginx应用广泛
  • Nginx优点

Nginx是什么、适用场景

  • HTTP的反向代理服务器
  • 动态静态资源分离
正向代理

普通的请求转发,客户端把信息传递到代理服务器,代理服务器找到信息转发给我们
提供安全功能 代理服务器有防火墙,可以隐藏自身的信息

反向代理

提供安全和防火墙 在多个服务器后端提供负载均衡 为服务器提供缓存 把来自用户的压力平均分配[负载均衡]

动态静态资源分离 [加速访问 整体速度提高]
  • 不分离会变慢
  • 静态资源无需经过Tomcat,Tomcat只负责处理动态请求
  • 后缀为gif的时候,Nginx会直接获取到当前请求的文件并返回

Nginx应用广泛

Nginx的优点
  • 高并发、高性能
  • 可扩展性好
  • 高可靠性 [服务器运行可以达到数年之久]
  • 热部署
  • 开源、可商用

Nginx的安装

Nginx在CentOs安装
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install yum-utils
[root@iZbp1dssknxftmjczbtpndZ ~]# vim /etc/yum.repos.d/nginx.repo  //输入源信息 告诉它从哪下载
//输入:3-18行 配置源
[root@iZbp1dssknxftmjczbtpndZ ~]# [nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
    //然后查看源 列出可以使用的ngix地址
[root@iZbp1dssknxftmjczbtpndZ ~]# yum list | grep nginx
    //运行安装命令
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install nginx 1:1.16.1-1.el7.ngx
    //查看版本,若出现版本号,则安装成功
[root@iZbp1dssknxftmjczbtpndZ ~]# nginx -v
nginx version: nginx/1.24.0
用whereis nginx可以查看到目录:
[root@iZbp1dssknxftmjczbtpndZ ~]# nginx: /usr/sbin/nginx /usr/lib64/nginx /etc/nginx /usr/share/nginx /usr/share/man/man8/nginx.8.gz

常用命令讲解和演示

[root@iZbp1dssknxftmjczbtpndZ ~]# /usr/sbin/ngix 启动
[root@iZbp1dssknxftmjczbtpndZ ~]# systemctl stop docker //把之前的docker关停
[root@iZbp1dssknxftmjczbtpndZ ~]# ps -aux | grep nginx //提取相关进程 80是默认端口
[root@iZbp1dssknxftmjczbtpndZ ~]# nginx -h //提取帮助
    
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /etc/nginx
[root@iZbp1dssknxftmjczbtpndZ nginx]# ls //里面有nginx.conf配置文件 conf.d中也有一个defalut.conf
conf.d  fastcgi_params  mime.types  modules  nginx.conf  scgi_params  uwsgi_params
[root@iZbp1dssknxftmjczbtpndZ nginx]# cd conf.d/
    
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -c   //读取指定配置文件
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -s stop //停止nginx
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -c /etc/nginx/nginx.conf //以这个文件启动nginx

[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -t //发布前的测试
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -s stop
[root@iZbp1dssknxftmjczbtpndZ conf.d]# /usr/sbin/nginx //开启nginx
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -t //查看使用的哪个配置文件
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -v //打印版本信息
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -V //打印详细参数
    
[root@iZbp1dssknxftmjczbtpndZ conf.d]# whereis uginx//找nginx的路径
nginx: /usr/sbin/nginx /usr/lib64/nginx /etc/nginx /usr/share/nginx /usr/share/man/man8/nginx.8.gz
[root@iZbp1dssknxftmjczbtpndZ conf.d]# ps aux | grep nginx //打印进程信息

    //-s信号 1.stop立即停止 2.quit优雅停止 3.reload重启[优雅停止quit] 4.reopen更换日志文件

配置文件讲解

  • 语法
    • “; “ 结尾
    • “{}” 组织多条指令
    • “include” 引入
    • ‘’#’’ 注释
  • 实操演示
  • 默认配置文件分析
    • nginx.conf
    • default.conf
[root@iZbp1dssknxftmjczbtpndZ conf.d]# cat /etc/nginx/nginx.conf //打开文件配置
nginx.conf配置文件讲解
首先我们进入到cd etc/nginx.然后通过ls查看nginx目录的相关内容。在nginx目录下,我们需要关注nginx.conf文件,这个文件是我们的主配置文件,cat打开:
cat nginx.conf 【效果显示在下方分割线后】

[root@iZbp1dssknxftmjczbtpndZ conf.d]# cd /usr/share/nginx/html
[root@iZbp1dssknxftmjczbtpndZ html]# vim stst.html //新建文件
[root@iZbp1dssknxftmjczbtpndZ html]# ls
50x.html     index.html
[root@iZbp1dssknxftmjczbtpndZ html# vim test.html //新建一个html文件保存下来
//i编辑       hello nginx           esc + :wq
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -c /etc/nginx/nginx.conf //启动!
//如果不显示一定是服务器规则组的问题  自定义 TCP    目的:1/65535   源:0.0.0.0/0
网址输入:http://47.98.225.105/test.html 
============================================================================
# 运行用户,默认是nginx
user  nginx;
# nginx进程数,一般设置为和cpu核数一样
worker_processes  1;

# 全局错误日志路径
error_log  /var/log/nginx/error.log warn;
# 进程pid路径
pid        /var/run/nginx.pid;
 
events {
# 最大连接数
    worker_connections  1024;
}

# 设置http服务器
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
# 设置日志的格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志的路径
    access_log  /var/log/nginx/access.log  main;

# 开启高效传输模式
    sendfile        on;
    #tcp_nopush     on;
# 长连接超时时间,单位是秒
    keepalive_timeout  65;
#传输时是否压缩,压缩的话需要解压,但是传的大小就小了
    #gzip  on;
#加载其他的配置文件,一带多
    include /etc/nginx/conf.d/*.conf;
}
============================================================================

搭建静态资源服务器

搭建一个静态文件的Nginx服务
  • 实操演示
  • 配置和网页文件作为教辅提供 [静态网页]
//scp传输文件 -r每一个文件夹都传递归 /usr/share/nginx/web/ 上传到这个文件夹
cmd本机框
C:\Users\Pluminary> scp -r /Users/Pluminary/Desktop/静态资源/静态网页/ root@47.98.225.105:/usr/share/nginx/web/

cmd服务器框
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /usr/share/nginx/web/
[root@iZbp1dssknxftmjczbtpndZ web]# pwd //查看现在位置
/usr/share/nginx/web
[root@iZbp1dssknxftmjczbtpndZ web]# ls
css  fonts  images  index.html  js
//修改文件 可以让nginx访问到文件 访问web下面的内容
[root@iZbp1dssknxftmjczbtpndZ web]# vim /etc/nginx/conf.d/default.conf  
//修改location:.../web
===============================================================
location / {
        root   /usr/share/nginx/web;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/web;
    }
===============================================================
[root@iZbp1dssknxftmjczbtpndZ web]# nginx -t //测试一下是否有问题
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
[root@iZbp1dssknxftmjczbtpndZ web]# nginx -s reload //重启服务
//浏览器访问:http://47.98.225.105/index.html
[root@iZbp1dssknxftmjczbtpndZ web]# cat /etc/nginx/nginx.conf
//进去看到 access_log  /var/log/nginx/access.log  main; 打开它
[root@iZbp1dssknxftmjczbtpndZ web]# cat /var/log/nginx/access.log //打开日志文件

RabbitMQ

  • 初识RabbitMQ
  • RabbitMQ的安装和启动 [Erlang语言]
  • RabbitMQ管理后台
  • 实战案例演示
  • 交换机工作模式 [fanout、direct、topic、headers]
  • SpringBoot整合RabbitMQ

初识RabbitMQ

  • 核心思想:接收并转发消息。类似于邮局

  • producer信息生产者

  • queue队列

  • consumer会从queue中获取消息

消息队列

  • 什么是消息队列
  • MQ(Message Queue)
消息队列的性质
  • 业务无关 [不需要考虑上层业务模型]
  • FIFO [先进先出]
  • 容灾 [对于消息队列可以持久化 断电后也可以保存数据后重新发送]
  • 性能
为什么要用消息队列
  • 系统解耦
  • 异步调用 [我告诉你要做 我可以不等你做完 我再返回 相互不影响]
    [点外卖 用户发送外卖请求給中间件,随后MQ帮助分发后续流程(扣钱、召唤骑手 计算天气、时间等)]
  • 流量削峰 [MQ把请求先存在队列中 以合适的速度发送 化解压力]

RabbitMQ的特点和核心概念

  • 开源、跨语言
  • Erlang语言编写 [交换机、通信方面] [数据复制与转发性能好]
  • 应用广泛
  • 社区活跃、API丰富
AMQP协议
  • Advanced Message Queuing Protocol 高级消息队列协议
RabbitMQ核心概念
  • Server:服务
  • connection:与Server建立连接
  • channel:信道,几乎所有的操作都在信道上进行,客户端可以建立多个信道
  • message:消息,由properties和body组成
  • virtual host:虚拟主机,顶层隔离。同一个虚拟主机下,不能由重复的交换机和queue
  • Exchange:交换机,接收生产者的信息的,然后根据指定的路由器把消息转发到所绑定的队列上
  • binding:绑定交换机和队列
  • routing key:路由键,路由规则,虚拟机可以用它来确定这个消息如何进行一个路由
  • queue:队列,消费者只需要监听队列来消费消息,不需要关注消息来自于哪个Exchange
  • Exchange和Message Queue存在着绑定的关系,一个Exchange可以绑定多个消息队列

RabbitMQ的安装和启动

  • 安装Erlang
  • 安装RabbitMQ
  • 启动RabbitMQ
安装Erlang
  • 安装erlang-rpm包,该包经过RabbitMQ官方处理
  • 使用Erlang Solutions源进行安装
  • 使用EPEL(“Extra Packages for Enterprise Linux”)进行安装
我们先准备两个安装包://★★★★别看下面胡咧咧 直接教辅中有erlang和rabbitmq与教材相同★★★★
RabbitMQ: https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.8.13
Erlang:https://packages.erlang-solutions.com/erlang-solutions-2.0-1.noarch.rpm

注意: 如果说你选的是最新的版本那么你就要选取对应支持的 erlang 的版本才行,具体看下面的连接
https://www.rabbitmq.com/which-erlang.html

这些都准备好之后我们使用 Xptf 软件把这个安装包传输到 linux 端对应文件下(自己建一个文件夹)即可。

安装
第一步:
我们第一步先要对 linux 端进行一个 erlang 的环境配置,所以我们要先解压 erlang 包。
    # rpm -Uvh erlang-solutions-2.0-1.noarch.rpm
    # yum install -y erlang
安装完成后,查看一下版本号,能查到说明就是安装好了
    # erl -v

第二步:
在安装一下
    # yum install -y socat

第三步:
接下来我们就需要进行 RabbitMQ 的安装了

    # rpm -Uvh rabbitmq-server-3.8.33-1.el8.noarch.rpm 
    # yum install rabbitmq-server -y

安装完之后我们来启动测试一下:

    # systemctl start rabbitmq-server
    # systemctl status rebbitmq-server

最后一步:
安装跑起来之后我们设置一下开机自启动

    # systemctl enable rabbitmq-server
    # systemctl stop rabbitmq-server   # 关闭开机自起
=========================================================
可以在这些目录中查找RabbitMQ的配置文件、启动脚本等。另外,RabbitMQ的启动脚本通常会被安装到/usr/sbin或/usr/bin目录下。你可以尝试使用以下命令来查找RabbitMQ的启动脚本位置:

find /usr/sbin /usr/bin -name 'rabbitmq*'
=========================================================
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /usr/sbin
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl start rabbitmq-server
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl status rabbitmq-server
● rabbitmq-server.service - RabbitMQ broker
   Loaded: loaded (/usr/lib/systemd/system/rabbitmq-server.service; disabled; vendor preset: disabled)
   Active: active (running) since Mon 2024-04-08 23:24:39 CST; 38min ago
 Main PID: 3570 (beam.smp)
===================================================================================
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl enable rabbitmq-server
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl stop rabbitmq-server   # 关闭开机自起

Linux环境如何彻底卸载感干净RabbitMQ_linux卸载rabbitmq-CSDN博客

停止RabbitMQ
$rabbitmqctl stop

设置开机启动
$ systemctl enable rabbitmq-server 

启动RabbitMQ
$ systemctl start rabbitmq-server

看看端口有没有起来,查看状态

$ rabbitmqctl status 

要检查RabbitMQ服务器的状态,请运行:

systemctl status rabbitmq-server

开启web管理界面
rabbitmq-plugins enable rabbitmq_management

RabbitMQ管理后台

启动RabbitMQ
  • 启动RabbitMQ:systemctl start rabbitmq-server
  • 查看状态:rabbitmqctl status
  • 启动管理台
  • 配置admin用户
[root@iZbp1dssknxftmjczbtpndZ ~]# rabbitmq-plugins enable rabbitmq_management
Enabling plugins on node rabbit@iZbp1dssknxftmjczbtpndZ:
rabbitmq_management
The following plugins have been configured:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@iZbp1dssknxftmjczbtpndZ...
Plugin configuration unchanged. ##添加账户
[root@iZbp1dssknxftmjczbtpndZ ~]# rabbitmqctl add_user admin password
Adding user "admin" ...            ##设置管理员
[root@iZbp1dssknxftmjczbtpndZ ~]# rabbitmqctl set_user_tags admin administrator
Setting tags for user "admin" to [administrator] ...
浏览器进入RabbitMQ管理后台 输入 http://47.98.225.105:15672/
RabbitMQ管理后台
  • 浏览页面
  • 添加用户
  • 创建虚拟主机(Virtual Hosts) => 都有各自的队列、交换机

第一个生产者

实战案例演示
  • 新建项目
  • Hello World
    P(生产者)→hello→C(消费者)
  • 创建一个rabbitmq(Maven)新项目
  • rabbitmq支持多语言
登录上RabbitMQ后台 上面Admin 右面Virtual Hosts 
点表里的 /  下面增加一个admin

pom.xml
<?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>org.example</groupId>
    <artifactId>rabbitmq</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>20</maven.compiler.source>
        <maven.compiler.target>20</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.29</version>
        </dependency>
    </dependencies>

</project>
helloworld/send.java
package helloworld;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * Hello World的发送类,连接到RabbitMQ服务端,然后发送一条消息后退出
 */
public class send {
    //队列名字
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //发布消息
        String message = "Hello World!";
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + message);
        //关闭连接
        channel.close();
        connection.close();
    }
}
helloworld/Recv.java
package helloworld;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;

/**
 * 接收消息,并打印,持续运行
 */
public class Recv {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println("开始接收消息");
        //接收消息并消费 2:是否确认收到(快递签收 自动消息确认) 3:消息收到后进行处理
        channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
            //收到信息后会执行的函数
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:" + message);
            }
        });
    }
}
=======================================================
//由于是持续接收消息的 若将send.java中改为Hello World2 将会持续打印到控制台
开始接收消息
收到消息:Hello World!
收到消息:Hello World2!

根据消息内容做处理

多个消费者分担压力

RabbitMQ后台http://47.98.225.105:15672/

循环调度
workqueues/NewTask.java
package workqueues;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 生产者:生产批量消息
 */
public class NewTask {
    private final static String TAKS_QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(TAKS_QUEUE_NAME,true,false,false,null);
        for (int i = 0; i < 10; i++) {
            String message = i + "...";
            channel.basicPublish("",TAKS_QUEUE_NAME,null,message.getBytes("UTF-8"));
            System.out.println("发送了消息:" + message);
        }
        //关闭连接
        channel.close();
        connection.close();
    }
}
=============================================================================================
发送了消息:0...
发送了消息:1...
发送了消息:2...
发送了消息:3...
发送了消息:4...
发送了消息:5...
发送了消息:6...
发送了消息:7...
发送了消息:8...
发送了消息:9...
workqueues/Worker.java
package workqueues;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 消费者,接收前面的批量消息
 */
public class Worker {
    private final static String TAKS_QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(TAKS_QUEUE_NAME, true, false, false, null);
        System.out.println("开始接收消息");
        channel.basicConsume(TAKS_QUEUE_NAME, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到了消息:" + message);
                try{
                    doWork(message);
                }finally {
                    System.out.println("完成消息处理");
                }
            }
        });
    }

    private static void doWork(String task) {
        //有点延迟1秒
        char[] chars = task.toCharArray();
        for (char ch : chars) {
            if (ch == '.') {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
==========================================================================================
收到了消息:0...
完成消息处理
收到了消息:1...
完成消息处理
收到了消息:2...
完成消息处理
收到了消息:3...
完成消息处理
收到了消息:4...
完成消息处理
收到了消息:5...
完成消息处理
收到了消息:6...
完成消息处理
收到了消息:7...
完成消息处理
收到了消息:8...
完成消息处理
收到了消息:9...
完成消息处理
并行 多个消费者平均压力!!!
公平派遣[因为奇数偶数压力不同] + 消息确认 【以工作量的程度去分任务
//在Worker消费方打开并行消费 Run/Debug Configurations → Modify options蓝字 → Allow multiple instances             此时两个woker开始配合平均压力 一起工作
workqueues/Worker.java
package workqueues;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 消费者,接收前面的批量消息
 */
public class Worker {
    private final static String TAKS_QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(TAKS_QUEUE_NAME, true, false, false, null);
        System.out.println("开始接收消息");
        channel.basicQos(1); //最希望处理的数量 处理完之前不会接收下一个任务
        channel.basicConsume(TAKS_QUEUE_NAME, false, new DefaultConsumer(channel) { //关掉自动接收 要手动确认false
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到了消息:" + message);
                try{
                    doWork(message);
                }finally {
                    System.out.println("完成消息处理");
                    channel.basicAck(envelope.getDeliveryTag(), false); //false 不同时一起确认 手动确认消息
                }
            }
        });
    }

    private static void doWork(String task) {
        //有点延迟1秒
        char[] chars = task.toCharArray();
        for (char ch : chars) {
            if (ch == '.') {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
=======================================================================================
开始接收消息
收到了消息:1
消息处理完成
收到了消息:2...
消息处理完成
......

交换机工作模式

  • fanout:广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的
  • direct:根据RoutingKey匹配消息路由到指定队列
  • topic:生产者指定RoutingKey消息根据消费端指定的队列通过模糊匹配的方式进行相应转发
  • headers:根据发送消息内容中的headers属性来匹配
fanout模式
广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的 [所有消息无差别发送]

fanout/EmitLog.java【生产者】
package fanout;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 发送日志信息
 */
public class EmitLog {
    //创建交换机
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        String message = "info: Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + message);
        channel.close();
        connection.close();
    }
}
fanout/ReceiveLogs.java【消费者】
package fanout;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 描述:接收日志消息 有多个接收同样的消息
 */
public class ReceiveLogs {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        //非持久会自动删除的队列 在同一个类中多次启动 每一次队列名字都不一样
        String queueName = channel.queueDeclare().getQueue();
        //交换机和队列的绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println("开始接收消息");
        Consumer consumer =  new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:"+message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
direct模式
根据RoutingKey匹配消息路由到指定队列 [消费者接收消息不一致]

direct/ReceiveLogsDirect1.java
package direct;

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * 接收3个等级的日志
 */
public class ReceiveLogsDirect1 {

    private static final String EXCHANGE_NAME = "direct_log";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //生成一个随机的临时的queue
        String queueName = channel.queueDeclare().getQueue();

        //一个交换机同时绑定三个queue
        channel.queueBind(queueName, EXCHANGE_NAME, "info");
        channel.queueBind(queueName, EXCHANGE_NAME, "warning");
        channel.queueBind(queueName, EXCHANGE_NAME, "error");

        System.out.println(" 开始接收消息");

        Consumer consumer =  new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:"+message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
=============================================================================
收到消息:Hello World!
收到消息:Hello World!
收到消息:Hello World!
收到消息:Hello World!
direct/ReceiveLogsDirect2.java
package direct;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;

/**
 * 接收1个等级的日志
 */
public class ReceiveLogsDirect2 {

    private static final String EXCHANGE_NAME = "direct_log";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //生成一个随机的临时的queue
        String queueName = channel.queueDeclare().getQueue();

        //一个交换机同时绑定两个queue,比刚第一个Receiver少接收了一个error等级
        channel.queueBind(queueName, EXCHANGE_NAME, "info");
        channel.queueBind(queueName, EXCHANGE_NAME, "warning");

        System.out.println("开始接收消息");

        Consumer consumer =  new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:"+message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
=============================================================================
收到消息:Hello World!
收到消息:Hello World!
收到消息:Hello World!
direct/EmitLogDirect.java
package direct;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class EmitLogDirect {

    private static final String EXCHANGE_NAME = "direct_log";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机 DIRECT
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String message = "Hello World!";
        //发送第一个等级消息
        channel.basicPublish(EXCHANGE_NAME, "info",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + "等级为info,消息内容:" + message);

        //发送第二个等级消息
        channel.basicPublish(EXCHANGE_NAME, "warning",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + "等级为warning,消息内容:" + message);

        //发送第三个等级消息
        channel.basicPublish(EXCHANGE_NAME, "error",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + "等级为error,消息内容:" + message);
        channel.close();
        connection.close();
    }
}
=============================================================================
发送了消息:等级为info,消息内容:Hello World!
发送了消息:等级为warning,消息内容:Hello World!
发送了消息:等级为error,消息内容:Hello World!
topic模式
比如消息严重性怎么样、只想记录error模块的用户信息
  • ***** 可以替代一个单词
  • # 可以替代零个或多个单词

【发送者/生产者】
topic/EmitLogTopic.java
package topic;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

/**
 * topic模式交换机,发送消息
 */
public class EmitLogTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) {
        Connection connection = null;
        Channel channel = null;
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("47.98.225.105");
            factory.setUsername("admin");
            factory.setPassword("password");

            connection = factory.newConnection();
            channel = connection.createChannel();

            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            String message = "Animal WorldroutingKey";

            String[] routingKeys = new String[9];
            routingKeys[0] = "quick.orange.rabbit";
            routingKeys[1] = "lazy.orange.elephant";
            routingKeys[2] = "quick.orange.fox";
            routingKeys[3] = "lazy.brown.fox";
            routingKeys[4] = "lazy.pink.rabbit";
            routingKeys[5] = "quick.brown.fox";
            routingKeys[6] = "orange";
            routingKeys[7] = "quick.orange.male.rabbit";
            routingKeys[8] = "lazy.orange.male.rabbit";
            for (int i = 0; i < routingKeys.length; i++) {
                channel.basicPublish(EXCHANGE_NAME, routingKeys[i], null,
                        message.getBytes("UTF-8"));
                System.out.println("发送了:" + message + ":" + routingKeys[i]);
            }


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ignore) {
                }
            }
        }
    }
}
==================================================================================
发送了:Animal WorldroutingKey:quick.orange.rabbit
发送了:Animal WorldroutingKey:lazy.orange.elephant
发送了:Animal WorldroutingKey:quick.orange.fox
发送了:Animal WorldroutingKey:lazy.brown.fox
发送了:Animal WorldroutingKey:lazy.pink.rabbit
发送了:Animal WorldroutingKey:quick.brown.fox
发送了:Animal WorldroutingKey:orange
发送了:Animal WorldroutingKey:quick.orange.male.rabbit
发送了:Animal WorldroutingKey:lazy.orange.male.rabbit
【接收者/消费者Ⅰ(对"*.orange.*" 数据感兴趣)】
package topic;

import com.rabbitmq.client.*;

import java.io.IOException;
/**
 * 特定路由键
 */
public class ReceiveLogsTopic1 {
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queueName = channel.queueDeclare().getQueue();

        //指定bindingKey 最重要的一步!
        String bindingKey = "*.orange.*";
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);

        System.out.println("开始接收消息");

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                    AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(
                        " 接收消息: " + message + ":" + envelope.getRoutingKey());
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
==================================================================================
开始接收消息
 接收消息: Animal WorldroutingKey:quick.orange.rabbit
 接收消息: Animal WorldroutingKey:lazy.orange.elephant
 接收消息: Animal WorldroutingKey:quick.orange.fox
【接收者/消费者Ⅱ(对"*.*.rabbit" 和 "lazy.#" 数据感兴趣)】
package topic;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
/**
 * 特定路由键
 */
public class ReceiveLogsTopic2 {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queueName = channel.queueDeclare().getQueue();

        //指定bindingKey
        String bindingKey1 = "*.*.rabbit";
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey1);
        String bindingKey2 = "lazy.#";
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey2);

        System.out.println("开始接收消息");

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                    AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(
                        " 接收消息: " + message + ":" + envelope.getRoutingKey());
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
==================================================================================
开始接收消息
 接收消息: Animal WorldroutingKey:quick.orange.rabbit
 接收消息: Animal WorldroutingKey:lazy.orange.elephant
 接收消息: Animal WorldroutingKey:lazy.brown.fox
 接收消息: Animal WorldroutingKey:lazy.pink.rabbit
 接收消息: Animal WorldroutingKey:lazy.orange.male.rabbit
  • headers:根据发送消息内容中的headers属性来匹配

Spring Boot整合RabbitMQ

  • 实操代码演示

创建两个Springboot项目
spring-boot-rabbitmq-consumer 和 spring-boot-rabbitmq-consumer
在application.properties中设置 如果是本机用户名和密码都是guest
spring.rabbitmq.username=guest

项目:spring-boot-rabbitmq-producer [消息发送者/生产者]
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>spring-boot-rabbitmq-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-rabbitmq-consumer</name>
    <description>spring-boot-rabbitmq-consumer</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</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>
</project>
application.properties
spring.application.name=spring-boot-rabbitmq-producer
server.port=8080
spring.rabbitmq.addresses=127.0.0.1:5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
com/imooc/springbootrabbitmqproducer/MsgSender.java
package com.imooc.springbootrabbitmqproducer;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 发送消息
 */
//@Component让springboot发现
@Component
public class MsgSender {
    @Autowired
    private AmqpTemplate rabbitmqTemplate;
    public void send1(){
        String message = "This is message 1, routing key is dog.red";
        System.out.println("发送了:" + message);
        //指定交换机的名字  routing key is dog.red  消息内容
        this.rabbitmqTemplate.convertAndSend("bootExchange","dog.red",message);
    }
    public void send2(){
        String message = "This is message 2, routing key is dog.black";
        System.out.println("发送了:" + message);
        //指定交换机的名字  routing key is dog.red  消息内容
        this.rabbitmqTemplate.convertAndSend("bootExchange","dog.black",message);
    }
}
com/imooc/springbootrabbitmqproducer/TopicRabbitConfig.java
package com.imooc.springbootrabbitmqproducer;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * rabbitmq配置类
 */
@Configuration
public class TopicRabbitConfig {
    @Bean
    public Queue queue1(){
        return new Queue("queue1");
    }
    @Bean
    public Queue queue2(){
        return new Queue("queue2");
    }
    //用一个Bean定义交换机
    @Bean
    TopicExchange exchange(){
        return new TopicExchange("bootExchange");
    }

    //有了Q1 Q2后去指定一个topic交换机
    //绑定到交换机上
    @Bean
    Binding bingdingExchangeMessage1(Queue queue1, TopicExchange exchange){
        //to绑定到哪个交换机 with指定routingKey
        return BindingBuilder.bind(queue1()).to(exchange).with("dog.red");
    }
    @Bean
    Binding bingdingExchangeMessage2(Queue queue2, TopicExchange exchange){
        //to绑定到哪个交换机 with指定routingKey
        return BindingBuilder.bind(queue2()).to(exchange).with("dog.#");
    }
}
用test的测试类
com/imooc/springbootrabbitmqproducer/SpringBootRabbitmqProducerApplicationTests.java
package com.imooc.springbootrabbitmqproducer;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBootRabbitmqProducerApplicationTests {

    @Autowired
    MsgSender msgSender;

    @Test
    public void send1(){
        msgSender.send1();
    }
    @Test
    public void send2(){
        msgSender.send2();
    }
}
项目:spring-boot-rabbitmq-consumer [消息接收者/消费者]
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>spring-boot-rabbitmq-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-rabbitmq-consumer</name>
    <description>spring-boot-rabbitmq-consumer</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</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>
</project>
application.properties
spring.application.name=spring-boot-rabbitmq-consumer
server.port=8081
spring.rabbitmq.addresses=127.0.0.1:15672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
Receiver1.java
package com.imooc.springbootrabbitmqconsumer;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费者1
 */
@Component
@RabbitListener(queues = "queue1")
public class Receiver1 {
    @RabbitHandler //收到这个消息后怎么处理
    public void process(String message){
        System.out.println("Receiver1:" + message);
    }
}
Receiver2.java
package com.imooc.springbootrabbitmqconsumer;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费者2
 */
@Component
@RabbitListener(queues = "queue2")
public class Receiver2 {
    @RabbitHandler //收到这个消息后怎么处理
    public void process(String message){
        System.out.println("Receiver2:" + message);
    }
}

先启动消费者 然后再启动生产者test中的测试
就可以发现
send1() → 发送了:This is message 1, routing key is dog.red
send2() → 发送了:This is message 2, routing key is dog.black

阅读全文

Java面试

2024/3/28

Java面试 —— 准备

給职场新人的建议

  • 不要有为老板工作的心态
  • 永远不要敷衍工作
  • 事情做完马上汇报
  • 管理好自己的情绪,做一个可爱的人
  • 接受任务有确认机制
  • 注意工作汇报方式
  • 注重细节
  • 提建议要有数据支撑
  • 給主管选择题 而不是判断题
  • 要提升自己的价值 [人脉网]
  • 认真对待工作汇报3

介绍面试环节中常见问题

自我介绍
  • 我是誰
  • 我做过什么
  • 我的优势在哪里

张总你好,我叫潘春尧,今年23岁,来自河北唐山人,来应聘贵公司的Java软件工程师职位,26年毕业于南昌工学院软件工程专业,从事Java开发有2年的工作时间了,我对编程有着浓厚的兴趣,擅长基于HTML5的互联网应用开发,擅长使用Mysql数据库对大数据量方面的调优有经验,同时具备完整的项目开发能力,具备一定的分布式架构经验,之前我做过两个项目,都是独立完成的,第一个是XX 在2024年开发,这是一个电商网站,已经运用了一年的时间,我负责底层的架构搭建与前后端开发,技术方面采用了XXXX。架构方面底层使用了XXX,我负责的模块设计XXX,这些都是由我完整开发的,而第二个项目是由我开发的xxx,这是一个xxx平台,基于阿里云搭建,技术方面与上一个相似,工作原因产品需要增加XXX,我负责的网站模块主要负责xxx,这也是由我负责开发的,以上是我的个人介绍,有不足的请您多保函。

现场面对面 [要面试官知道我有闪光的价值]
  • 容貌要求 [注意形象(刮胡子,短发,指甲)]
  • 着装要求 [品牌+面料+皮鞋(商务休闲)]
  • 面试礼仪 [提前15分钟到达面试场地填写表格、敬语(您好,感谢,再见)、善意的微笑] [和前台了解面试官的称呼和职位 主动握手 张总… ] [跟着面试官进去后 注意关门 进去前注意敲门] [沟通时眼神不要游离] [提交简历调转180°双手呈递] [不要抖腿翘腿] [手放腿上 十指交叉放桌子上 不许靠着椅子靠背]
  • 一对多面试 [一男一女 男: 技术主管[考察技术能力] 女: HR[根据动作分析本人能力]] [面试紧张的时候要降低语速]
面试必知必会
  • 谈谈你的职业规划 [对未来职业有清晰的规划]

我从小对软件技术有着很浓厚的兴趣,在未来我五年内我希望从事于与技术相关的岗位,我给自己定位了一个小目标,在五年的时间内我希望拿下系统架构师的认证,同时在分布式、微服务的领域有自己独道的见识。希望在未来的一段时间里公司能给我提供成长的机会。

  • 与同时遇到正面冲突,你会怎么办

[不诋毁任何人 不传达任何负面情绪 以大局为重]
我很少与别人产生正面的冲突,面对别人对我个人的职责批评,我通常先会反思我是不是真的做错了,如果是我会虚心接受,尽快改正。如果是别人对我无端的诽谤,在公共场合也会保持沉默,我会在私下跟他心平气和的沟通看看之间是不是有什么误会。如果是在工作中就某些问题产生了分歧。在会议时直接打开天窗说亮话,阐述自己的观点,这种是典型的对事不对人,这也是点到为止,如果双方意见僵持不下,这可能需要项目经理在中间做出定夺,他看到的问题可能比我们的更加全面而且更有建设性,总之我处事的原则是对事不对人,做事必须认真,做人还是要谦虚一些

  • 你生活中最重要的是什么

[考察你对待工作 生活 家庭是什么态度] [强调以事业为重]
我一个人来到XX打拼,目前是单身,对于我现在而言,事业就是我的全部,我会珍惜每一天,让自己尽快的成长起来

  • 这份工作你能遇见哪些问题

[不能太过实在 避重就轻 强调自己的特性]
我对咱们公司的业务不太了解,经验很少,进入公司以后我会听取领导的指示和要求,尽快的熟悉公司的业务和技能,并且制定一份近期的工作计划并报告給领导批准,最后按照计划展开工作,其实针对这个问题,明眼人都看得出来,都说了一堆套话,废话,但是对于面试官来说,你正面的回答了这些问题。而这些问题肯定是现实中存在的,这样既无伤大雅又巧妙的表达了自己对这份工作的期望

  • 你希望与什么样的同事或上级公事

[考察情商]
作为刚步入社会的新人,我应该要求自己多熟悉工作环境适应环境,而不是对环境提出要求,对于我的工作只要能发挥我的专长就可以了,如果工作中有做的不对的地方还请领导多批评指正

  • 你将如何开展工作

[面试通过几率已经很大了]
每天我会提高工作效率,完成公司交付的工作任务,任务未完成绝不下班,根据领导的指示和工作安排制定好工作计划,提前预备并按照计划完成,多请示领导及时汇报,如果有遇到不明白的地方要向前辈们请教,抓住缝隙时间多学习多提升,尽快的提高自己的专业能力与业务水平

  • 你有想过创业吗

[面试官喜欢有冲劲的小伙子 给出明确的时间规划 不要说我是来公司学习的]
我当然有想过创业,但我觉得不是现在,我计划在30岁前后选择创业,软件行业需要大量的技术积累才能有竞争力,同时呢创业对我来说也需要很多的资金,我是一名刚走出校园的新人,在这两方面都不成熟,所以在未来的5年时间内,我会努力的工作,在经验和资金方面做好准备,同时之所以来贵公司,是因为贵公司在XXX行业是一个知名的企业,我想在为公司创造价值的同时,也能进一步的掌握咱们行业最新的动向,希望您能给我一次证明自己的机会,我肯定不会让公司失望的。

  • 你还应聘了哪些公司

[千万不能说海投 也不能说hr打了电话才来 最好不要跨行业]
做一些准备了

  • 为什么你的学历是专科

[很难回答 送命题 学历非常重要的 话术把劣势转换优势]
实话实说上高中的时候我并不是一个好学生,在那时候爱玩电脑,确实也耽误了不少学业。但我认为学历的高低并不代表能力的好坏,在我上了专科以后我更加意识到因为学历比别人落后,自己就更需要努力了,从小对编程感兴趣,上了大学后就更有时间朝着自己喜欢的领域努力。在大学几年的时间里面我没有醉生梦死,自己呢也连续学习了XXX技术内容,我知道有朝一日,这些知识一定能给我带来用处,我一定能成为所向往的软件工程师,我能用实际行动来证明专科不比本科差

  • 谈谈你对加班的看法

[测试你是否能为公司奉献 要强调上班提高效率 强调加班并不是我的工作效率低下]
上班时间我会提高效率,做到当日事当日毕,但是如果当天的事没有按时完成,我会义不容辞的加班,现在我是单身,没有家庭的负担,可以全身心的工作

  • 你的薪资的要求

[提前有明确主张 自己能赚多少钱 给出hr谈薪资的余地期待8k 可以报价8.5k 千万不要说区间6-8k]
我期望的薪资是8.5k,每一个公司都有自己的薪资结构,咱们公司是xxx行业中是非常有实力的企业,相对于薪资来说我更看重技术和行业本身,如果您觉得薪资水平超出了公司的预期的话,这个咱们可以谈

  • 谈谈你对跳槽的看法

[千万不要说负面话语 也不能说工资太低、福利太差、没有晋升空间]
我这个人稳定性很好,本身反对频繁跳槽,但是有一种情况我会选择跳槽,就是公司的发展对于我的事业规划不符,我之前在进入xxx公司的时候,公司主要从事金融行业的项目开发,后来该行业金融不理想改作了电信行业了,当我的长期规划是想成为长期的金融软件工程师与架构师,如果遇到这种情况我会好不容易的选择离开。

Java面试——Java基础面试技巧

JDK、JRE、JVM有什么区别

  • JDK:Java Development Kit 针对Java程序员的产品
  • JRE:Java Runtime Environment是运行Java的环境集合
  • JVM:Java虚拟机用于运行Java字节码文件[class],跨平台的核心。通过JVM隐藏了操作系统最底层的API,对于我们java工程师只需要面对jvm层面进行开发,再转移到其他就可以了
为什么可以跨平台?

因为JVM可以跨平台,JVM只认识字节码,能够解释到系统的API调用,对于不同的系统有不同的JVM实现。Windows和Linux有不同的JVM实现,但是编成字节码后都是一样的。

常用数字类型的区别

名次 取值范围 存储空间
字节(byte) -$2^7$ ~ $2^7$-1
-128 ~ 127
1个字节
短整数(short) -$2^{15}$ ~ $2^{15}$-1
-32768 ~ 32767
2个字节
整数(int) -$2^{31}$ ~ $2^{31}$-1
-2147483648 ~ 2147483647
4个字节
长整数(long) -$2^{63}$ ~ $2^{63}$-1 8个字节
单精度(float) $2^{-149}$ ~ $2^{128}$-1 4个字节
双精度(double) $2^{-1074}$ ~ $2^{1024}$-1 8个字节

Float在JVM的表达方式及使用陷阱

float d1 = 423432423f;
float d2 = d1 + 1;
if(d1 == d2){
    System.out.println("d1==d2");
}else{
    System.out.println("d1!=d2");
}

答案:d1 == d2
对于单精度浮点型float d1在内存中是采用科学计数法表示
表达为:4.2343242E7 保留小数点后七位 [如果更高精度的用double]
d1是八位数
无论是d1还是d2不考虑最后一位
d2用科学计数法表示同样为4.2343242E7
因此 d1 == d2

银行业务数值精确计算要用BigDecimal类进行加减乘除的计算

随机数的使用

编程题:随机生成30~100之间的整数
==========================================================
package com.example.demo12;

import java.util.Random;

public class RandomSample {
    public Integer randomInt1(){
        int min = 30;
        int max = 100;
        //生成正整数范围0-70的随机数 => 30~100
        int result = new Random().nextInt(max - min) + min;
        return  result;
    }
    public Integer randomInt2(){
        int min = 30;
        int max = 100;
        int result = (int)(Math.random() * (max-min)) + min;
        return result;
    }

    public static void main(String[] args) {
        System.out.println(new RandomSample().randomInt1());
        System.out.println(new RandomSample().randomInt2());
    }
}
编程题:列出1-1000的质数[大于1的情况下 只能被1和自身整除的数]
==========================================================
package com.example.demo12;

public class PrimeNumber {
    public static void main(String[] args) {
        for (int i = 2; i <= 1000; i++){
            boolean flag = true;
            for (int j = 2; j < i; j++){
                if(i % j == 0){
                    flag = false;
                    break;
                }
            }
            if (flag){
                System.out.println(i);
            }
        }
    }
}

面向对象的三大特征是什么

  • 封装
    对同一类事务的特征和功能包装到一起,只对外暴露需要调用接口而已
    [我想让你看到的你能看到 不想让你看到的不能看到]。作用:对接口进行实现的过程中每一个接口的实现类对接口进行了实现,但是在调用的时候通常是面对接口的,对于使用者来说只需要知道接口对应的哪些方法做什么用的就可以了,对内部的什么是不需要理解的。接口是体现封装的常见方法
    封装的好处

    1. 实现专业的分工
    2. 减少代码耦合
    3. 可以自由修改类的内部结构
  • 继承
    是Java中面向对象最显著的特征,继承是从已有的类中派生出新的类,新的类能够吸收原有属性的属性和行为并扩展新的类,Java中类是不支持多继承的,接口可以,一个接口可以继承多个其他接口 父类是子类的抽象化,子类是父类的具体化。
    接口与抽象类有那些异同?

    相同点 不同点
    都是上层的抽象 抽象类可包含方法的实现
    接口则只能包含方法的声明
    不能被实例化 [不可new] 继承类只能继承一个抽象类
    实现类可以实现多个接口
    都可以包含抽象发发 抽象级别:接口>抽象类>实现类

    作用不同:接口用于约束程序行为。继承则用于代码复用
    注意:JDK8以上版本,接口可以有default方法,包含方法实现

  • 多态
    最重要的特性 前面是为多态服务的
    多态是三大特性中最重要的操作
    多态是同一个行为具有多个不同表现形式或形态的能力
    多态是同一个接口,使用不同的实例而执行不同操作

静态和实例变量(方法)的区别

  • 语法区别:静态变量前要加static关键字,实例则不用
  • 隶属区别:实例变量属于某个对象的属性,而静态属于类
  • 运行区别静态变量在JVM加载类字节码创建,实例变量在实例化对象时创建

类的执行顺序

package com.example.text;
//请写出程序输出结果
//1. 静态优先
//2. 父类优先
//3. 非静态块优先于构造函数
public class ExecutionSequence {
    public static void main(String[] args) {
        new GeneralClass();
    }
}
class ParentClass{
    static {
        System.out.println("①我是父类静态块");
    }

    {
        System.out.println("②我是父类非静态块");
    }
    public ParentClass(){
        System.out.println("③我是父类构造函数");
    }
}

class GeneralClass extends ParentClass{
    static{
        System.out.println("④我是子类静态块");
    }

    {
        System.out.println("⑤我是子类非静态块");
    }

    public GeneralClass(){
        System.out.println("⑥我是子类构造函数");
    }
}
/*
①我是父类静态块
④我是子类静态块
②我是父类非静态块
③我是父类构造函数
⑤我是子类非静态块
⑥我是子类构造函数
*/

请说明java的异常体系

Error → Throwable
RuntimeException → Exception → Throwable
运行时产生的异常不要求对其强制try catch…

Error和Exception的区别和联系
Exception Error
可以是可被控制或不可控制的 总是不可控制的
表示一个由程序员导致的错误 经常用来用于表示系统错误或低层资源的错误
表示在应用程序级被处理 如果可能的话,应该在系统级被捕捉

String与字符串常量池 [比较内存地址]

String s1 = "abc";
String s2 = "abc";
String s3 = "abc"+"def";
String s4 = "abcdef";
String s5 = s2 + "def";
String s6 = new String("abc");
System.ouut.println(s1==s2); //true
System.ouut.println(s3==s4); //true 相等的都保存到常量池
System.ouut.println(s4==s5); //false 
//s5中的s2属于引用类型 在java编译期间无法确定其类型 只有在运行时才能确定具体的值 会产生一个新的内存地址给s5 所以不相等
System.ouut.println(s4.equals(s5));//true equals比较的是内容
System.ouut.println(s1==s6); //false
//s1是在常量池的常量 new String("abc");创建的东西不在常量池 存储的地方是完全不同的

String、StringBuilder、StringBuffer的区别

String StringBuffer StringBuilder
执行速度 最差 其次 最高
线程安全 线程安全 线程安全 线程不安全
使用场景 少量字符串操作 多线程环境下的大量操作 单线程环境下的大量操作

List与Set的区别

List Set
允许重复
是否允许null
是否有序
常用类 ArrayList
LinkedList
HashSet
LinkedHashSet
TreeSet

ArrayList与LinkedList的区别

ArrayList LinkedList
存储结构 基于动态数组 基于链表
遍历方式 连续读取 基于指针
使用场景 大数据量读取 频繁新增、插入[写入类型]

HashSet和TreeSet的区别

HashSet TreeSet
排序方式 不能保证顺序 按预置规则顺序
底层存储 基于HashMap 基于TreeMap
底层实现 基于Hash表实现 基于二叉树实现

List排序的编码实现

编程题:List排序
Employee.java
package com.example.text;

public class Employee {
    private String ename;
    private Integer age;
    private Float salary;

    @Override
    public String toString() {
        return "Employee{" +
                "ename='" + ename + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                '}';
    }

    public Employee() {}
    public Employee(String ename, Integer age, Float salary) {
        this.ename = ename;
        this.age = age;
        this.salary = salary;
    }
    public String getEname() { return ename;}
    public void setEname(String ename) {this.ename = ename;}
    public Integer getAge() { return age;}
    public void setAge(Integer age) {this.age = age;}
    public Float getSalary() {return salary;}
    public void setSalary(Float salary) {this.salary = salary;}
}
ListSorter.java
package com.example.text;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class ListSorter {
    public static void main(String[] args) {
        List<Employee> emps = new ArrayList<>();
        emps.add(new Employee("张三", 33, 1800f));
        emps.add(new Employee("李四", 55, 3800f));
        emps.add(new Employee("王五", 40, 2300f));
        Collections.sort(emps, new Comparator<Employee>() {
            @Override
            public int compare(Employee o1, Employee o2) {
                //以薪资排序 如果是以年龄的话o1.getAge() - o2.getAge();
                return (int) (o1.getSalary() - o2.getSalary());
                //返回正数 第一个大于第二个
            }
        });
        System.out.println(emps);
    }
}

TreeSet排序的编码实现

编程题:TreeSet排序
Employee.java 同上
[自定义排序]
SetSorter.java 
package com.example.text.TreeSet;

import java.util.*;

//自定义排序
public class SetSorter {
    public static void main(String[] args) {
        TreeSet<Employee> emps = new TreeSet<Employee>(new Comparator<Employee>() {
            @Override
            public int compare(Employee o1, Employee o2) {
                return (int)(o2.getSalary() - o1.getSalary());
            }
        });
        emps.add(new Employee("张三", 33, 1800f));
        emps.add(new Employee("李四", 55, 3800f));
        emps.add(new Employee("王五", 40, 2300f));
    }
}
[自然排序]
package com.example.text;

public class Employee implements Comparable<Employee>{
    private String ename;
    private Integer age;
    private Float salary;

    @Override
    public String toString() {
        return "Employee{" +
                "ename='" + ename + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                '}';
    }
    @Override
    public int compareTo(Employee o){
 //如果返回-1 Employee对象返回到红黑树的左边 采用降序排列
 //如果返回1
        return this.getAge().compareTo(o.getAge());
//      return o.getAge().compareTo(this.getAge()); 输出降序
//如果前面比后面大 返回1 反之返回-1 如果两者相等返回0
    }
    public Employee() {}
    public Employee(String ename, Integer age, Float salary) {
        this.ename = ename;
        this.age = age;
        this.salary = salary;
    }
    public String getEname() { return ename;}
    public void setEname(String ename) {this.ename = ename;}
    public Integer getAge() { return age;}
    public void setAge(Integer age) {this.age = age;}
    public Float getSalary() {return salary;}
    public void setSalary(Float salary) {this.salary = salary;}
}
=============================================================
SetSorter.java 
package com.example.text.TreeSet;

import java.util.*;

//自定义排序
public class SetSorter {
    public static void main(String[] args) {
        TreeSet<Employee> emps = new TreeSet<Employee>
        emps.add(new Employee("张三", 33, 1800f));
        emps.add(new Employee("李四", 55, 3800f));
        emps.add(new Employee("王五", 40, 2300f));
        System.out.println(emps);
    }
}

hashCode与equals的联系与区别

Object类hashCode()和equals()的区别
  • equals()方法用来判断两个对象是否”相同”

  • hashCode()返回一个int,代表”将该对象的内部地址” [如果不相等肯定不是同一个对象] [小概率情况下不同的对象也产生相同的hashCode(), 之后再用equals()去比较]

    序号 描述
    1 两个对象如果equals()成立, hashcode()一定成立
    2 如果equals()不成立, hashcode()可能成立
    3 如果hashcode()成立, equals()不一定成立
    4 hashcode()不相等, equals()一定不成立

Java IO中有集中类型的流

输入流 输出流
字节流 InputStream[父类]
FileInputStream
BufferedInputStream[缓存]
OutputStream[父类]
FileOutputStream
BufferedOutputStream[缓存]
字符流 Reader
FileReader
InputStreamReader
BufferedReader
Writer
FileWeiter
OutputStreamWriter
BufferedWeiter

★ 利用IO实现文件复制 ★ [笔试]

编程题:复制文件到指定文件夹
package com.example.text.FileCopy;

import java.io.*;

public class FileCopy {
    public static void main(String[] args) {
        //1. 利用JavaIo
        File source = new File("D:/application.rar");
        File target = new File("D:/target/application.rar");
        InputStream input = null;
        OutputStream output = null;
        try {
            input = new FileInputStream(source);
            output = new FileOutputStream(target);
            byte[] buf = new byte[1024];
            int byteRead;
            while((byteRead = input.read(buf)) != -1){
                output.write(buf,0,byteRead);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                input.close();
                output.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        //2. 利用FileChannel实现文件复制
        //3. Commons Io组件实现文件复制
        //FileUtils.copyFile(Source,Target);
    }
}

请介绍JVM的内存组成

[共享区:供所有对象共享 所有线程都访问到的地方]
:垃圾回收销毁处理区
方法区:虚拟机加载的类的信息、常量、静态变量等信息

[私有区:所有对象都是私有的]
程序计数器PC: 当前线程所执行的字节码指令的行号指示器[分值、跳转、循环、异常处理、线程恢复] 每个线程都有单独的程序计数器 [私有的]
虚拟机栈VM Stack:为Java的方法服务 当线程创建的时候虚拟机栈会为线程分配一个虚拟机栈,在线程执行的时候创建的每个方法都会创建一个栈针,用于存放局部变量、操作方法等 [入栈+出栈]
本地方法栈:类似于虚拟机栈,本地方法栈是执行本地方法使用

请简述Java的垃圾回收(GC)

  • GC(Garbage Collection)用于回收不再使用的内存
  • GC负责3项任务:分配内存、确保引用、回收内存
  • GC回收的依据某对象没有任何引用、则可以被回收
垃圾回收(GC)算法
  • 引用计数算法 [计数器 引用到这个对象的时候+1 被释放或断开计数器-1 当计数器变成0就可以被垃圾回收]
    若两个对象相互引用就会产生循环计数器 这个对于引用计数算法是无效的
  • 跟踪回收算法 [利用JVM维护的对象引用图 每个对象彼此依赖和引用 从根节点标记对象引用图和可以被标记的对象 遍历结束以后未被标记的对象是不能够使用的对象可以被回收]
  • 压缩回收算法 [将JVM堆中活动的对象放到一个集中的区域中 另外一大块留出一个活动的区域 对堆中的碎片进行集中的处理]
  • 复制回收算法 [把堆分成两个相同大小的区域 只有其中的一个区域被使用 直到这个区域被消耗完 垃圾回收器就会中断这个程序的运行 以遍历的方式把活动的对象移动到另一个区域 移动的过程中对象是紧密挨在一起的 可以消除其中的碎片 【在复制的同时也对对象进行重新复制和安排 一次性解决了内存碎片的问题】【需要两倍大小的内存空间 降低了效率】]
  • 按代回收算法 [把堆分成多个子堆,每个堆命名成一代,算法优先收集年幼年轻的对象,在这其中对象仍然存活的就把它移动到高级堆中 【减少扫描次数和范围 提高回收效益】]

Java的内存泄露的场景

请列举Java中内存泄漏的场景

一个不需要使用的对象还在内存中占用并使用其内存空间,忘记释放就会存在内存泄露

  • 静态集合类 static描述对象 存储在方法区 垃圾回收器不会对其扫码回收
  • 各种连接 JVM一直可达不会被回收
  • 监听器 全局存在的
  • 不合理的作用域 能用在方法中public 不要用在方法外private
  • 没有把对象及时设置为null【极少发生】

请实现对象的浅复制与深复制

  • 潜复制:只对对象及变量值进行复制,引用对象地址不变
  • 深复制[序列化方式]:不仅对象及变量值进行复制,引用对象也进行复制[彼此互不干扰]
com/example/text/clone/Dancer.java
package com.example.text.clone;

import java.io.*;

public class Dancer implements Cloneable,Serializable{
    private String name;
    private Dancer partner;

    public Dancer() {
    }

    public Dancer(String name, Dancer partner) {
        this.name = name;
        this.partner = partner;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dancer getPartner() {
        return partner;
    }

    public void setPartner(Dancer partner) {
        this.partner = partner;
    }
    public Dancer deepClone() throws IOException, ClassNotFoundException {
        //序列化, 将内存中的对象序列化为字节数组
        //序列化要注意加Serializable接口
        ByteArrayOutputStream bos = new ByteArrayOutputStream();//字节数组
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);

        //反序列化, 将字节数组转回到对象, 同时完成深复制的任务
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis); //字节数组还原成对象
        return (Dancer) ois.readObject();
    }
    //初始化一下
    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
        Dancer d1 = new Dancer();
        d1.setName("小明");
        Dancer d2 = new Dancer();
        d2.setName("小红");
        d1.setPartner(d2);
        //hashCode()是内存地址转化后的一个整数
        System.out.println("Partner:" + d2.hashCode());
        //浅复制
        Dancer shallow =  (Dancer)d1.clone();
        System.out.println("浅复制:" + shallow.getPartner().hashCode());
        //深复制
        Dancer deep = (Dancer) d1.deepClone();
        System.out.println("深复制:" + deep.getPartner().hashCode());
    }
}

Java面试——Web基础与数据处理

请说明Servlet执行流程

问:Servlet什么时候被实例化的呢?

Servlet默认情况下是第一次访问的时候实例化
也可以通过web.xml配置load-on-startup,使其在服务启动的时候实例化

问:Servlet在并发条件下如何处理的?

基于单例多线程技术提供web服务
使用servlet时不允许使用存在状态改变的变量

Servlet生命周期

  • 启动时装载 - web.xml
  • 创建 - 构造函数
  • 初始化 - init() [专门的初始化资源]
  • 提供服务 - service() [对于发来的请求无论是get post 一律使用service进行处理] [细化分出doget() dopost()]
  • 停止时销毁 - destory() [销毁资源]
com/example/text/servlet/SampleServlet.java
package com.example.text.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class SampleServlet extends HttpServlet {
    public SampleServlet(){
        System.out.println("正在创建Servlet");
    }

    @Override
    public void init() throws ServletException {
        System.out.println("正在初始化Servlet");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println(this.hashCode() + ": Service()正在提供服务");
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().print(this.hashCode() + ": Service()正在提供服务");
    }

    @Override
    public void destroy(){
        System.out.println("正在销毁Servlet");
    }
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>SampleServlet</servlet-name>
        <servlet-class>com.example.text.servlet.SampleServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>SampleServlet</servlet-name>
        <url-pattern>/sample</url-pattern>
    </servlet-mapping>
</web-app>

=================================================
http://localhost:8080/sample

正在创建Servlet 【启动时】
正在初始化Servlet
1025518765: Service()正在提供服务
正在销毁Servlet 【停止时】

请求与响应的结构

  • HTTP请求包含三部分:请求行、请求头、请求体

HTTP请求三部分

  • HTTP响应包含三部分:响应行、响应头、响应体

HTTP响应三部分

请求转发与响应重定向的区别

请求转发 [一次请求]
  • 请求转发是服务器跳转,只会产生一次请求
  • 请求转发语句是:request.getRequestDispatcher().forward()

请求转发

网页请求Tomcat的Servlet1,Servlet1请求Servlet2,Servlet2响应给网页,网页收到响应

响应重定向 [两次请求]
  • 重定向则是浏览器端跳转,会产生两次请求
  • 响应重定向语句是:response.sendRedirect()

响应重定向

网页$请求_1$Tomcat的Servlet1,Servlet1直接$响应_1$给网页,网页收到$响应_1$[让网页创建一个新的$请求_2$]
网页新创了一个$请求_2$Tomcat的Servlet2,Servlet2只对请求进行处理返回给$响应_2$网页

请阐述Session的原理

简述Session的原理

浏览器第一次向Tomcat发送请求,Tomcat收到请求自动在tomcat运行内存中生成一个存储空间并赋予一个SessionId:7U809J唯一编号,若使用了session.setAttribute(name,小潘),会在那个运行内存中存放一个张三的内存属性[name:张三] 当程序处理完后返回给浏览器其中包含了一个特殊信息SessionId:7U809J,浏览器收到响应后把SessionId:7U809J保存到浏览器的Cookie中SessionId:7U809J[只要当前浏览器没有被关闭的话 Cookie会一直存在 会在发送请求的时候一并发送给服务器],当第二次发送请求的时候浏览器会把SessionId:7U809J第二次发送给Tomcat,若使用了session.getAttribute(name) = 小潘,得到的结果就出现了小潘 返回浏览器响应

JSP九大内置对象是什么

内置对象 描述
request 请求对象 - HttpServletRequest
response 响应对象 - HttpServletResponse
session 用户会话 - HttpSession
application [全局实例] 应用全局对象 - ServletContext
out 输出对象 - PrintWriter
page 当前页面对象 - this [Object实例]
pageContext 页面上下文对象 - PageContext
config [全局参数] 应用配置对象 - ServletConfig
exception 应用异常对象 - Throwable

Statement 和 PreparedStatement的区别

  • PreparedStatement是预编译的SQL语句,效率高于Statement
  • PreparedStatement支持 ‘?’ 操作符,相对于Statement更加灵活
  • PreparedStatement可以防止SQL注入,安全性高于Statement
package com.example.text.jdbc;

import java.sql.*;

public class StatementSample {
    //使用Statement sql语句解释N多次
    //SQL注入攻击:  ' or 1=1 or '
    public void findByEname1(String ename) {
        String driverName = "com.mysql.jdbc.Driver";
        String URL = "jdbc:mysql://127.0.0.1:3306/scott";
        String username = "root";
        String password = "root";
        Connection conn = null;
        try {
            Class.forName(driverName);
            conn = DriverManager.getConnection(URL, username, password);
            Statement ps = conn.createStatement();
            //使用Statement
            String sql = "SELECT * FROM emp where ename = '" + ename + "' ";
            System.out.println(sql);
            ResultSet rs = ps.executeQuery(sql);

            while (rs.next()) {
                System.out.println(rs.getString("ename") + "," +
                        rs.getString("job") + "," +
                        rs.getFloat("sal"));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        //5.关闭连接
        finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
package com.example.text.jdbc;

import java.sql.*;

public class StatementSample {

    //使用预编译PreparedStatement sql语句解释一次
    public void findByEname2(String ename) {
        String driverName = "com.mysql.jdbc.Driver";
        String URL = "jdbc:mysql://127.0.0.1:3306/scott";

        String username = "root";
        String password = "root";
        Connection conn = null;
        try {
            Class.forName(driverName);
            conn = DriverManager.getConnection(URL, username, password);

            //使用预编译PreparedStatement
            String sql = "SELECT * FROM emp where ename = ?";
            System.out.println(sql);
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, ename);
            ResultSet rs = ps.executeQuery();

            while (rs.next()) {
                System.out.println(rs.getString("ename") + "," +
                        rs.getString("job") + "," +
                        rs.getFloat("sal"));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        //5.关闭连接
        finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        StatementSample sample = new StatementSample();
        sample.findByEname1("SMITH' or 1=1 or '"); //SQL注入攻击
        sample.findByEname2("SMITH");
    }
}

请说明JDBC的使用步骤

  • 加载JDBC驱动
  • 创建数据库连接(Connection)
  • 创建命令(Statement)
  • 处理结果(ResultSet)
  • 关闭连接
package com.example.text.jdbc;

import java.sql.*;

public class MysqlJDBC {
    public static void main(String[] args) {
        //连接需要提前下载好包   mysql-connector-java-5.1.47.jar
        String driverName = "com.mysql.jdbc.Driver";
        String URL = "jdbc:mysql://127.0.0.1:3306/scott";
        String sql = "SELECT * FROM emp";
        String username = "root";
        String password = "root";
        Connection conn = null;
        try {
            //1.加载JDBC驱动
            Class.forName(driverName);
            //2.建立连接
            conn = DriverManager.getConnection(URL, username, password);
            //3.创建命令(Statement)
            Statement ps = conn.createStatement();
            //4.处理结果
            ResultSet rs = ps.executeQuery(sql);
            while (rs.next()) {
                System.out.println(rs.getString("ename") + "," +
                        rs.getString("job") + "," +
                        rs.getFloat("sal"));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        //5.关闭连接
        finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

SQL编程训练

scott.sql文件已准备好

★按部门编号升序、工资倒序的排列员工信息
SELECT * FROM emp ORDER BY deptno ASC, sal DESC

★列出deptno=30的部门名称及员工[关联查询]
SELECT dept.dname, emp.* FROM emp,dept
WHERE emp.deptno = dept.deptno
AND dept.deptno = 30

★列出每个部门最高、最低、平均工资 #若再列出人数只需要再SELECT后面加 COUNT(*)
SELECT deptno, max(sal), min(sal), avg(sal)
FROM emp 
GROUP BY deptno  

★列出市场部(SALES)及研发部(RESEARCH)的员工
SELECT * FROM dept d, emp e
WHERE d.deptno = e.deptno 
AND (d.dname = 'SALES' or d.dname = 'RESEARCH')

★列出人数超过3人的部门 #按照部门分组 
# WHERE对原始数据进行筛选[在GROUP BY之前进行的] 
# HAVING对分组后的进行二次筛选[在GROUP BY之后进行的]
SELECT d.dname, count(*)
FROM dept d, emp e
WHERE d.deptno = e.deptno 
GROUP BY d.dname 
HAVING count(*)>3

★计算MILLER年薪比SMITH高多少
SELECT a.a_sal, b.b_sal, a.a_sal - b.b_sal FROM 
(SELECT sal * 12 a_sal FROM emp WHERE ename = 'MILLER') a,
(SELECT sal * 12 b_sal FROM emp WHERE ename = 'SMITH') b

★★列出直接向King汇报的员工
SELECT * FROM emp
WHERE mgr = (SELECT empno FROM emp WHERE ename = 'KING')
#或
SELECT e.* FROM emp e, (SELECT empno FROM emp WHERE ename = 'KING') k
WHERE e.mgr = k.empno

★★列出公司所有员工的工龄,并倒序排列 
# NOW获取当前时间 
# SELECT DATE_FORMAT(date,format)   DATE_FORMAT只对日期形式的数据生效
SELECT * FROM(
    SELECT emp.*, DATE_FORMAT(NOW(),"%Y") - DATE_FORMAT(hiredate,"%Y") yg 
    FROM emp
)d 
ORDER BY d.yg DESC

★★计算管理者与基层员工平均薪资差额
SELECT a_avg - b_avg 
FROM
 (SELECT avg(sal) a_avg 
 FROM emp
 WHERE job = 'MANAGER' OR job = 'PRESIDENT') a,
  (SELECT avg(sal) b_avg
  FROM emp
  WHERE job IN ('CLERK','SALESMAN','ANALYST'))b

Java面试 —— 主流框架

谈谈你对IOC和DI的理解

  • IOC控制反转,设计理念,由第三方来管理与控制对象
  • DI依赖注入,具体实现,由对象容器在运行时动态注入对象

[DI是一种具体的技术实现,是对宏观IOC里面的一种技术上的形式,在Spring中使用了反射+工厂模式来实现DI]

IDAO dao = new UserDao(); //原始版本 原本的容器控制器

运行时Spring动态进行创建 对程序进行有效的解耦 //将控制权交给第三方控制容器  IOC容器
// applicationContext最底层是类工厂模式
IDAO dao = (IDAO)applicationContext.getBean("userDAO"); //ICO容器 动态实例化DAO
 <bean id = "userDAO" class = "com.imooc.dao.UserDAO"/>
 <bean id = "userDAO" class = "com.imooc.dao.UserExtDAO"/> //方便随时更改

Spring中Bean实例化有几种方式

  • 使用类构造器实例化
  • 使用静态工厂方法实例化
  • 使用实例工厂方法实例化
com/example/text/spring/Person.java
package com.example.text.spring;

public class Person {

    private String name;
    private Integer age;


    public Person() {
        System.out.println("Person默认构造函数");
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;

        System.out.println("Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}');
    }
}
com/example/text/spring/PersonStaticFactory.java
package com.example.text.spring;

/**
 * Person静态工厂
 */
public class PersonStaticFactory {
    public static Person createPerson(String name,int age){
        return new Person(name,age);
    }
}
com/example/text/spring/PersonFactory.java
package com.example.text.spring;

/**
 * Person实例工厂 实例化以后才可以调用
 */
public class PersonFactory {
    public Person createPerson(String name,int age){
        return new Person(name,age);
    }
}
C:\Users\Pluminary\Desktop\text\src\main\resources\applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <!--构造函数实例化-->
    <!-- 1.调用无参构造函数 -->
    <bean id="person1" class="com.imooc.spring.instance.Person"></bean>

    <!-- 2.调用有参构造函数 -->
    <bean id="person2" class="com.imooc.spring.instance.Person">
        <constructor-arg name="name" value="枫叶(构造函数)"/>
        <constructor-arg name="age" value="23"/>
    </bean>


    <!-- 通过静态工厂创建对象-->
    <bean id="person3" class="com.imooc.spring.instance.PersonStaticFactory" factory-method="createPerson">
        <constructor-arg name="name" value="蓝天(静态工厂)"/>
        <constructor-arg name="age" value="27"/>
    </bean>

    <!-- 通过实例工厂创建对象-->
<!-- 实例化PersonFactory-->
    <bean id="instanceFactory" class="com.imooc.spring.instance.PersonFactory"></bean>

    <bean id="person4" factory-bean="instanceFactory" factory-method="createPerson">
        <constructor-arg name="name" value="绿地(实例工厂)"/>
        <constructor-arg name="age" value="29"/>
    </bean>

</beans>
com/example/text/Application.java
package com.example.text;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
    }
}
===================================================================
Person默认构造函数
Person{name='枫叶(构造函数)',age=23}
Person{name='蓝天(静态工厂)',age=27}
Person{name='绿地(实例工厂)',age=29}

Bean注入属性有哪几种方式

  • setter注入
  • 构造器注入
  • 注解注入
  • Java Config注入
setter注入

在UserService使用的时候依赖于UserDao,目前UserDao是空的,要想运行时通过setter方法注入的话,我们需要在injection.xml中
<context:component-scan base-package=”com.imooc.spring”/>




前面的name=”userDAO”对应着UserService.java中的userDAO属性 private UserDAO userDAO
后面的ref=”userDAO”对应着当前文件中的bean id = “userDAO”

com/example/text/injection/UserDAO.java
package com.example.text.injection;

public interface UserDAO {
    public void insert();
}
com/example/text/injection/UserDAOImpl.java
package com.example.text.injection;

import org.springframework.stereotype.Repository;

//@Repository("userDAO") ★注解方式注入
public class UserDAOImpl implements UserDAO{
    public UserDAOImpl(){
        System.out.println(this + "已创建");
    }
    public void insert() {
        System.out.println(this + ":正在调用UserDAOImpl.insert()");
    }
}
com/example/text/injection/UserService.java
package com.example.text.injection;

import org.springframework.stereotype.Service;

import javax.annotation.Resource;

//@Service("userService") ★注解方式注入
//spring需要动态将userDao注入到UserService上
public class UserService {
//  @Resource(name="userDAO")  注解方式注入 可以把原始的bean都注释掉
    private UserDAO userDAO;

    public UserService() {
        System.out.println(this + "已创建");
    }
/* 注解方式这些可以抛弃 并且摒弃了xml的那些bean 只需要增加个扫描的注解方式注入
    //通过构造函数注入
    public UserService(UserDAO userDAO) {
        this.userDAO = userDAO;
        System.out.println(this + ":正在调用构造函数注入,UserService(" + userDAO + ")");
    }

    //通过Setter方法注入
    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
        System.out.println(this + ":正在调用Setter方法注入,setUserDAO(" + userDAO + ")");
    }
*/

    public void createUser(){
        System.out.println(this + ":正在调用UserService.createUser()");
        userDAO.insert();
    }
}
com/example/text/InjectionRunner.java
package com.example.text;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class InjectionRunner {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:injection.xml");
        UserService userService = (UserService)ctx.getBean("userService");
        userService.createUser();
    }
}

==============================================================
UserDAOImpl@531be3c5对象已创建
UserService@5034c75a对象已创建
UserService@5034c75a:正在调用Setter方法注入,setUserDAO
UserService@5034c75a:正在调用UserService.createUser()
UserDAOImpl@531be3c5:正在调用UserDAOImpl.insert()
C:\Users\Pluminary\Desktop\text\src\main\resources\injection.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.imooc.spring"/> //★扫描注解方式注入
    <bean id="userDAO" class="com.imooc.spring.injection.UserDAOImpl"/>
    <bean id="userService" class="com.imooc.spring.injection.UserService">
        <property name="userDAO" ref="userDAO"/> //★这个是setter注入
        <constructor-arg name="userDAO" ref="userDAO"/> //★这个是构造函数注入
    </bean>
</beans>
Java config注入
Appconfig.java 
[配置文件 在原有代码之外的东西 优点:不用破坏原始代码去达到效果 比xml优点在于java可以及时检查]
package com.imooc.spring.injection;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig { //类似创建对象的方法
    @Bean(name="userDAO")
    public UserDAO userDAO(){
        return new UserDAOImpl();
    }

    @Bean(name="userService")
    public UserService userService(){
        UserService userService = new UserService();
        userService.setUserDAO(this.userDAO());
        return userService;
    }
}

Spring常见面试问题

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="userDAO" class="com.imooc.spring.scope.UserDAOImpl"/>
    <bean id="userService" class="com.imooc.spring.scope.UserService" scope="prototype" lazy-init="false">  
//scope是控制对象的数量是单例(singleton)还是多例(prototype)[每一次创建getbean创建对象]
//还有两个不常见的值 request[同一个对象是同一个请求 不同对象ICO容器创建不同对象] 和 
//session[调用多个getbean是同一个对象 不同session是不同对象] [SpringMVC时用]
        //lazy-init="true" 延迟初始化 需要的时候才会加载 若getBean被注释则不会加载
        <property name="userDAO" ref="userDAO"/>
    </bean>
</beans>
ScopeRunner.java
package com.imooc.spring.scope;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ScopeRunner {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:scope.xml");
        UserService userService1 = (UserService)ctx.getBean("userService");
        UserService userService2 = (UserService)ctx.getBean("userService");
    }
}
UserDAO.java   UserDAOImpl.java   UserService.java 与上方相同
两个注解

@Autowired[第三方] 和 @Resource[官方推荐]

@Resource(name=”userDAO”) //设置了则进行精准匹配
private UserDAO userDAO;//没设置则优先将名字userDAO在beanId内查找, 再没有就按照UserDAO类型在IOC容器中查找

AOP有几种通知类型

在不修改原始程序的前提下使用通知来对程序进行扩展
  • 前置通知[Before]
  • 返回通知[After returning]
  • 异常通知[After throwing]
  • 后置通知[After]
  • 环绕通知[Around] (统统实现)
SampleAspect.java
package com.imooc.spring.aop;

import java.util.Date;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

//切面类
public class SampleAspect {
    
    //1.前置通知
    public void doBefore(JoinPoint jp){
        String clzName = jp.getTarget().getClass().getName();
        //getTarget() 获取即将要执行的对象
        String method = jp.getSignature().getName();//即将要执行的方法
        Object args = jp.getArgs();
        System.out.println("【前置通知】"  + clzName + "." + method );
    }
    
    //2.后置通知
    public void doAfter(JoinPoint jp){
        String clzName = jp.getTarget().getClass().getName();
        //getTarget() 获取即将要执行的对象
        String method = jp.getSignature().getName();//即将要执行的方法
        System.out.println("【后置通知】"  + clzName + "." + method );

    }
    
    //3.返回通知
    public void doAfterReturning(JoinPoint jp , Object ret){
        System.out.println("【返回后通知】" + ret);
        
    }
    //4.异常通知
    public void doAfterThrowing(JoinPoint jp , Throwable t){
        System.out.println("【异常通知】" + t.getMessage());
    }

    //5.环绕通知
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
        String clzName = pjp.getTarget().getClass().getName();
        //getTarget() 获取即将要执行的对象
        String method = pjp.getSignature().getName();//即将要执行的方法
        Object args = pjp.getArgs();
        System.out.println("【前置通知】"  + clzName + "." + method );
        Object ret = null;
        try {
            ret = pjp.proceed();//执行目标方法
            System.out.println("【返回后通知】" + ret);
        } catch (Throwable t) {
            // TODO Auto-generated catch block
            System.out.println("【异常通知】" + t.getMessage());
            throw t;
        }finally{
            System.out.println("【后置通知】"  + clzName + "." + method + "()");
        }
        return ret;
    }
    
}
UserService.java
package com.imooc.spring.aop;

import com.imooc.spring.scope.UserDAO;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
//@Transactional(propagation = Propagation.REQUIRED , rollbackFor = Exception.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED , readOnly = true )
public class UserService {

    public String createUser(){
        //打开事务
        System.out.println(this + ":正在调用UserService.createUser()");
        //提交事务
        //catch块中回滚事务 - RuntimeException
        return "success";
    }
}
AOPRunner.java
package com.imooc.spring.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPRunner {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:aop.xml");
        UserService userService1 = (UserService)ctx.getBean("userService");
        userService1.createUser();
    }
}
aop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/context
 http://www.springframework.org/schema/context/spring-context.xsd
 http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/aop
 http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 定义类被Spring IOC容器管理 -->
    <bean name="sampleAspect" class="com.imooc.spring.aop.SampleAspect"></bean>
    <bean name="userService" class="com.imooc.spring.aop.UserService"></bean>
    <!-- 配置Spring AOP -->
    <aop:config>
        <!-- 配置过程中引用切面类  sampleAspect是切面 扩展-->
        <aop:aspect ref="sampleAspect">
            <!-- PointCut(切点) 限制了切面应用的范围 ↓↓aop作用了哪些方法上↓↓-->
            <aop:pointcut id="samplePC" expression="execution(* com.imooc.spring.aop.*Service.create*(..))" />
            <!-- 定义通知 -->
            <!-- 前置通知 method=对应着SampleAspect.java中的doBefore-->
            <aop:before pointcut-ref="samplePC" method="doBefore"/>
            <!-- 后置通知 -->
            <aop:after pointcut-ref="samplePC" method="doAfter"/>
            <!-- 返回后通知,注意:返回后通知需要增加retruning属性
            指向doAfterReturning的名为ret的参数,使用ret参数获取方法的返回值 -->
            <aop:after-returning method="doAfterReturning" pointcut-ref="samplePC" returning="ret"/> 
<!--3.ret是返回通知里面的参数 public void doAfterReturning(JoinPoint jp , Object ret)-->
             <!-- 异常通知 -->
<!--public void doAfterThrowing(JoinPoint jp , Throwable t)中的Throwable的参数是目标方法所抛出的异常  throwing="t"相关噢~ -->
            <aop:after-throwing method="doAfterThrowing" pointcut-ref="samplePC" throwing="t"/>
            -->
            <!-- ★★★★ 环绕通知 ★★★★ -->
            <aop:around method="doAround" pointcut-ref="samplePC"/>
        </aop:aspect>
    </aop:config>
</beans>

请介绍Spring的声明式事务

  • 在执行方法前自动开启的事务
  • 声明式事务式指利用AOP自动提交、回滚数据库事务
  • 声明式事务式规则进入方法打开事务,成功提交,运行时异常回滚
  • @Transactional是声明式事务的注解 放在类上则所有方法执行此事务 放在方法上则单独方法执行此事务
  • propagation = 确定方法是否启动事务[Propagation.REQUIRED]执行的方法自动使用事务
    rollbackFor = Exception.class在什么时机进行回滚 readOnly = true 方法只读
  • 通过程序打开或关闭事务属于编程式事务
UserService.java
package com.imooc.spring.aop;

import com.imooc.spring.scope.UserDAO;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
//@Transactional(propagation = Propagation.REQUIRED , rollbackFor = Exception.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED , readOnly = true )
public class UserService {
    public String createUser(){
        //进入方法 → 打开事务
        System.out.println(this + ":正在调用UserService.createUser()");
        //方法执行成功 → 提交事务
        
        //方法抛出运行时异常 → 回滚事务
        //catch块中回滚事务 - RuntimeException[及其子类会自动回滚]
        return "success";
    }
}

使用SpringMVC实现REST风格

  • REST(表述性状态传递)以URL表示要访问的资源
  • GET/POST/PUT/DELETE对应查询、新增、更新、删除操作[浏览器不支持PUT和DELETE操作]
    [用GET对应查询操作(写)、用POST对应新增、更新、删除操作(写)]
  • REST风格只响应的数据,通常是以JSON形式体现
pom.xml
<?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.imooc</groupId>
    <artifactId>interview-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.54</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>
web/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
interview-springmvc\src\main\resources\applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 配置扫描的包 -->
    <context:component-scan base-package="com.imooc.interview"/>
    <!-- 注册HandlerMapper、HandlerAdapter两个映射类 开启注解模式-->
    <mvc:annotation-driven/>
    <!-- 访问静态资源 -->
    <mvc:default-servlet-handler/>
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/> //所有URL不分层级和格式
            <bean class="com.imooc.interview.rest.MyInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>
Employee.java
package com.imooc.interview.rest;

public class Employee {
    private String name;
    private int age;
    private String department;

    public Employee(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }
}
RestfulController.java
package com.imooc.interview.rest;

import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
@Controller //在IOC容器初始化的情况下自动扫描这个类进行加载并且通知IOC容器 这个是一个MVC中的控制类
//@RestController
public class RestfulController { //{XXX}是路径变量 随时产生变化的 注解@PathVariable("DEPT")
    @GetMapping("/emp/list/{DEPT}") //响应GET请求 前面的是响应URL输入这个的时候响应下面的方法
    @ResponseBody
    public String list(@PathVariable("DEPT") String department) {
        List<Employee> list = new ArrayList<Employee>();
        if (department.equals("RESEARCH")) {
            list.add(new Employee("JAMES", 38, "RESEARCH"));
            list.add(new Employee("ANDY", 23, "RESEARCH"));
            list.add(new Employee("SMITH", 31, "RESEARCH"));
        }
        return JSON.toJSONString(list);//只返回纯粹的JSON数据 只返回页面名称
        //如何只返回数据不跳转页面呢? 用 @RespondBoday 直接将返回的数据输出到客户端
//但是可以简化使用 @RestController 所有的可以直接不用写 @RespondBoday
    }
}//前端使用AJAX技术接收数据

请说明SpringMVC拦截器的作用 [底层就是AOP面向切面编程技术]

  • SpringMVC拦截器用于对控制器方法进行前置、后置处理
  • 拦截器的底层实现技术是AOP(面向切面编程)
  • 拦截器必须实现HandlerInterceptor接口
★★ 上一个代码通用 ★★
interview-springmvc\src\main\java\com\imooc\interview\rest\MyInterceptor.java
package com.imooc.interview.rest;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.logging.Logger;

public class MyInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("进入 preHandle 方法..." + request.getRequestURL().toString());
        return true;
    }
 

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("进入 postHandle 方法..." + request.getRequestURL().toString());
    }
 

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("进入 afterCompletion 方法..." + request.getRequestURL().toString());
    }
}
====================================================================================
查看上面的applicationContext.xml
<bean>
<!-- 访问静态资源 -->
    <mvc:default-servlet-handler/>
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/> //所有URL不分层级和格式
            <bean class="com.imooc.interview.rest.MyInterceptor"/> 
            //对上面这个类进行处理和拦截
        </mvc:interceptor>
    </mvc:interceptors>
</bean>

SpringMVC的执行流程

说明Mybatis的开发过程

案例沿用之前的scott.sql表

pom.xml
<?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.imooc</groupId>
    <artifactId>interview-mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
    </dependencies>
</project>
mybatis.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <!-- 配置数据库连接信息 -->
            <dataSource type="POOLED"> <!--数据库连接池的方式-->
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/scott" />
                <property name="username" value="root" />
                <property name="password" value="root" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/emp.xml"></mapper>
    </mappers>
</configuration>
Employee.java
package com.imooc.interview.mybatis.entity;

import java.io.Serializable;
import java.util.Date;

public class Employee implements Serializable {
    private Integer empno;
    private String ename;
    private String job;
    private Integer mgr;
    private Date hiredate;
    private Float sal;
    private Float comm;
    private Integer deptno;
}Getter + Setter
用于查询的配置文件 emp.xml
<?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.imooc.interview.mybatis.mapper.EmpMapper"> 
    <cache size="512" eviction="FIFO" flushInterval="60000" readOnly="true"/>
    <select id="findAll" resultType="com.imooc.interview.mybatis.entity.Employee" useCache="false">
        select * from emp
    </select>
<!-- 类型是int 把传入的int代入到#{value}中 -->
    <select id="findById" parameterType="int" 
            resultType="com.imooc.interview.mybatis.entity.Employee" useCache="true">
        select * from emp where empno = #{value}
<!--查询数据 根据数据自动创建Employee对象 根据字段名把值一一的设置到属性中-->
<!--记得写完以后再mybatis.xml中注册 通知mybatis 有个<mapper resource="mapper/emp.xml">-->
<!--至此mybatis的配置全部完成 -->
    </select>
</mapper>
MybatisRunner.java
package com.imooc.interview.mybatis.entity;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class MybatisRunner {
    public static void main(String[] args) {
        //mybatis的配置文件 核心配置文件↓
        String resource = "mybatis.xml";
        InputStream is = MybatisRunner.class.getClassLoader().getResourceAsStream(resource);
        //构建sqlSession的工厂(对Mybatis进行解析)  build(is)是初始化
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);
//com.imooc.interview.mybatis.mapper.EmpMapper前面是命名空间
        //.findById对应着select的Id
        String statement = "com.imooc.interview.mybatis.mapper.EmpMapper.findById";
        SqlSession session = sessionFactory.openSession();
//selectOne获取唯一的查询结果  之前定义了要传入整数int
        Employee emp1 = session.selectOne(statement, 7566);
        System.out.println(emp1);
        session.close();
    }
}

Mybatis的缓存机制

把一些不太常变动的数据保存在内存中或高速存储器中,从而降低查询时间提高查询效率
  • Mybatis存在两级缓存

    • 一级缓存与SqlSession会话绑定,默认开启 [巨大的HashMap] [默认开启]

    • 二级缓存是应用全局缓存,所有SqlSession共享 [分布式数据库等]

//一级缓存验证结果
Employee emp1 = session.selectOne(statement, 7566);
Employee emp2 = session.selectOne(statement, 7566);
System.out.println(emp1);
System.out.println(emp2);
======================打印结果========================
com.imooc.interview.mybatis.entity.Employee@6a41eaa2
com.imooc.interview.mybatis.entity.Employee@6a41eaa2
从缓存中将这个7566提出来,所以对象是一样的
随着session的关闭 → session.close(); 一级缓存也随着消失; 输出的对象结果就不相同了
//二级缓存验证结果  让7566全局缓存 默认是不开启的 需要在mapper中开启 → emp.xml
//在缓存中最大容纳512个对象 缓存多余清除策略[FIFO先进先出算法 LRU访问最少的对象清除] 
//flushInterval=>时间间隔定时清理缓存[毫秒数 每60秒]
<cache size="512" eviction="FIFO" flushInterval="60000" readOnly="true"/>
//若对全部查找 则不适合使用缓存 因为每次都变化 这样就可以设置useCache="false"
<select id="findAll" resultType="com.imooc.interview.mybatis.entity.Employee" useCache="false">
//对数据进行新增、修改、删除也会对缓存进行强制更新 上面设置 readOnly="true" 保存在缓存中的数据都是只读的
//二级缓存要对工具类Employee进行接口的实现
public class Employee implements Serializable {...}

Spring、SpringMVC与SpringBoot的区别

  • Spring是所有应用的基础,提供了IOC与AOP特性实现对对象轻松的管理
  • SpringMVC是Spring的子项目用于提供Web支持 替代传统的Servlet [提高兼容性和可维护性]
  • SpringBoot是Spring体系的敏捷开发框架,提高了开发效率 [对Spring整个体系都有良好的支撑]
    • Spring Framework是整个Spring的框架基础
    • Spring Cloud对分布式架构与微服务提供了良好的支持
    • Spring Data简化关系型数据库 非关系型数据库 大数据源
    • Spring Batch提供高效率的批处理任务
    • Spring Security超级牛逼的安全框架 登录验证之类的
    • Spring Amqp消息队列进行支持 统一的接口进行适配
    • Spring Mobile手机开发特性

SpringBoot面试题

  • Spring Initializr是创建SpringBoot Project的唯一方法吗?

    否!也可以用Maven进行手动创建,也可以官网创建

  • SpringBoot支持几种配置文件?

    只有两种

    一种
    server.port=80
    debug=false
    server.servlet.context-path=/myspringboot
    logging.file=d:/logs/msb.log
    spring.mvc.date-format=yyyy-MM-dd
    
    二种 [按层级进行区分 yml 对程序维护有很大帮助]
    debug:false
    server:
      port:8000
      servlet:
        context-path:/
    spring:
      mvc:
        date-format:yyyy-MM-dd
    
  • 请列举至少五个SpringBoot中配置选项

    配置名称 默认值 描述
    server.port 8080 端口号
    server.servlet.context-path / 设置应用上下文
    logging.file 日志文件输出路径
    logging.level info 最低日志输出级别
    debug false 开启/关闭调试模式
    spring.datasource.* 与数据库相关的设置

Maven的构建生命周期

命令 用途
mvn archetype:generate 创建Maven工程构建
mvn validate 验证工程结构
mvn compile 编译源代码
mvn test 执行测试用例
mvn package 项目打包
mvn install 安装至本地仓库
mvn deploy 发布至远程仓库
阅读全文

SpringBoot入门及电商项目[SpringBoot_MyBatis.generator_Swagger2_Redis_Postman_RESTful_AOP_UUID_QRCode_Linux_Aliyun]

2024/2/28

Spring Boot电商项目入门与实战

  • 重量级的电商项目,前后端彻底分离
  • 体验企业级开发流程:需求分析、设计、编码、测试、上线
  • 用户管理、商品分析、商品管理、购物车、订单等核心功能
  • 数据库的设计与实现
  • 应用RESTful进行接口设计,并使用Postman进行接口测试
  • 应用MyBatis generator代码自动生成插件,提升开发效率
  • 使用阿里云完成项目的线上部署
  • 使用Swagger2构建强大的API文档
  • 使用Redis对商品分类信息进行缓存
  • 使用JSR-303实现请求参数校验

Spring Boot入门

软件版本
  • Java 1.8.0
  • MySQL 8.0.12
  • Maven 3.3.9
  • Spring Boot 2.2.1(严格一致)

Spring Boot诞生历史

  • Spring的缺点:配置过于繁琐
  • Spring Boot开发团队:Pivotal

Spring Boot简介

  • 简化初始搭建以及开发过程
  • 不再需要定义样板化的配置
  • 快速应用开发领域

Spring、Spring MVC、Spring Boot

  • Spring最初利用IOC和AOP解耦
  • 按照这种模式搞了MVC框架
  • 写很多样板代码很麻烦,就有了Spring Boot
  • Spring Cloud是在Spring Boot基础上

Spring Boot核心特点

  • 开箱即用
  • 约定优于配置

Spring Boot版本介绍

  • CURRENT 2.2.1 CURRENT GA [最新版本]
  • GA 2.2.2 SNAPSHOT [GA=General Availability 面向大众稳定版本 版本永恒不变]
  • SNAPSHOT 2.1.11 SNAPSHOT [快照 版本随时被修改]
  • 如何选择版本? 2.1.10 GA

新建Spring Boot项目演示

  • IDEA集成的Spring Initializr

第一个接口开发

com/imooc/springbootlearn/SpringBootlearnApplication.java
package com.imooc.springbootlearn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootlearnApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootlearnApplication.class, args);
    }

}
com/imooc/springbootlearn/ParaController.java
package com.imooc.springbootlearn;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 演示各种传参形式
 */
@RestController
public class ParaController {
    @GetMapping({"/firstrequest"})
    public String firstRequest(){
        return "第一个Spring Boot接口";
    }
}
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>SpringBootlearn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBootlearn</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

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

</project>

多种配置URL的方式

com/imooc/springbootlearn/ParaController.java
package com.imooc.springbootlearn;

import org.springframework.web.bind.annotation.*;

/**
 * 演示各种传参形式
 */
@RestController
@RequestMapping("/prefix")//统一增加公共前缀
public class ParaController {
    @GetMapping({"/firstrequest"})
    public String firstRequest() {
        return "第一个Spring Boot接口";
    }

    //@RequestParam 从请求中找到这个参数进行绑定
    @GetMapping({"/requestpara"})
    public String requestpara(@RequestParam Integer num) {
        return "para from request: " + num;
    }

    //@PathVariable 从URL中寻找对应的参数进行绑定
    @GetMapping({"/para/{num}"})
    public String pathpara(@PathVariable Integer num) {
        return "para from path: " + num;
    }

    //多URL的用法
    @GetMapping({"/multiurl1", "/multiurl2"})
    public String multiurl(@RequestParam Integer num) {
        return "para from path: " + num;
    }

    //增加程序健壮性 不一定必须传值
    @GetMapping({"/required"})
    public String required(@RequestParam(required =
            false, defaultValue = "0") Integer num) {
        return "para form request: " + num;
    }
}

Web项目的三层结构

  • Controller职责:对外暴露接口
  • Service职责:复杂业务场景下对业务逻辑做一层抽象和封装,保持Controller的简洁和独立,抽象出来的Service可以被Controller重复调用。具体业务代码写在service层,Controller只做简单的逻辑判断
  • DAO层职责:和数据相关的、增删改查数据库代码

配置文件简介

  • properties:容器端口名、数据库信息、日志级别…

  • yml:分层级,冒号后需要空格

可以自动转换的网址:在线yaml转properties-在线properties转yaml-ToYaml.com

Properties:
server.port=8081
server.servlet.context-path=/first
============================================================
YAML:
server:
  port: 8081
  servlet:
    context-path: /first

进行自定义配置

注解类配置载入数据[利用@Value注解]

com/imooc/springbootlearn/PropertiesController.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 演示读取配置的Controller
 */
@RestController
public class PropertiesController {
    //用@Value将properties的设置值进行绑定
    @Value("${school.grade}")
    Integer grade;
    @Value("${school.classnum}")
    Integer classnum;
    @GetMapping("/gradeclass")
    public String gradeClass(){
        return "年级: " + grade + " 班级:" + classnum;
    }
}
resoources/application.properties
#server.port=8081
##对于整个项目建立统一的前缀 http://127.0.0.1:8081/first/prefix/required
#server.servlet.context-path=/first
#school.方便分类
school.grade=3
school.classnum=6

配置类文件配置载入数据

com/imooc/springbootlearn/SchoolConfig.java
package com.imooc.springbootlearn;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * School配置类
 */
@Component
@ConfigurationProperties(prefix = "school")
public class SchoolConfig {
    //自动去配置信息里寻找且绑定
    Integer grade;
    Integer classnum;

    public Integer getGrade() {
        return grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }

    public Integer getClassnum() {
        return classnum;
    }

    public void setClassnum(Integer classnum) {
        this.classnum = classnum;
    }
}
com/imooc/springbootlearn/ConfigController.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 读取配置类
 */
@RestController
public class ConfigController {
    @Autowired
    SchoolConfig schoolConfig;
    @GetMapping({"/gradefromconfig"})
    public String gradeclass(){
        return "年级: " + schoolConfig.grade + " 班级:" + schoolConfig.classnum;

    }
}

Service和Dao的编写

  • 学生信息查询案例
com/imooc/springbootlearn/Student.java
package com.imooc.springbootlearn;

/**
 * 学生实体类
 */
public class Student {
    Integer id;
    String name;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
com/imooc/springbootlearn/StudentController.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 学生Controller
 */
@RestController
public class StudentController {
    @Autowired
    StudentService studentService;
    //服务层 去中转

    @GetMapping("/student")
    public String student(@RequestParam Integer num){
        Student student = studentService.findStudent(num);
        return student.toString();
    }
}
/*
联系
@Autowired和@Resource注解都是作为bean对象注入的时候使用的
两者都可以声明在字段和setter方法上
注意:如果声明在字段上,那么就不需要再写setter方法。但是本质上,该对象还是作为set方法的实参,通过执行set方法注入,只是省略了setter方法罢了

区别
@Autowired注解是Spring提供的,而@Resource注解是J2EE本身提供的
@Autowird注解默认通过byType方式注入,而@Resource注解默认通过byName方式注入
@Autowired注解注入的对象需要在IOC容器中存在,否则需要加上属性required=false,表示忽略当前要注入的bean,如果有直接注入,没有跳过,不会报错
*/
com/imooc/springbootlearn/StudentService.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 学生Service  之后调用mapper操作数据库[interface]
 */
@Service
public class StudentService {
    @Autowired
    StudentMapper studentMapper;

    public Student findStudent(Integer id){
        return studentMapper.findById(id);
    }
}
com/imooc/springbootlearn/StudentMapper.java 【接口】
package com.imooc.springbootlearn;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 学生Mapper
 */
@Mapper
@Repository
public interface StudentMapper {
    @Select("select * from students where id = #{id}")
    Student findById(Integer id);
}
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>springbootlearn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBootlearn</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

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

</project>
com/imooc/springbootlearn/SpringBootlearnApplication.java
package com.imooc.springbootlearn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages="com.imooc")
public class SpringBootlearnApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootlearnApplication.class, args);
    }

}
application.properties
#server.port=8081
#server.servlet.context-path=/first
school.grade=10
school.classnum=6

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root


SpringBoot电商项目

慕慕生鲜前台
慕慕生鲜后台

课程整体介绍

  • 为什么要做电商项目
  • 项目亮点以及功能模块介绍
  • 项目演示
  • 项目开发所需要工具准备
  • 数据库设计与项目初始化
  • 功能模块开发
  • 阿里云部署
  • 项目总结

项目亮点

  • 最新的业界的互联网企业的优质技术
  • 代码规范简介,充分优化
  • 流程完整、电商功能丰富
  • 前后端彻底分离,符合未来趋势,学了就能用

功能模块介绍

  • 项目功能:

    • 前台  {用户、商品分类、商品信息、购物车、订单}
      • 用户模块{注册、登录、更新签名、身份认证、登出}
      • 商品分类模块{多级目录、递归查询、缓存}
      • 商品模块{商品搜索、商品排序、商品列表、目录展示、商品详情}
      • 购物车模块{加入商品、列表显示、数量更改、删除商品、勾选反选、全选全不选}
      • 订单模块{下单、订单流程、订单详情、取消订单、支付二维码、扫码支付、个人订单、确认收货}
    • 后台  {用户、商品分类、商品信息、订单}
      • 管理员模块{登录登出、身份认证、安全限制}
      • 商品分类模块{分类列表、增加分类、修改分类、删除分类}
      • 商品模块{商品列表、新增商品、图片上传、更新删除、批量上下架}
      • 订单模块{订单列表、地址信息、发货、订单完结}

项目演示

  • 前后端分离
  • 接口文档
  • 全栈、CTO
  • 核心是接口的设计

项目开发所需工具准备

  • IDEA常用优质插件介绍
    • Maven Helper
    • Free MyBatis plugin [跳转/识别mapper语法错误]
  • Postman安装和常用功能演示
  • MySQL可视化工具

数据库设计与项目初始化

  • 表设计
  • 技术选型、思路
  • 新建项目,整合Mybatis,跑通接口
  • 引用log4j2日志组件
  • 使用AOP统一处理Web请求日志 [请求的参数 返回的商品、字段]

技术选型

  • Spring Boot 2.2.1RELEASE
  • MyBatis 3.4.6(优点)
  • Maven 3.6.1

技术选型需要考虑的点

  • 选择你最熟悉的技术 [最好不要超过30%的新技术]
  • 选择拥有强大社区支撑的开源技术
  • 确保技术前进步伐
  • 学会从业务端开始思考
  • 重视经验

项目初始化

  • 新建项目
  • mybatis-generator的安装配置
  • 自动生成DAO层文件
  • 跑通接口

【下方链接的主要问题就是 由于项目中的jdk和你实际安装的jdk不匹配】
{修改Project Structure → Project的版本}
{修改启动Edit的版本}
{修改setting→Build,Execution,Deployment→Compiler→Java Compiler中的Project version以及项目的Target version}

【Java异常】完美解决this version of the Java Runtime only recognizes class file versions up to xx.0异常_java runtime (class file version 61.0), this versi-CSDN博客

fileversion 55.0 this version of the Java Runtime only recognizes class file versions up to 52.0:已解决_class file version 55.0-CSDN博客

彻底解决:IDEA java: 警告: 源发行版 17 需要目标发行版 17-CSDN博客

generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
  <!-- 配置文件,放在resource目录下即可 -->
  <!--数据库驱动个人配置-->
  <classPathEntry
    location="/Users/Pluminary/.m2/repository/mysql/mysql-connector-java/8.0.18/mysql-connector-java-8.0.18.jar"/>
  <context id="MysqlTables" targetRuntime="MyBatis3">
    <property name="autoDelimitKeywords" value="true"/>
    <!--可以使用``包括字段名,避免字段名与sql保留字冲突报错-->
    <property name="beginningDelimiter" value="`"/>
    <property name="endingDelimiter" value="`"/>
    <!-- optional,旨在创建class时,对注释进行控制 -->
    <commentGenerator>
      <property name="suppressDate" value="true"/>
      <property name="suppressAllComments" value="true"/>
    </commentGenerator>
    <!--数据库链接地址账号密码-->
    <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
      connectionURL="jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull"
      userId="root"
      password="root">
      <property name="nullCatalogMeansCurrent" value="true"/>
    </jdbcConnection>
    <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
    <javaTypeResolver>
      <property name="forceBigDecimals" value="false"/>
    </javaTypeResolver>
    <!--生成Model类存放位置-->
    <javaModelGenerator targetPackage="com.imooc.mall.model.pojo"
      targetProject="src/main/java">
      <!-- 是否允许子包,即targetPackage.schemaName.tableName -->
      <property name="enableSubPackages" value="true"/>
      <!-- 是否对类CHAR类型的列的数据进行trim操作 -->
      <property name="trimStrings" value="true"/>
      <!-- 建立的Model对象是否 不可改变  即生成的Model对象不会有 setter方法,只有构造方法 -->
      <property name="immutable" value="false"/>
    </javaModelGenerator>
    <!--生成mapper映射文件存放位置-->
    <sqlMapGenerator targetPackage="mappers" targetProject="src/main/resources">
      <property name="enableSubPackages" value="true"/>
    </sqlMapGenerator>
    <!--生成Dao类存放位置-->
    <javaClientGenerator type="XMLMAPPER" targetPackage="com.imooc.mall.model.dao"
      targetProject="src/main/java">
      <property name="enableSubPackages" value="true"/>
    </javaClientGenerator>
    <!--生成对应表及类名-->
    <table schema="root" tableName="imooc_mall_cart" domainObjectName="Cart"
      enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_category" domainObjectName="Category" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_order" domainObjectName="Order" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_order_item" domainObjectName="OrderItem"
      enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_product" domainObjectName="Product" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_user" domainObjectName="User" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>

  </context>
</generatorConfiguration>
application.properties
spring.datasource.name=imooc_mall_datasource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>mall</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>small</name>
    <description>small</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <!--  1.依赖mysql-connector-java  mybatis-spring-boot-starter 加入generatorConfig.xml 写下面的Plugins-->
<!-- 2. 加入imooc_mall_local.sql后 点Maven -> Plugins -> mybatis-generator -> 点击第一个 -->
<!--  通过插件生成 此时就出来了com/imooc/mall/model/dao里面所有的 和 com/imooc/mall/model/pojo里面所有的 和 mappers-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</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>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
package com.imooc.mall.controller;

import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 用户控制器
 */
@Controller
public class UserController {
    //4.返回对象的基本信息 return里面会写和service相关的  5建立Service层
    @Autowired
    UserService userService;
    @GetMapping("/test")
    @ResponseBody //返回Json格式内容
    public User personalPage(){
//  6.补全return 7告诉mapper在哪里怎么去找 去application.properties编写 mybatis.mapper-locations:......
//  去主类里编写@MapperScan(basePackages = "com.imooc.mall.model.dao") 以防找不到mapper 8.去配置端口8083
//   9.加入log4j2.xml
        return userService.getUser();
    }
}
com/imooc/mall/service/UserService.java
package com.imooc.mall.service;

import com.imooc.mall.model.pojo.User;

//5.这是抽象的接口 还要让它实现 再创建一个impl 实现接口类 UserServiceImpl.java
public interface UserService {
    User getUser();
}
com/imooc/mall/service/impl/UserServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.model.dao.UserMapper;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * UserService实现类
 */
@Service
//5.重写里面的方法  @Autowired引入一个mapper去查询数据库返回真正的信息 6返回UserController补全return
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User getUser(){
        //通过主键来查询一个对象
        return userMapper.selectByPrimaryKey(1);
    }
}
package com.imooc.mall;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.mall.model.dao")
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}

log4j2日志

  • 日志级别(优先级降低):error, warn, info, debug, trace
  • 排除Logback依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="fatal">
  <Properties>
    <Property name="baseDir" value="${sys:user.home}/Desktop/idea_Space/logs"/>
  </Properties>

  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
      <ThresholdFilter level="info" onMatch="ACCEPT"
        onMismatch="DENY"/>
      <PatternLayout
        pattern="[%d{MM:dd HH:mm:ss.SSS}] [%level] [%logger{36}] - %msg%n"/>
    </Console>

    <!--debug级别日志文件输出-->
    <RollingFile name="debug_appender" fileName="${baseDir}/debug.log"
      filePattern="${baseDir}/debug_%i.log.%d{yyyy-MM-dd}">
      <!-- 过滤器 -->
      <Filters>
        <!-- 限制日志级别在debug及以上在info以下 -->
        <ThresholdFilter level="debug"/>
        <ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
      </Filters>
      <!-- 日志格式 -->
      <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
      <!-- 策略 -->
      <Policies>
        <!-- 每隔一天转存 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
        <!-- 文件大小 -->
        <SizeBasedTriggeringPolicy size="100 MB"/>
      </Policies>
    </RollingFile>

    <!-- info级别日志文件输出 -->
    <RollingFile name="info_appender" fileName="${baseDir}/info.log"
      filePattern="${baseDir}/info_%i.log.%d{yyyy-MM-dd}">
      <!-- 过滤器 -->
      <Filters>
        <!-- 限制日志级别在info及以上在error以下 -->
        <ThresholdFilter level="info"/>
        <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
      </Filters>
      <!-- 日志格式 -->
      <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
      <!-- 策略 -->
      <Policies>
        <!-- 每隔一天转存 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
        <!-- 文件大小 -->
        <SizeBasedTriggeringPolicy size="100 MB"/>
      </Policies>
    </RollingFile>

    <!-- error级别日志文件输出 -->
    <RollingFile name="error_appender" fileName="${baseDir}/error.log"
      filePattern="${baseDir}/error_%i.log.%d{yyyy-MM-dd}">
      <!-- 过滤器 -->
      <Filters>
        <!-- 限制日志级别在error及以上 -->
        <ThresholdFilter level="error"/>
      </Filters>
      <!-- 日志格式 -->
      <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
      <Policies>
        <!-- 每隔一天转存 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
        <!-- 文件大小 -->
        <SizeBasedTriggeringPolicy size="100 MB"/>
      </Policies>
    </RollingFile>
  </Appenders>
  <Loggers>
    <Root level="debug">
      <AppenderRef ref="Console"/>
      <AppenderRef ref="debug_appender"/>
      <AppenderRef ref="info_appender"/>
      <AppenderRef ref="error_appender"/>
    </Root>

  </Loggers>
</Configuration>

AOP统一处理Web请求日志

  • 为什么需要AOP统一处理Web请求日志 [对系统健壮性的保证 创建filter]
com/imooc/mall/filter/WebLogAspect.java
package com.imooc.mall.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * 打印请求和响应信息
 */
@Aspect
@Component
public class WebLogAspect {
    //生成loger类
    private final Logger log = LoggerFactory.getLogger(WebLogAspect.class);
//10.增加一个拦截点AOP
    @Pointcut("execution(public * com.imooc.mall.controller..*.*(..))")
    public void webLog(){

    }
//  10.提供请求参数
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint){
        //收到请求,记录请求内容  请求到来所作的事情
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        log.info("URL: " + request.getRequestURI().toString());
        log.info("HTTP_METHOD: " + request.getMethod());
        log.info("IP: " + request.getRemoteAddr());
        log.info("CLASS_METHOD: " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("ARGS: " + Arrays.toString(joinPoint.getArgs()));
    }
//  11.返回的时候也要拦截  返回参数res 拦截点webLog
    @AfterReturning(returning = "res",pointcut = "webLog()")
    public void doAfterReturning(Object res)throws Exception{
        //处理完请求,返回内容
        log.info("RESPONSE: " + new ObjectMapper().writeValueAsString(res));
    }
}

用户模块

  • 整体介绍:知识点、功能点
    [登录、注册、重名校验、密码加密存储、Session的使用、越权校验、统一响应对象、异常枚举、Java异常体系、Postman实操、统一异常处理、更新个人信息]
  • 接口设计
  • 编码设计

API统一返回对象

package com.imooc.mall.common;

import com.imooc.mall.exception.ImoocMallExceptionEnum;

/**
 * 通用返回对象 T有可能是返回的购物车对象
 * 12.编写响应API      13创建枚举异常com/imooc/mall/exception/ImoocMallExceptionEnum.java
 */
public class ApiRestResponse<T> {
    private Integer status;
    private String msg;
    private T data;
    private static final int OK_CODE = 10000;
    private static final String OK_MSG = "SUCCESS";

    public ApiRestResponse(Integer status, String msg, T data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    public ApiRestResponse(Integer status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    public ApiRestResponse() {
        //默认请求信息
        this(OK_CODE, OK_MSG);
    }
    public static<T> ApiRestResponse<T> success(){
//   建立带着10000 和 SUCCESS的方法
        return new ApiRestResponse<>();
    }

    public static <T> ApiRestResponse<T> error(Integer code, String msg) {
        return new ApiRestResponse<>(code, msg);
    }
//  14.用枚举来搞错误 为了方便调试编写完后生成一个toString方法 15修改UserController的register()
    public static <T> ApiRestResponse<T> error(ImoocMallExceptionEnum ex) {
        return new ApiRestResponse<>(ex.getCode(), ex.getMsg());
    }
//   把错误[异常]创建成一个枚举类

    public static<T> ApiRestResponse<T> success(T result){
//    两个成功success的方法
        ApiRestResponse<T> response = new ApiRestResponse<>();
        response.setData(result);
        return response;
    }

    @Override
    public String toString() {
        return "ApiRestResponse{" +
                "status=" + status +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

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

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
package com.imooc.mall.exception;

/**
 * 异常枚举
 */
//13.编写异常枚举 注意类是enum噢  14返回ApiRestResponse
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位");


    //异常码
    Integer code;
    //异常信息
    String msg;

    ImoocMallExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

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

注册接口开发

com/imooc/mall/controller/UserController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 用户控制器
 */
@Controller
public class UserController {
    //4.返回对象的基本信息 return里面会写和service相关的  5建立Service层
    @Autowired
    UserService userService;
    @GetMapping("/test")
    @ResponseBody //返回Json格式内容
    public User personalPage(){
//  6.补全return 7告诉mapper在哪里怎么去找 去application.properties编写 mybatis.mapper-locations:......
//  去主类里编写@MapperScan(basePackages = "com.imooc.mall.model.dao") 以防找不到mapper 8.去配置端口8083
//   9.加入log4j2.xml  增加aop的pom
        return userService.getUser();
    }
// 12.创造一个统一返回对象 com/imooc/mall/common/ApiRestResponse.java
    // 15.参数加在请求中所以加上@RequestParam 编写校验  16去UserService增加register接口
    @PostMapping("/register")
    @ResponseBody
    public ApiRestResponse register(@RequestParam("userName") String userName, @RequestParam("password") String password) throws ImoocMallException {
//    字符串为空 || 符合参数
        if (StringUtils.isEmpty(userName)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }if (StringUtils.isEmpty(password)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }
//   防止用户密码长度设置简单 密码长度不能少于8
        if (password.length()<8){
            return ApiRestResponse.error(ImoocMallExceptionEnum.PASSWORD_TOO_SHORT);
        }
 //18.补全操作  19进行统一处理异常[对前端安全考虑] GlobalExceptionHandler.java
        userService.register(userName, password);
        return ApiRestResponse.success();
    }
}

=========================================
http://127.0.0.1:8083/register

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Mar 06 00:06:06 CST 2024
There was an unexpected error (type=Method Not Allowed, status=405).
Request method 'GET' not supported
    
因为注册的时候采用的是@PostMapping("/register")
单纯查询用get 往数据库写东西post
@RequestMapping("/register")是GET和POST都支持的 但是不推荐
根据不同的业务逻辑选择类型

打开postman 新建一个去查询post
POST: 127.0.0.1:8083/register?userName=mumu&password=12345678

{
    "status": 10000,
    "msg": "SUCCESS",
    "data": null
}
com/imooc/mall/service/UserService.java
package com.imooc.mall.service;

import com.imooc.mall.model.pojo.User;

//5.这是抽象的接口 还要让它实现 再创建一个impl 实现接口类 UserServiceImpl.java
public interface UserService {
    User getUser();
// 16.写完接口去实现接口UserServiceImpl.java
    void register(String userName, String password);
}
com/imooc/mall/service/impl/UserServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.UserMapper;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * UserService实现类
 */
@Service
//5.重写里面的方法  @Autowired引入一个mapper去查询数据库返回真正的信息 6返回UserController补全return
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User getUser(){
        //通过主键来查询一个对象
        return userMapper.selectByPrimaryKey(1);
    }
    @Override
    public void register(String userName, String password) throws ImoocMallException {
//    16.查询用户名是否存在, 不允许重名 用userMapper去查数据
//    但未编写功能 去手动编写UserMapper.java
        User result = userMapper.selectByName(userName);
        if (result != null){
//    17. 用户已存在 在Service层不能直接return但是controller可以直接返回
            // 创建一个异常类 com/imooc/mall/exception/ImoocMallException.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
    // 通过检测 允许写入数据库
        User user = new User();
        user.setUsername(userName);
        user.setPassword(password);
        //先判断是不是空 不是空才修改  实现完以后回到controller层进行调用
        int count = userMapper.insertSelective(user);
        if (count==0){
            throw new ImoocMallException(ImoocMallExceptionEnum.INSERT_FAILED);
        }
    }
}
com/imooc/mall/model/dao/UserMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(User record);

    int insertSelective(User record);

    User selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(User record);

    int updateByPrimaryKey(User record);

    // 16. 新增功能 去对应的UserMapper.xml进行描述 117行  BaseResultMap就是一开始自动生成的User对象
    // <include refid="Base_Column_List"/> 选取完整的User对象
    User selectByName(String userName);
}
mappers/UserMapper.xml
<?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.imooc.mall.model.dao.UserMapper">
  <resultMap id="BaseResultMap" type="com.imooc.mall.model.pojo.User">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="personalized_signature" jdbcType="VARCHAR" property="personalizedSignature" />
    <result column="role" jdbcType="INTEGER" property="role" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
  </resultMap>
  <sql id="Base_Column_List">
    id, username, `password`, personalized_signature, `role`, create_time, update_time
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from imooc_mall_user
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    delete from imooc_mall_user
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.imooc.mall.model.pojo.User">
    insert into imooc_mall_user (id, username, `password`, 
      personalized_signature, `role`, create_time, 
      update_time)
    values (#{id,jdbcType=INTEGER}, #{username,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, 
      #{personalizedSignature,jdbcType=VARCHAR}, #{role,jdbcType=INTEGER}, #{createTime,jdbcType=TIMESTAMP}, 
      #{updateTime,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" parameterType="com.imooc.mall.model.pojo.User">
    insert into imooc_mall_user
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="id != null">
        id,
      </if>
      <if test="username != null">
        username,
      </if>
      <if test="password != null">
        `password`,
      </if>
      <if test="personalizedSignature != null">
        personalized_signature,
      </if>
      <if test="role != null">
        `role`,
      </if>
      <if test="createTime != null">
        create_time,
      </if>
      <if test="updateTime != null">
        update_time,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="id != null">
        #{id,jdbcType=INTEGER},
      </if>
      <if test="username != null">
        #{username,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        #{password,jdbcType=VARCHAR},
      </if>
      <if test="personalizedSignature != null">
        #{personalizedSignature,jdbcType=VARCHAR},
      </if>
      <if test="role != null">
        #{role,jdbcType=INTEGER},
      </if>
      <if test="createTime != null">
        #{createTime,jdbcType=TIMESTAMP},
      </if>
      <if test="updateTime != null">
        #{updateTime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.imooc.mall.model.pojo.User">
    update imooc_mall_user
    <set>
      <if test="username != null">
        username = #{username,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        `password` = #{password,jdbcType=VARCHAR},
      </if>
      <if test="personalizedSignature != null">
        personalized_signature = #{personalizedSignature,jdbcType=VARCHAR},
      </if>
      <if test="role != null">
        `role` = #{role,jdbcType=INTEGER},
      </if>
      <if test="createTime != null">
        create_time = #{createTime,jdbcType=TIMESTAMP},
      </if>
      <if test="updateTime != null">
        update_time = #{updateTime,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.imooc.mall.model.pojo.User">
    update imooc_mall_user
    set username = #{username,jdbcType=VARCHAR},
      `password` = #{password,jdbcType=VARCHAR},
      personalized_signature = #{personalizedSignature,jdbcType=VARCHAR},
      `role` = #{role,jdbcType=INTEGER},
      create_time = #{createTime,jdbcType=TIMESTAMP},
      update_time = #{updateTime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
  <select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
        <include refid="Base_Column_List"/>
        from imooc_mall_user
        where username = #{userName,jdbcType=VARCHAR}
  </select>
</mapper>
com/imooc/mall/exception/ImoocMallException.java
package com.imooc.mall.exception;

/**
 * 17. 统一异常(感觉像递归之前编写的枚举异常)
 */
public class ImoocMallException extends Exception{
    private final Integer code;
    private final String message;

    public ImoocMallException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public ImoocMallException(ImoocMallExceptionEnum exceptionEnum) {
        this(exceptionEnum.getCode(), exceptionEnum.getMsg());
    }

    public Integer getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
package com.imooc.mall.exception;

/**
 * 异常枚举
 */
//13.编写异常枚举 注意类是enum噢  14返回ApiRestResponse
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试");


    //异常码
    Integer code;
    //异常信息
    String msg;

    ImoocMallExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

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

GlobalExceptionHandler编写

  • 对前端安全考虑,敏感信息不会暴露给用户
  • 抛出异常,直接转化为Json的APIResponse
  • 抛出重名异常
  • 拦截异常并且转变成APIRespond统一类型输出
com/imooc/mall/exception/GlobalExceptionHandler.java
package com.imooc.mall.exception;

import com.imooc.mall.common.ApiRestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 19.处理统一异常的handler 业务异常 处理不同逻辑异常  
   20对密码进行MD5加密UserServiceImpl 先创建MD5Utils
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //   统一处理Exception.class异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e) {
        log.error("Default Exception: ", e);
        return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);
    }
    @ExceptionHandler(ImoocMallException.class)
    @ResponseBody
    public Object handleImoocMallException(ImoocMallException e) {
        log.error("ImoocMallException: ", e); //传进来的是什么就传出去
        return ApiRestResponse.error(e.getCode(), e.getMessage());
    }
}

对密码进行MD5保护

com/imooc/mall/util/MD5Utils.java
package com.imooc.mall.util;

import com.imooc.mall.common.Constant;
import org.apache.tomcat.util.codec.binary.Base64;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

//20.不具备解密的协议 哈希算法  因为有破解MD5的网站所以需要加盐 21com/imooc/mall/common/Constant.java
public class MD5Utils {
    public static String getMD5Str(String strValue) throws NoSuchAlgorithmException {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        return Base64.encodeBase64String(md5.digest((strValue+ Constant.SALT).getBytes()));
    }
//  用这个方法测试生成MD5的值
    public static void main(String[] args) throws NoSuchAlgorithmException {
        String md5Str = getMD5Str("12345");
        System.out.println(md5Str);
    }
}
com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
}
com/imooc/mall/service/impl/UserServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.UserMapper;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import com.imooc.mall.util.MD5Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.security.NoSuchAlgorithmException;

/**
 * UserService实现类
 */
@Service
//5.重写里面的方法  @Autowired引入一个mapper去查询数据库返回真正的信息 6返回UserController补全return
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User getUser(){
        //通过主键来查询一个对象
        return userMapper.selectByPrimaryKey(1);
    }
    @Override
    public void register(String userName, String password) throws ImoocMallException, NoSuchAlgorithmException {
//    16.查询用户名是否存在, 不允许重名 用userMapper去查数据
//    但未编写功能 去手动编写UserMapper.java
        User result = userMapper.selectByName(userName);
        if (result != null){
//    17. 用户已存在 在Service层不能直接return但是controller可以直接返回
            // 创建一个异常类 com/imooc/mall/exception/ImoocMallException.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
    // 通过检测 允许写入数据库
        User user = new User();
        user.setUsername(userName);
//        user.setPassword(password);
        //21.重写密码
        user.setPassword(MD5Utils.getMD5Str(password));
        //先判断是不是空 不是空才修改  18实现完以后回到controller层进行调用
        int count = userMapper.insertSelective(user);
        if (count==0){
            throw new ImoocMallException(ImoocMallExceptionEnum.INSERT_FAILED);
        }
    }
}

登录功能分析

  • 登录状态需要保持
  • session的实现方案:登陆后,会保存用户信息到session
  • 之后的访问,会先从session中获取用户信息,然后再执行业务逻辑
com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    SYSTEM_ERROR(20000,"系统异常");
com/imooc/mall/controller/UserController.java
//22登录login接口的开发 23返回UserServiceImpl写login
    @PostMapping("/login")
    @ResponseBody
    public ApiRestResponse login(@RequestParam("userName") String userName, @RequestParam("password") String password, HttpSession session) throws ImoocMallException, NoSuchAlgorithmException {
        if (StringUtils.isEmpty(userName)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }if (StringUtils.isEmpty(password)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }//26.编写完毕login接口
        User user = userService.login(userName, password);
        //保存用户信息时,不保存密码
        user.setPassword(null);
        //把对象放入session中
        session.setAttribute(Constant.IMOOC_MALL_USER,user);
        return ApiRestResponse.success(user);
    }
com/imooc/mall/service/impl/UserServiceImpl.java
    //23.写方法判断md5与其匹配
    @Override
    public User login(String userName, String password) throws ImoocMallException {
        String md5Password = null;
        try {
            md5Password = MD5Utils.getMD5Str(password);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
//   23写方法判断md5与其匹配 UserMapper中写 login
        //25.匹配 且去接口增加
        User user = userMapper.selectLogin(userName, password);
        if (user == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_PASSWORD);
        }//能找到就返回用户 直接从上面写@Override再自动修复就可以自动生成login的接口
        //26.返回UserController写完login接口
        return user;
    }

用户模块剩余接口开发 [更新个性签名]

com/imooc/mall/exception/ImoocMallExceptionEnum.java
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    SYSTEM_ERROR(20000,"系统异常");
}
com/imooc/mall/controller/UserController.java
//22登录login接口的开发 23返回UserServiceImpl写login
    @PostMapping("/login")
    @ResponseBody
    public ApiRestResponse login(@RequestParam("userName") String userName, @RequestParam("password") String password, HttpSession session) throws ImoocMallException, NoSuchAlgorithmException {
        if (StringUtils.isEmpty(userName)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }
        if (StringUtils.isEmpty(password)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }//26.编写完毕login接口  27更新个性签名接口
        User user = userService.login(userName, password);
        //保存用户信息时,不保存密码
        user.setPassword(null);
        //把对象放入session中 KEY
        session.setAttribute(Constant.IMOOC_MALL_USER, user);
        return ApiRestResponse.success(user);
    }

    //  27.个性签名接口 28编写UserServiceImpl的updateUserInfo方法
    @PostMapping("/user/update")
    @ResponseBody
    public ApiRestResponse updateUserInfo(HttpSession session, @RequestParam String signature) throws ImoocMallException {
        User currentUser = (User)session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }
        User user = new User();
        user.setId(currentUser.getId());
        user.setPersonalizedSignature(signature);
        //29.搞全代码
        userService.updateInformation(user);
        return ApiRestResponse.success();
    }
com/imooc/mall/service/impl/UserServiceImpl.java
//23.写方法判断md5与其匹配
    @Override
    public User login(String userName, String password) throws ImoocMallException {
        String md5Password = null;
        try {
            md5Password = MD5Utils.getMD5Str(password);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
//   23写方法判断md5与其匹配 UserMapper中写 login
        //25.匹配 且去接口增加
        User user = userMapper.selectLogin(userName, md5Password);
        if (user == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_PASSWORD);
        }//能找到就返回用户 直接从上面写@Override再自动修复就可以自动生成login的接口
        //26.返回UserController写完login接口
        return user;
    }
// 28写updateUserInfo方法 不需要返回任何信息 只需要提醒更行成功即可
    @Override
    public void updateInformation(User user) throws ImoocMallException {
//    更新个性签名
        int updateCount = userMapper.updateByPrimaryKeySelective(user);
        if(updateCount>1){
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }
        //快速使用@Override 快速补全接口代码 29返回UserController补全代码
    }
com/imooc/mall/service/UserService.java
package com.imooc.mall.service;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.model.pojo.User;

import java.security.NoSuchAlgorithmException;

//5.这是抽象的接口 还要让它实现 再创建一个impl 实现接口类 UserServiceImpl.java
public interface UserService {
    User getUser();
// 16.写完接口去实现接口UserServiceImpl.java
    void register(String userName, String password) throws ImoocMallException, NoSuchAlgorithmException;

    //23.写方法判断md5与其匹配
    User login(String userName, String password) throws ImoocMallException;

    // 28写updateUserInfo方法 不需要返回任何信息 只需要提醒更行成功即可
    void updateInformation(User user) throws ImoocMallException;
}

退出登录

com/imooc/mall/controller/UserController.java
//30.退出登录接口
    @PostMapping("/user/logout")
    @ResponseBody
    public ApiRestResponse logout(HttpSession session){
        session.removeAttribute(Constant.IMOOC_MALL_USER);
        return ApiRestResponse.success();
    }

管理员接口

com/imooc/mall/controller/UserController.java
    //  31.管理员登录接口 思路可以借鉴
    @PostMapping("/adminLogin")
    @ResponseBody
    public ApiRestResponse adminLogin(@RequestParam("userName") String userName, @RequestParam("password") String password, HttpSession session) throws ImoocMallException, NoSuchAlgorithmException {
        if (StringUtils.isEmpty(userName)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }
        if (StringUtils.isEmpty(password)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }//31.拿到用户名和密码的时候 对其进行校验是否为管理员
        //数据库中 role=1是普通用户   role=2是管理员用户
        // 32去serviceImpl写一个方法
        User user = userService.login(userName, password);
        //33.判断是否为管理员
//        userService.checkAdminRole(user).if
        if (userService.checkAdminRole(user)) {
            //是管理员
            //保存用户信息时,不保存密码
            user.setPassword(null);
            //把对象放入session中 KEY
            session.setAttribute(Constant.IMOOC_MALL_USER, user);
            return ApiRestResponse.success(user);
        }else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }
    //  34.创建分类接口 CategoryController
    }
com/imooc/mall/service/impl/UserServiceImpl.java
//  32.拿到用户  role=1是普通用户   role=2是管理员用户
    //33.回到Controller
    @Override
    public boolean checkAdminRole(User user){
        return user.getRole().equals(2);
    }
com/imooc/mall/exception/ImoocMallExceptionEnum.java
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    NEED_ADMIN(10009,"无管理员权限"),
    SYSTEM_ERROR(20000,"系统异常");
}

总结用户模块

  • 重难点:统一响应对象、登录状态保持、统一异常处理
  • 常见错误:响应对象不规范、异常不统一处理

商品分类管理模块开发

  • 模块介绍
  • 编码
  • 自测
  • 总结
什么是商品分类
  • 条例清楚,层次分明
  • 方便用户进行筛选和辨别
  • 可以通过分类的设置快速的进入对应的商品列表页面进行商品选择
分类层级
  • 在商品分类上需要继续做归类操作
  • 分类设置成三级
  • 层级太深的弊端:
    • 一是对用户不太友好,不利于寻找
    • 二是对后台管理人员不友好,不方便管理

分类模块的主要功能

  • 分类数据的设置
  • 分类的父一级目录、递归
模块介绍
  • 接口设计
  • 表设计

开发添加分类接口part1

com/imooc/mall/controller/CategoryController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.model.AddCategoryReq;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.CategoryService;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpSession;

//  34.创建分类接口 CategoryController
@Controller
public class CategoryController {
    @Autowired
    UserService userService;
    @Autowired
    CategoryService categoryService;
    @PostMapping("admin/category/add")
    @ResponseBody
    public ApiRestResponse addCategory(HttpSession session, @RequestBody AddCategoryReq addCategoryReq) {
        //登录且必须为管理员才可以 参数中需要添加很多元素 可以使用封装来搞
        //com/imooc/mall/model/model/AddCategoryReq.java
        if (addCategoryReq.getName() == null || addCategoryReq.getType() == null || addCategoryReq.getParentId() == null || addCategoryReq.getOrderNum() == null) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.PARA_NOT_NULL);
        }
        //对身份进行校验 用session获取当前的用户
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole){ //35.创建一个CategoryService 分类目录Service
        //37.补全代码 是管理员  在上面添加@RequestBody 去postman Body->(raw/JSON)里测试接口
            categoryService.add(addCategoryReq);
            return ApiRestResponse.success();
        }else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }
//38.简化参数校验过程 || || || ||  @Valid  @Size(AddCategoryReq) @NotNull
        //39.增加GlobalExceptionHandler.java中的提示代码 不仅仅只提供20000 系统异常 handleMethodArg。。。
    }
}
com/imooc/mall/model/model/AddCategoryReq.java
package com.imooc.mall.model.model;

/**
 * 34.AddCategoryReq 新建一个类 供添加接口进行封装 回去引用
 */
public class AddCategoryReq {
    private String name;
    private Integer type;
    private Integer parentId;
    private Integer orderNum;
}+GETTER SETTER
com/imooc/mall/service/CategoryService.java
package com.imooc.mall.service;

import com.imooc.mall.model.model.AddCategoryReq;
import com.imooc.mall.model.pojo.Category;

//35.创建一个CategoryService 分类目录Service 36创建一个它的实现类CategoryServiceImpl
public interface CategoryService {
    void add(AddCategoryReq addCategoryReq);
}
com/imooc/mall/service/impl/CategoryServiceImpl.java
package com.imooc.mall.service.impl;

import com.fasterxml.jackson.databind.util.BeanUtil;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.CategoryMapper;
import com.imooc.mall.model.model.AddCategoryReq;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.service.CategoryService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 36.目录分类Service实现类
 */
@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
//   去Mapper里增加selectbyName 117行
    CategoryMapper categoryMapper;
    public void add(AddCategoryReq addCategoryReq) {
        Category category = new Category();
//        category.setName(addCategoryReq.getName());
        //字段类型一样 字段名一样的话可以自动拷贝进去
        BeanUtils.copyProperties(addCategoryReq,category);
        Category categoryOld = categoryMapper.selectByName(addCategoryReq.getName());
        if (categoryOld != null){ //重名目录 不允许创建 (优化)将ImoocMallException中的extends换一个
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
        int count = categoryMapper.insertSelective(category);
        if (count == 0){
            throw new ImoocMallException(ImoocMallExceptionEnum.NEED_ADMIN);
        }//37.回到CategoryController.java
    }
}
mappers/CategoryMapper.xml
  <select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_category
    where name = #{name,jdbcType=VARCHAR}
  </select>

参数校验

注解 说明
@Valid 需要验证
@NotNull 非空
@Max(value) 最大值
@Size(max, min) 字符串长度范围限制
com/imooc/mall/model/model/AddCategoryReq.java
package com.imooc.mall.model.model;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * 34.AddCategoryReq 新建一个类 供添加接口进行封装 回去引用
 */
public class AddCategoryReq {
    @Size(min=2,max=5)
    private String name;
    @NotNull
    @Max(3)
    private Integer type;
    @NotNull(message = "parentId不能为null")
    private Integer parentId;
    @NotNull
    private Integer orderNum;
    
===================================================
POST:127.0.0.1:8083/admin/category/add
Body→raw→JSON:{"name":"鸭货伴手零食","type":2,"parentId":6,"orderNum":10}

{
    "status": 20000,
    "msg": "系统异常",
    "data": null
}

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [1] in public com.imooc.mall.common.ApiRestResponse com.imooc.mall.controller.CategoryController.addCategory(javax.servlet.http.HttpSession,com.imooc.mall.model.model.AddCategoryReq): [Field error in object 'addCategoryReq' on field 'name': rejected value [鸭货伴手零食]; codes [Size.addCategoryReq.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [addCategoryReq.name,name]; arguments []; default message [name],5,2]; default message [个数必须在2和5之间]] 
com/imooc/mall/exception/GlobalExceptionHandler.java
package com.imooc.mall.exception;

import com.imooc.mall.common.ApiRestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

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

/**
 * 19.处理统一异常的handler 业务异常 处理不同逻辑异常  20对密码进行MD5加密UserServiceImpl 先创建MD5Utils
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //   统一处理Exception.class异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e) {
        log.error("Default Exception: ", e);
        return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);
    }

    @ExceptionHandler(ImoocMallException.class)
    @ResponseBody
    public Object handleImoocMallException(ImoocMallException e) {
        log.error("ImoocMallException: ", e); //传进来的是什么就传出去
        return ApiRestResponse.error(e.getCode(), e.getMessage());
    }

    //  39.处理方法参数不合规的情况
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ApiRestResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException: ", e);
        return handleBindingResult(e.getBindingResult());
    }
//  40.处理返回异常的ApiRespond 41去pom引入Swagger自动生成API文档
    private ApiRestResponse handleBindingResult(BindingResult result){
//  把异常处理为对外暴露的提示
        List<String> list = new ArrayList<>();
        if (result.hasErrors()){
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError objectError : allErrors) { //itli快速  对着for按alt+回车 改成增强for
                String message = objectError.getDefaultMessage();
                list.add(message);
            }
        }
        if (list.size() == 0){
            return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR);
        } //list.toString()生成所创建的异常描述信息
        return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR.getCode(), list.toString());
    }
}

Swagger自动生成API文档

pom.xml
<!--41.导入Swagger自动生成API文档 并在main函数中加入注解 @EnableSwagger2-->
<!--42.再去创造一个config  配置文件    -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
com/imooc/mall/config/SpringFoxConfig.java
package com.imooc.mall.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
public class SpringFoxConfig {
 //41.访问http://localhost:8083/swagger-ui.html可以看到API文档 
    //42创建ImoocMallWebMvcConfig
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕慕生鲜")
                .description("")
                .termsOfServiceUrl("")
                .build();
    }
}
com/imooc/mall/MallApplication.java
package com.imooc.mall;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.mall.model.dao")
@EnableSwagger2
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}
com/imooc/mall/config/ImoocMallWebMvcConfig.java
package com.imooc.mall.config;

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

/**
 * 42.配置地址映射 
 * 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
   44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {

    public void addResourceHandles(ResourceHandlerRegistry registry){
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}
==================================================
http://localhost:8083/swagger-ui.html

更新目录接口

com/imooc/mall/model/request/UpdateCategoryReq.java
package com.imooc.mall.model.request;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * 45.CategoryController.java中创建新增方法
 */
public class UpdateCategoryReq {
    @NotNull(message = "id不能为null")
    private Integer id;

    @Size(min=2,max=5)
    private String name;
    @Max(3)
    private Integer type;

    private Integer parentId;
    private Integer orderNum;
com/imooc/mall/controller/CategoryController.java
 //    45.创建新增接口   46在CategoryServiceImpl.java中新增更新方法
    @ApiOperation("后台更新目录")
    @PostMapping("admin/category/update")
    @ResponseBody
    public ApiRestResponse updateCategory( @Valid @RequestBody UpdateCategoryReq updateCategoryReq, HttpSession session) {
        //对身份进行校验 用session获取当前的用户
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole) {
            //补全代码 是管理员  在上面添加@RequestBody 去postman Body->(raw/JSON)里测试接口
            //46.补全接口代码
            Category category = new Category(); //复制过来
            BeanUtils.copyProperties(updateCategoryReq, category);
            categoryService.update(category);
            return ApiRestResponse.success();
        } else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }//47.为了统一接口校验管理员身份 NEED_LOGIN NEED_ADMIN
        // 创建一个com/imooc/mall/filter/AdminFilter.java
    }
com/imooc/mall/service/impl/CategoryServiceImpl.java
 @Override //47.快速生成
    // 46在CategoryServiceImpl.java中新增更新方法
    public void update(Category updateCategory){
        if (updateCategory.getName() != null){
            Category categoryOld = categoryMapper.selectByName(updateCategory.getName());
            if (categoryOld != null && !categoryOld.getId().equals(updateCategory.getId())){ //不能为空且和原来的名字不一样
                throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
            }
        }
        categoryMapper.updateByPrimaryKeySelective(updateCategory); //根据主键更新
// 不和其他名字冲突
    }
com/imooc/mall/service/CategoryService.java
package com.imooc.mall.service;

import com.imooc.mall.model.pojo.Category;
import com.imooc.mall .model.request.AddCategoryReq;

//35.创建一个CategoryService 分类目录Service 36创建一个它的实现类CategoryServiceImpl
public interface CategoryService {
    void add(AddCategoryReq addCategoryReq);

    //47.快速生成
    // 46在CategoryServiceImpl.java中新增更新方法
    void update(Category updateCategory);
}

统一校验管理员身份

com/imooc/mall/controller/CategoryController.java
//    45.创建新增接口   46在CategoryServiceImpl.java中新增更新方法
    @ApiOperation("后台更新目录")
    @PostMapping("admin/category/update")
    @ResponseBody
    public ApiRestResponse updateCategory( @Valid @RequestBody UpdateCategoryReq updateCategoryReq, HttpSession session) {
        //对身份进行校验 用session获取当前的用户
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole) {
            //补全代码 是管理员  在上面添加@RequestBody 去postman Body->(raw/JSON)里测试接口
            //46.补全接口代码
            Category category = new Category(); //复制过来
            BeanUtils.copyProperties(updateCategoryReq, category);
            categoryService.update(category);
            return ApiRestResponse.success();
        } else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }//47.为了统一接口校验管理员身份 NEED_LOGIN NEED_ADMIN
        // 创建一个com/imooc/mall/filter/AdminFilter.java
    }
    @ApiOperation("后台更新目录")
    @PostMapping("admin/category/update")
    @ResponseBody //没有在接口里做权限校验
    public ApiRestResponse deleteCategory(){
        return null;
    }
com/imooc/mall/filter/AdminFilter.java
package com.imooc.mall.filter;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

/**
 *
 * 47. 管理员校验过滤器  48.Admin过滤器的配置
 */
public class AdminFilter implements Filter {
    @Autowired
    UserService userService;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对身份进行校验 用session获取当前的用户
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpSession session = request.getSession();
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            PrintWriter out = new HttpServletResponseWrapper((HttpServletResponse) servletResponse).getWriter();
            out.write("{\n" //用户未登录
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            out.flush();
            out.close();
            return;
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole) { //放行代码
            filterChain.doFilter(servletRequest,servletResponse);
        } else {
            PrintWriter out = new HttpServletResponseWrapper(
                    (HttpServletResponse) servletResponse).getWriter();
            out.write("{\n"
                    + "    \"status\": 10009,\n"
                    + "    \"msg\": \"NEED_ADMIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            out.flush();
            out.close();
        }
    }

    @Override
    public void destroy() {

    }
}
com/imooc/mall/filter/AdminFilterConfig.java
package com.imooc.mall.filter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Admin过滤器的配置
 */
@Configuration
public class AdminFilterConfig {
    @Bean
    public AdminFilter adminFilter(){
        return new AdminFilter();
    }
    @Bean(name = "adminFilterConf")
    public FilterRegistrationBean adminFilterConfig(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(adminFilter());
        filterRegistrationBean.addUrlPatterns("/admin/category/*");
        filterRegistrationBean.addUrlPatterns("/admin/product/*");
        filterRegistrationBean.addUrlPatterns("/admin/order/*");
        filterRegistrationBean.setName("adminFilterConfig");
        return filterRegistrationBean;
    }
}
com/imooc/mall/filter/AdminFilterConfig.java

删除目录接口、分页功能开发

@RequestBody用来接收http post请求的body,前端传入序列化好的json数据,后端可以解析为json对象(Content-Type需要指定为 application/json)。
@RequestParam用来接收请求url?后面的参数,或者Content-Type为multipart/form-data、application/x-www-form-urlencoded时的http body数据。
com/imooc/mall/controller/CategoryController.java
     @ApiOperation("后台删除目录")
    @PostMapping("admin/category/delete")
    @ResponseBody //没有在接口里做权限校验
    //49.编写delete接口  再去CategoryServiceImpl.java 写delete方法
    public ApiRestResponse deleteCategory(@RequestParam Integer id) {
        categoryService.delete(id);
        return ApiRestResponse.success();
    }
com/imooc/mall/service/impl/CategoryServiceImpl.java
//49.写delete方法     @Override快速更新service
    @Override
    public void delete(Integer id){
        Category categoryOld = categoryMapper.selectByPrimaryKey(id);
        //查不到记录,无法删除,删除失败
        if (categoryOld == null){
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
        int count = categoryMapper.deleteByPrimaryKey(id);
        if (count == 0){
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
    }
com/imooc/mall/controller/CategoryController.java
 //50.写后台查询商品分类列表的接口
    @ApiOperation("后台目录列表")
    @PostMapping("admin/category/list")
    @ResponseBody //没有在接口里做权限校验
    public ApiRestResponse listCategoryForAdmin(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        //51.去CategoryServiceImpl编写该接口的实现类
        PageInfo pageInfo = categoryService.listForAdmin(pageNum, pageSize);
        return ApiRestResponse.success(pageInfo);
    }
com/imooc/mall/model/vo/CategoryVO.java
package com.imooc.mall.model.vo;

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

public class CategoryVO {
    private Integer id;
    private String name;
    private Integer type;
    private Integer parentId;
    private Integer orderNum;
    private Date createTime;
    private Date updateTime;
    private List<CategoryVO> childCategory = new ArrayList<>();
}Setter+Getter
com/imooc/mall/service/impl/CategoryServiceImpl.java
//51.创建vo[转换过后反应给前端的一个类]  pageInfo里面蕴藏着一个List<Category>
    @Override
    public PageInfo listForAdmin(Integer pageNum, Integer pageSize){
        //把分页的功能包裹在List后 而不是直接返回
        //52.引入分页查询的pom 续写分页代码
        PageHelper.startPage(pageNum,pageSize,"type,order_num");
        //53.写一个查询的mapper新的sql语句 CategoryMapper.java => List<Category> selectList();
        //54.返回Categorycontroller补全代码
        List<Category> categoryList = categoryMapper.selectList();
        PageInfo pageInfo = new PageInfo(categoryList);
        return pageInfo;
    }
pom.xml
<!--  52.引入分页的依赖 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.13</version>
        </dependency>

用户分类列表接口开发

com/imooc/mall/controller/CategoryController.java
//54.用户分类列表接口开发 去Impl补写实现类
    @ApiOperation("前台目录列表")
    @GetMapping("category/list")
    @ResponseBody
    public ApiRestResponse listCategoryForCustomer(){
        List<CategoryVO> categoryVOS = categoryService.listCategoryForCustomer();
        return ApiRestResponse.success(categoryVOS);
    }

============================================
目录列表(給用户看):127.0.0.1:8083/category/list
com/imooc/mall/service/impl/CategoryServiceImpl.java
//54.用户分类列表接口开发
    @Override
    public List<CategoryVO> listCategoryForCustomer(){
        ArrayList<CategoryVO> categoryVOList = new ArrayList<>();
        recursivelyFindCategories(categoryVOList, 0);
        //55.去Mapper.java 和 Mapper.xml写方法
        return categoryVOList;
    }

    private void recursivelyFindCategories(List<CategoryVO> categoryVOList, Integer parentId){
        //递归获取所有子类别并组合 合成一个"目录树"
        List<Category> categoryList = categoryMapper.selectCategoriesByParentId(parentId);
        if (!CollectionUtils.isEmpty(categoryList)){
            //空 或 有无元素   itli
            for (int i = 0; i < categoryList.size(); i++) {
                Category category =  categoryList.get(i);
                CategoryVO categoryVO = new CategoryVO();
                //拷贝链接  比原来的多一个childCategory这个字段未被赋值
                BeanUtils.copyProperties(category, categoryVO);
                categoryVOList.add(categoryVO);
                //拿到childCategory字段并赋值 再从上面return categoryVOList;
                recursivelyFindCategories(categoryVO.getChildCategory(), categoryVO.getId());
            }
        }
    }
com/imooc/mall/model/dao/CategoryMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Category;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CategoryMapper {
    int deleteByPrimaryKey(Integer id);
    int insert(Category record);
    int insertSelective(Category record);
    Category selectByPrimaryKey(Integer id);
    int updateByPrimaryKeySelective(Category record);
    int updateByPrimaryKey(Category record);
    Category selectByName(String name);
    List<Category> selectList();
    List<Category> selectCategoriesByParentId(Integer parentId);
}
mappers/CategoryMapper.xml
<mapper>
 <select id="selectList" resultMap="BaseResultMap">
    select <include refid="Base_Column_List"/>
    from imooc_mall_category
  </select>
  <select id="selectCategoriesByParentId" resultMap="BaseResultMap" parameterType="int">
    select <include refid="Base_Column_List"/>
    from imooc_mall_category
    where parent_id = #{parentId}
  </select>
</mapper>

利用Redis缓存加速响应 [目录变化频繁(访问量大的) 增加效率]

pom.xml
<!--    55.导入Redis的pom文件    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
56.增加一个@EnableCaching注解让idea知道你想打开缓存功能
com/imooc/mall/MallApplication.java
package com.imooc.mall;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.mall.model.dao")
@EnableSwagger2
@EnableCaching
//56.增加注解 并去想用的前面加注解 @Cacheable(value = "listCategoryForCustomer")
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}

import org.springframework.cache.annotation.Cacheable; [对的]
import springfox.documentation.annotations.Cacheable; [错的]

com/imooc/mall/service/impl/CategoryServiceImpl.java
//这里要格外注意 不要导错包 不然运行不了redis 里面的keys会没有键值对
// import org.springframework.cache.annotation.Cacheable; [对的]
// import springfox.documentation.annotations.Cacheable; [错的]
 //54.用户分类列表接口开发
    @Override //56.在下方加想用Redis的注解  
  //57.创建一个对于缓存的配置类com/imooc/mall/config/CachingConfig.java
    @Cacheable(value = "listCategoryForCustomer")
    public List<CategoryVO> listCategoryForCustomer(){
        ArrayList<CategoryVO> categoryVOList = new ArrayList<>();
        recursivelyFindCategories(categoryVOList, 0);
        //55.去Mapper.java 和 Mapper.xml写方法
        return categoryVOList;
    }

    private void recursivelyFindCategories(List<CategoryVO> categoryVOList, Integer parentId){
        //递归获取所有子类别并组合 合成一个"目录树"
        List<Category> categoryList = categoryMapper.selectCategoriesByParentId(parentId);
        if (!CollectionUtils.isEmpty(categoryList)){
            //空 或 有无元素   itli
            for (int i = 0; i < categoryList.size(); i++) {
                Category category =  categoryList.get(i);
                CategoryVO categoryVO = new CategoryVO();
                //拷贝链接  比原来的多一个childCategory这个字段未被赋值
                BeanUtils.copyProperties(category, categoryVO);
                categoryVOList.add(categoryVO);
                //拿到childCategory字段并赋值 再从上面return categoryVOList;
                recursivelyFindCategories(categoryVO.getChildCategory(), categoryVO.getId());
            }
        }
    }
com/imooc/mall/config/CachingConfig.java
package com.imooc.mall.config;

import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.time.Duration;

/**
 * 57.缓存的配置类 想要运行成功保存序列化 要去弄个序列化接口com/imooc/mall/model/vo/CategoryVO.java
 */
@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheWriter redisCacheWriter = RedisCacheWriter
                .lockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));

        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
                cacheConfiguration);
        return redisCacheManager;
    }
}
com/imooc/mall/model/vo/CategoryVO.java
public class CategoryVO implements Serializable {
     private Integer id;
    private String name;
    private Integer type;
    private Integer parentId;
    private Integer orderNum;
    private Date createTime;
    private Date updateTime;
    private List<CategoryVO> childCategory = new ArrayList<>();
}Setter+Getter

Idea调试技巧

  • 断点统一开关
  • 条件断点
  • 单步调试
    • step into 会进入方法内部
    • step out 跳出方法
  • 表达式求值 [Evaluate Expression]

总结商品分类模块

  • 重难点:参数校验、Swagger[API文档自动]、统一鉴权[Filter过滤器]、Redis整合[Springboot整合]、调试功能
  • 常见错误:参数手动校验、项目没用Redis缓存、不善用调试

商品模块介绍

演示、数据表设计、接口设计

新增商品功能开发

com\imooc\mall\controller\ProductAdminController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import org.springframework.stereotype.Controller;

/**
 * 58.后台商品管理Controller  pojo的product复制一份到request变成AddProductReq
   59.需要ProductService.java
 */
@Controller
public class ProductAdminController {
    @Autowired
    ProductService productService;
    @PostMapping("admin/product/add")
    public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq){
    //61.补全代码  目前图片上传还未开发成功
        productService.add(addProductReq);
        return ApiRestResponse.success();
    }
}
com/imooc/mall/model/request/AddProductReq.java
package com.imooc.mall.model.request;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

public class AddProductReq {
    @NotNull(message = "商品名称不能为null")
    private String name;
    @NotNull(message = "商品图片不能为null")
    private String image;

    private String detail;
    @NotNull(message = "商品分类不能为null")
    private Integer categoryId;
    @NotNull(message = "商品价格不能为null")
    @Min(value = 1, message = "价格不能为null")
    private Integer price;

    @NotNull(message = "商品库存不能为null")
    @Max(value = 10000, message = "库存不能大于10000")
    private Integer stock;

    private Integer status;
}Getter+Setter
com/imooc/mall/service/ProductService.java
package com.imooc.mall.service;

import com.imooc.mall.model.request.AddProductReq;

/**
 * 59.商品Service 再创建一个实现类ProductServiceImpl
 */
public interface ProductService {

    void add(AddProductReq addProductReq);
}
com/imooc/mall/service/impl/ProductServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.ProductMapper;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.AddProductReq;
import com.imooc.mall.service.ProductService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 59.商品服务实现类
 */
@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    ProductMapper productMapper;//60.ProductMapper SQL通过名字查找product是否存在 151行
    @Override
    public void add(AddProductReq addProductReq){
        Product product = new Product();
        BeanUtils.copyProperties(addProductReq, product);//赋值
        //61.商品重名情况  返回ProductAdminController补全代码  
        //62.图片上传接口 ProductAdminController
        Product productOld = productMapper.selectByName(addProductReq.getName());
        if (productOld != null){
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
        int count = productMapper.insertSelective(product);
        if (count == 0){
            throw new ImoocMallException(ImoocMallExceptionEnum.CREATE_FAILED);
        }
    }
}

图片上传

  • 文件名UUID
  • 通用唯一识别码(Universally Unique Identifier)
  • 防止重名、防止爬图
  • 生成规则:日期和时间、MAC地址、HashCode、随机数
com/imooc/mall/controller/ProductAdminController.java
//62.图片上传接口
    @PostMapping("admin/upload/file")
    public  ApiRestResponse upload(HttpServletRequest httpServletRequest, @RequestParam("file") MultipartFile file){
        String fileName = file.getOriginalFilename();
        String suffixName = fileName.substring(fileName.lastIndexOf("."));//用文件后面的名字
        //生成文件名称UUID
        UUID uuid = UUID.randomUUID();
        String newFileName = uuid.toString() + suffixName;
        //创建文件 放在常量类中 Constant.java
        //63.application.properties配置file.upload.dir
//        new File()
        File fileDirectory = new File(Constant.FILE_UPLOAD_DIR);
        File destFile = new File(Constant.FILE_UPLOAD_DIR + newFileName);
        if (!fileDirectory.exists()){
            if (!fileDirectory.mkdir()){//新建文件夹
                throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
            }
        }
        try {
            file.transferTo(destFile); //传进来的写到空的方法中去
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        try {
            return ApiRestResponse.success(getHost(new URI(httpServletRequest.getRequestURL()+""))+"/images/"+newFileName); //生成的路径IP和端口号
        } catch (URISyntaxException e) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.UPLOAD_FAILED);
        }
    }
    private URI getHost(URI uri){
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),null,null,null);
        } catch (URISyntaxException e) {
            effectiveURI = null; //如果新建失败 就返回回去
        }
        return effectiveURI;
    }
com/imooc/mall/common/Constant.java
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
    @Value("${file.upload.dir}")
    public static String FILE_UPLOAD_DIR;
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    NEED_ADMIN(10009,"无管理员权限"),
    NAME_NOT_NULL(10010,"参数不能为空"),
    PARA_NOT_NULL(10011,"参数不能为空"),
    CREATE_FAILED(10012,"新增失败"),
    REQUEST_PARAM_ERROR(10013,"参数错误"),
    DELETE_FAILED(10014,"删除失败"),
    MKDIR_FAILED(10015,"文件夹创建失败"),
    UPLOAD_FAILED(10015,"图片上传失败"),
    SYSTEM_ERROR(20000,"系统异常");
}

资源映射开发

此时从postman中创建一个上传图片的接口
127.0.0.1:8083/admin/upload/file
选择Body→form-data
此时上传图片会报错 {
“status”: 20000,
“msg”: “系统异常”,
“data”: null
}

com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
@Component //让spring帮注入value
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
    public static String FILE_UPLOAD_DIR;
    //64.为了解决上传图片系统异常报错 注入失败的原因是上方是static普通变量 set方法把静态变量赋值
    @Value("${file.upload.dir}")
    public void setFileUploadDir(String fileUploadDir){
        FILE_UPLOAD_DIR = fileUploadDir;
    }//65.打开ImoocMallWebMvcConfig 加一个映射规则
}

自定义静态资源映射目录

  • 上传图片后回显
  • 配置SpringBootWebMvcConfig
  • 静态资源到本地目录的映射
  • 演示打开图片
com/imooc/mall/config/ImoocMallWebMvcConfig.java
package com.imooc.mall.config;

import com.imooc.mall.common.Constant;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 42.配置地址映射 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
 * 44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
    public void addResourceHandles(ResourceHandlerRegistry registry){
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + Constant.FILE_UPLOAD_DIR);
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}

更新(UpdateProductReq)和新增(AddProductReq)商品

  • 合并写法不可取
  • 业务逻辑清晰、独立
com/imooc/mall/model/request/UpdateProductReq.java
public class UpdateProductReq {
    @NotNull
    private Integer id; 
    private String name;
    private String image;
    private String detail;
    private Integer categoryId;
    @Min(value = 1, message = "价格不能为null")
    private Integer price;

    @Max(value = 10000, message = "库存不能大于10000")
    private Integer stock;

    private Integer status;
}
com/imooc/mall/config/ImoocMallWebMvcConfig.java
package com.imooc.mall.config;

import com.imooc.mall.common.Constant;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 42.配置地址映射 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
 * 44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
    public void addResourceHandles(ResourceHandlerRegistry registry){
        //65.增加一个registry  66.新增接口继续开发 ProductAdminController
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + Constant.FILE_UPLOAD_DIR);
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}
com/imooc/mall/controller/ProductAdminController.java
 //66. 接口 复制一个request中的AddProductReq 之后回来补全代码
    @ApiOperation("后台更新商品")
    @PostMapping("/admin/product/update")
    public ApiRestResponse updateProduct(@Valid @RequestBody UpdateProductReq updateProductReq){
        Product product = new Product();
        BeanUtils.copyProperties(updateProductReq, product);
        //67.进入ProductServiceImpl编写接口实现类
        productService.update(product);
        return ApiRestResponse.success();
    }
    //68. 搞一个删除的接口 同理也在ProductServiceImpl中写实现类 之后回来补全代码
    @ApiOperation("后台删除商品")
    @PostMapping("/admin/product/delete")
    public ApiRestResponse deleteProduct(@RequestParam Integer id){
        productService.delete(id);
        return ApiRestResponse.success();
    }com/imooc/mall/controller/ProductAdminController.java
com/imooc/mall/service/impl/ProductServiceImpl.java
//67.写updateProduct实现类 @Override自动导入
    @Override
    public void update(Product updateProduct) {
        Product productOld = productMapper.selectByName(updateProduct.getName());
        //同名且不同id,不能继续修改
        if (productOld != null && productOld.getId().equals(updateProduct.getId())) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
        int count = productMapper.updateByPrimaryKeySelective(updateProduct);
        if (count == 0) {
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }
    }

    //68.删除实现类  @Override自动导入
    @Override
    public void delete(Integer id) {
        Product productOld = productMapper.selectByPrimaryKey(id);
        //查不到该记录,无法删除
        if (productOld == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
        int count = productMapper.deleteByPrimaryKey(id);
        if (count == 0) {
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
    }

批量上下架

  • MyBatis遍历List
  • where语句拼接
com/imooc/mall/controller/ProductAdminController.java
//69. 批量上下架接口 同理也在ProductServiceImpl中写实现类 Napper中增加批量上下架的SQL
    // 之后回来补全代码
    @ApiOperation("后台批量上下架接口")
    @PostMapping("/admin/product/batchUpdateSellStatus")
    public ApiRestResponse batchUpdateSellStatus(@RequestParam Integer[] ids, @RequestParam Integer sellStatus){
        productService.batchUpdateSellStatus(ids,sellStatus);
        return ApiRestResponse.success();
    }
=====================================================
127.0.0.1:8083//admin/product/batchUpdateSellStatus?ids=2,3&sellStatus=0
com/imooc/mall/service/impl/ProductServiceImpl.java
//69.去ProductAdminController增加batchUpdateSellStatus接口
    @Override
    public void batchUpdateSellStatus(Integer[] ids, Integer sellStatus){
        productMapper.batchUpdateSellStatus(ids, sellStatus);
    return null;
    }
com/imooc/mall/model/dao/ProductMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Product;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Product record);

    int insertSelective(Product record);

    Product selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Product record);

    int updateByPrimaryKey(Product record);

    Product selectByName(String name);
    int batchUpdateSellStatus(@Param("ids") Integer[] ids, @Param("sellStatus") Integer sellStatus);

}
mappers/ProductMapper.xml
  <select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
    select <include refid="Base_Column_List"/>
    from imooc_mall_product
    where name = #{name,jdbcType = VARCHAR}
  </select>
  <update id="batchUpdateSellStatus">
    update imooc_mall_product
    set status=#{sellStatus}
    where id in
    <foreach collection="ids" close=")" item="id" open="(" separator=",">
      #{id}
    </foreach>
  </update>

后台商品列表、商品详情接口

com/imooc/mall/service/impl/ProductServiceImpl.java
 //70.后台商品列表接口
    @Override
    public PageInfo listForAdmin(Integer pageNum, Integer pageSize){
        PageHelper.startPage(pageNum, pageSize); //在mapper里写查询sql
        List<Product> products = productMapper.selectListForAdmin();
        PageInfo pageInfo = new PageInfo(products);
        return pageInfo;
    }
    //72.商品详情接口开发 再去ProductController调用
    @Override
    public Product detail(Integer id){
        Product product = productMapper.selectByPrimaryKey(id);
        return product;
    }
com/imooc/mall/controller/ProductAdminController.java
 //70.后台商品列表接口 同理也在ProductServiceImpl中写实现类 Napper中增加批量上下架的SQL 补全代码
    //71.开发与前台商品ProductController.java
    @ApiOperation("后台商品列表接口")
    @PostMapping("/admin/product/list")
    public ApiRestResponse list(@RequestParam Integer[] pageNum, @RequestParam Integer pageSize){
        PageInfo pageInfo = productService.batchUpdateSellStatus(pageNum,pageSize);
        return ApiRestResponse.success(pageInfo);
    }
com/imooc/mall/controller/ProductController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.service.ProductService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 71.前台商品Controller 72新增detail接口 ProductServiceImpl.java
 */
@RestController
public class ProductController {
    @Autowired
    ProductService productService;

    @ApiOperation("商品详情")
    @GetMapping("product/detail")
    public ApiRestResponse detail(@RequestParam Integer id){
        Product product = productService.detail(id);
        return ApiRestResponse.success(product);
    }
}

前台商品列表接口part1

商品列表:搜索功能

入参判空 → 加%通配符 → like关键字

对于查询目录的in处理
  • 目录处理:如果查某个目录下的商品,不仅是需要查出来该目录的,还需要查出来子目录的所有商品
  • 所以这里要拿到某一个目录Id下的所有子目录id的List
前台:商品列表
  • 排序功能
  • MyBatis PageHelper
  • 枚举:order by [自定义]
com/imooc/mall/service/impl/ProductServiceImpl.java
//72.商品详情接口开发 再去ProductController调用
    @Override
    public Product detail(Integer id){
        Product product = productMapper.selectByPrimaryKey(id);
        return product;
    }
    //73.完成实现类
    @Override
    public PageInfo list(ProductListReq productListReq){
        //复杂查询就构建一个querry对象 ProductListQuery.java
        ProductListQuery productListQuery = new ProductListQuery();
        //☆☆搜索处理☆☆  拼接且转换成字符串去数据库查找
        if (!StringUtils.isEmpty(productListReq.getKeyword())){
            String keyword = new StringBuilder().append("%").append(productListReq.getKeyword()).append("%").toString();
            productListQuery.setKeyword(keyword);
        }
        //☆☆目录处理☆☆:如果查某个目录下的商品,不仅是需要查出该目录下的,还要把所有子目录的所有商品都查出来,所以要拿到一个目录id的List
        if (productListReq.getCategoryId() != null){
            //要拿到子目录 引用CategoryService   CategoryVO原本是给前台目录用的 需要重构一下 不是所有目录而是指定目录
            //改动代码 CategoryServiceImpl.java中的listCategoryForCustomer 传入的参数是Integer parentId
            //productListReq获取了所有根节点的list  因为点开List<CategoryVO>里面包括一个  private List<CategoryVO>递归结构
            List<CategoryVO> categoryVOList = categoryService.listCategoryForCustomer(productListReq.getCategoryId());
            ArrayList<Integer> categoryIds = new ArrayList<>(); //拿过来之后存储的list
            categoryIds.add(productListReq.getCategoryId());
            getCategoryIds(categoryVOList,categoryIds);
            productListQuery.setCategoryIds(categoryIds);
        }
        //74.排序能力 错误:前端传什么就 就传到sql中排序  这样是不安全的 要提前处理好
        //☆☆排序处理☆☆:去Constant定义支持的排序模式和手段
        String orderBy = productListReq.getOrderBy();
        if (Constant.ProductListOrderBy.PRICE_ASC_DESC.contains(orderBy)) {
            PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize(), orderBy);
        }else {//前端不一定包含数据的话就不排序了
            PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize());
        }//75.去写Mapper 最后回到ProductController写调用
        List<Product> productList = productMapper.selectList(productListQuery);
        PageInfo pageInfo = new PageInfo(productList);
        return pageInfo;
    }
    //74.写一个方法拿到所有的id 拿到参数之后往哪里存放
    private void getCategoryIds(List<CategoryVO>categoryVOList, ArrayList<Integer> categoryIds){
        for (int i = 0; i < categoryVOList.size(); i++) {
            CategoryVO categoryVO =  categoryVOList.get(i);
            if (categoryVO != null){
                categoryIds.add(categoryVO.getId());
                //递归子节点 子子节点
                getCategoryIds(categoryVO.getChildCategory(), categoryIds); //去上面调用方法 传入对象
            }
            
        }
    }
com/imooc/mall/model/query/ProductListQuery.java
package com.imooc.mall.model.query;

import java.util.List;

/**
 * 查询商品列表的Query
 */
public class ProductListQuery {
    private String keyword;
    private List<Integer> categoryIds;
}Getter+Setter
com/imooc/mall/model/dao/ProductMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.query.ProductListQuery;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductMapper {
    List<Product> selectListForAdmin(); //给前台用户用的
    List<Product> selectList(@Param("ids")ProductListQuery query); //给后台用户用的 Type是一个类

}
mappers/ProductMapper.xml
 <select id="selectList" resultMap="BaseResultMap"
          parameterType="com.imooc.mall.model.query.ProductListQuery">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_product
    <where>
      <if test="query.keyword != null">
        and name like #{query.keyword}
      </if>
      <if test="query.categoryIds != null">
        and category_id in
        <foreach collection="query.categoryIds" close=")" item="item" open="(" separator=",">
          #{item}
        </foreach>
      </if>
      and status = 1
    </where>
    order by update_time desc
  </select>
com/imooc/mall/model/dao/ProductMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.query.ProductListQuery;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Product record);

    int insertSelective(Product record);

    Product selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Product record);

    int updateByPrimaryKey(Product record);

    Product selectByName(String name);
    int batchUpdateSellStatus(@Param("ids") Integer[] ids, @Param("sellStatus") Integer sellStatus);

    List<Product> selectListForAdmin(); //给前台用户用的

    List<Product> selectList(@Param("query")ProductListQuery query); //给后台用户用的 Type是一个类

}
com/imooc/mall/controller/ProductController.java
package com.imooc.mall.controller;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.ProductListReq;
import com.imooc.mall.service.ProductService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 71.前台商品Controller 72新增detail接口 ProductServiceImpl.java
 */
@RestController
public class ProductController {
    @Autowired
    ProductService productService;

    @ApiOperation("商品详情")
    @GetMapping("product/detail")
    public ApiRestResponse detail(@RequestParam Integer id){
        Product product = productService.detail(id);
        return ApiRestResponse.success(product);
    }
    //73.开发前台商品列表 开一个ProductListReq.java 去ProductServiceImpl
    @ApiOperation("商品详情")
    @GetMapping("product/list")
    public ApiRestResponse list(ProductListReq productListReq){
        PageInfo list = productService.list(productListReq);
        return ApiRestResponse.success(list);
    }
}

总结商品模块

  • 重难点:商品的搜索[like]、排序[枚举+自定义]、目录查询[所有目录id都查到 再用查询列表方式]
  • 常见错误:更新和新增放在同一个接口、排序字段不用枚举

购物车模块介绍

业务流程
  • 添加商品到购物车 → 商品是否在售、是否有库存
    • →[否] 提示用户
    • →[是] 该商品之前就在购物车里
      • →[否] 添加新商品
      • →[是] 原有基础上添加数量
com/imooc/mall/controller/CartController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * 76.购物车Controller  77.用一个通用UserFilter逻辑
 */
@Controller
@RequestMapping("/cart")
public class CartController {//因为放在url中 要加数据绑定 通过HttpSession获取用户信息太麻烦 用一个通用filter逻辑
    @PostMapping("/add")
    public ApiRestResponse add(@RequestParam Integer productId, @RequestParam Integer count){
        return null;
    }
}
========================================================
127.0.0.1:8083/cart/add?productId=22&count=1
com/imooc/mall/filter/UserFilter.java
package com.imooc.mall.filter;

import com.imooc.mall.common.Constant;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

/**
 *
 * 77.用户过滤器 希望把用户信息currentUser保存下来
 */
public class UserFilter implements Filter {
    public static User currentUser;
    @Autowired
    UserService userService;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对身份进行校验 用session获取当前的用户
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpSession session = request.getSession();
        currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            PrintWriter out = new HttpServletResponseWrapper((HttpServletResponse) servletResponse).getWriter();
            out.write("{\n" //用户未登录
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            out.flush();
            out.close();
            return;
        }
        //78.续写过滤器链条 写好后对用户链条进行config配置 UserFilterConfig.java
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}
com/imooc/mall/config/UserFilterConfig.java
package com.imooc.mall.config;

import com.imooc.mall.filter.UserFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 78.对调用接口时对哪个类进行拦截 过滤器的配置 79创建cartService
 */
@Configuration
public class UserFilterConfig {
    @Bean
    public UserFilter userFilter(){
        return new UserFilter();
    }
    @Bean(name = "userFilterConf")
    public FilterRegistrationBean adminFilterConfig(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(userFilter());
        //下面的是校验与拦截
        filterRegistrationBean.addUrlPatterns("/cart/*"); //CartController.java
        filterRegistrationBean.addUrlPatterns("/order/*");
        filterRegistrationBean.setName("userFilterConf");
        return filterRegistrationBean;
    }
}
com/imooc/mall/service/impl/CartServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.CategoryMapper;
import com.imooc.mall.model.dao.ProductMapper;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.vo.CartVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 80.购物车Service实现类 其中List<>中间是需要包括 商品id 图片 名字 商品选中 数量信息
 * 81.创建一个VO增加那些属性放在<>中 是返回给前端组合后的对象
 */
@Service
public class CartServiceImpl {
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CategoryMapper categoryMapper;

    public List<CartVO> add(Integer userId, Integer productId, Integer count) {
        validProduct(productId, count);

    }

    private void validProduct(Integer productId, Integer count) {
        Product product = productMapper.selectByPrimaryKey(productId);
        //判断商品是否存在,商品是否上架
        if (product == null || product.getStatus().equals(Constant.SaleStatus.NOT_SALE)) {
            //82. 1是上架 1不明确要形成代码让顾客知道 Constant.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_SALE);
        }
        //判断商品库存 如果要买的比库存多 就买不了
        if (count > product.getStock()) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
        }
    }
}
com/imooc/mall/common/Constant.java
    //83.创建商品上下架的状态 创建CartMapper[Cart selectCartByUserIdAndProductId] 写sql 
    //84再去CartServiceImpl
    public interface SaleStatus{
        int NOT_SALE = 0; //商品下架状态
        int SALE = 1; //商品上架状态
    }
@Service
public class CartServiceImpl implements CartService {
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CategoryMapper categoryMapper;
    @Autowired
    CartMapper cartMapper;
@Override
    public List<CartVO> add(Integer userId, Integer productId, Integer count) {
        validProduct(productId, count);
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        //84.补全代码
        if (cart==null){
            //这个商品之前不在购物车里,需要新增一个记录
            cart = new Cart();
            cart.setProductId(productId);
            cart.setUserId(userId);
            cart.setQuantity(count);
            cart.setSelected(Constant.Cart.CHECKED);
            cartMapper.insertSelective(cart);
        }else {
            //这个商品已经在购物车里了,数量相加
            count = cart.getQuantity() + count;
            Cart cartNew = new Cart();
            cartNew.setQuantity(count);
            cartNew.setId(cart.getId());
            cartNew.setProductId(cart.getProductId());
            cartNew.setUserId(cart.getUserId());
            //无论是否想买都选中
            cartNew.setSelected(Constant.Cart.CHECKED);
            cartMapper.updateByPrimaryKeySelective(cartNew);
        }
        //85.去CartController补全代码逻辑 返回购物车列表哦~ 86.CartMapper.java
        return null;
    }

    private void validProduct(Integer productId, Integer count) {
        Product product = productMapper.selectByPrimaryKey(productId);
        //判断商品是否存在,商品是否上架
        if (product == null || product.getStatus().equals(Constant.SaleStatus.NOT_SALE)) {
            //82. 1是上架 1不明确要形成代码让顾客知道 Constant.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_SALE);
        }
        //判断商品库存 如果要买的比库存多 就买不了
        if (count > product.getStock()) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
        }
    }
}
com/imooc/mall/model/dao/CartMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Cart;
import com.imooc.mall.model.vo.CartVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Repository
public interface CartMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Cart record);

    int insertSelective(Cart record);

    Cart selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Cart record);

    int updateByPrimaryKey(Cart record);

    //86.选中列表的方法 传入的参数是userId
    List<CartVO> selectList(@Param("userId") Integer userId);
    Cart selectCartByUserIdAndProductId(@Param("userId") Integer userId, @Param("productId")Integer productId);
}
mappers/CartMapper.xml
 <select id="selectList" resultType="com.imooc.mall.model.vo.CartVO" parameterType="java.lang.Integer">
    select
    c.id as id,
    p.id as productId,
    c.user_id as userId,
    c.quantity as quantity,
    c.selected as selected,
    p.price as price,
    p.name as productName,
    p.image as productImage
    from imooc_mall_cart c
    left join imooc_mall_product p on p.id = c.product_id
    where c.user_id = #{userId}
    and p.status = 1
  </select>
com/imooc/mall/controller/CartController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.filter.UserFilter;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.service.CartService;
import io.swagger.annotations.ApiOperation;
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 java.util.List;

/**
 * 76.购物车Controller  77.用一个通用User_filter逻辑
 */
@RestController //404的错误① 返回的json格式 要用RestController
@RequestMapping("/cart")
public class CartController {//因为放在url中 要加数据绑定 通过HttpSession获取用户信息太麻烦 用一个通用filter逻辑
    @Autowired
    CartService cartService;
    @PostMapping("/list")
    @ApiOperation("购物车列表")
    public ApiRestResponse list(){
        //86.去写service的list接口 CartMapper中写选中列表的方法
        //内部获取用户Id,防止横向越权
        List<CartVO> cartList = cartService.list(UserFilter.currentUser.getId());
        return ApiRestResponse.success();
    }

    @PostMapping("/add")
    @ApiOperation("添加商品到购物车")
    public ApiRestResponse add(@RequestParam Integer productId, @RequestParam Integer count){
        //85.补全完逻辑代码 87在下面补全代码 List<CartVO> cartVOList =
        List<CartVO> cartVOList = cartService.add(UserFilter.currentUser.getId(), productId, count);
        return ApiRestResponse.success(cartVOList);
    }
}

更新、删除购物车接口

com/imooc/mall/controller/CartController.java
@PostMapping("/update")
    @ApiOperation("更新购物车")
    public ApiRestResponse update(@RequestParam Integer productId, @RequestParam Integer count){
        //86.去service层中写更新方法 CartServiceImpl.java
        List<CartVO> cartVOList = cartService.update(UserFilter.currentUser.getId(), productId, count);
        return ApiRestResponse.success(cartVOList);
    }

    @PostMapping("/delete")
    @ApiOperation("删除保护购物车")
    public ApiRestResponse delete(@RequestParam Integer productId){
        //87.删除保护购物车方法
        //不能传入userId, cartId, 否则可以删除别人的购物车
        List<CartVO> cartVOList = cartService.delete(UserFilter.currentUser.getId(), productId);
        return ApiRestResponse.success(cartVOList);
    }
=====================================================================
127.0.0.1:8083/cart/delete?productId=22
com/imooc/mall/service/impl/CartServiceImpl.java
 @Override
    public List<CartVO> update(Integer userId, Integer productId, Integer count){
        //86.去service层中写更新方法
        validProduct(productId, count);
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        if (cart==null){
            //这个商品之前不在购物车里,无法更新
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }else {
            //这个商品已经在购物车里了,则更新数量
            Cart cartNew = new Cart();
            cartNew.setQuantity(count);
            cartNew.setId(cart.getId());
            cartNew.setProductId(cart.getProductId());
            cartNew.setUserId(cart.getUserId());
            //无论是否想买都选中
            cartNew.setSelected(Constant.Cart.CHECKED);
            cartMapper.updateByPrimaryKeySelective(cartNew);
        }
        return this.list(userId);
    }

    @Override
    public List<CartVO> delete(Integer userId, Integer productId){
        //86.去service层中写更新方法
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        if (cart==null){
            //这个商品之前不在购物车里,无法更新
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }else {
            //这个商品已经在购物车里了,则可以删除
            cartMapper.deleteByPrimaryKey(cart.getId());
        }
        return this.list(userId);
    }
======================================================================
127.0.0.1:8083/cart/update?productId=22&count=1
com/imooc/mall/service/CartService.java
package com.imooc.mall.service;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.model.request.AddCategoryReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.model.vo.CategoryVO;

import java.util.List;

/**
 * 79.购物车service 增加CartServiceImpl
 */
public interface CartService {

    //87.补全list实现类
    List<CartVO> list(Integer userId);

    List<CartVO> add(Integer userId, Integer productId, Integer count);

    List<CartVO> update(Integer userId, Integer productId, Integer count);

    List<CartVO> delete(Integer userId, Integer productId);
}

浅谈@RequestParam、@RequestBody、@PathVariable - 知乎 (zhihu.com)

选中购物车相关接口

com/imooc/mall/controller/CartController.java
 @PostMapping("/select")
    @ApiOperation("选择/不选择购物车的某商品")
    public ApiRestResponse select(@RequestParam Integer productId, @RequestParam Integer selected){
        //88.选/不选购物车某商品
        //不能传入userId, cartId, 否则可以删除别人的购物车
        List<CartVO> cartVOList = cartService.selectOrNot(UserFilter.currentUser.getId(), productId,selected);
        return ApiRestResponse.success(cartVOList);
    }

    @PostMapping("/selectAll")
    @ApiOperation("全选择/全不选择购物车的某商品")
    public ApiRestResponse selectAll(@RequestParam Integer selected){
        //89.全选/全不选购物车某商品
        //不能传入userId, cartId, 否则可以删除别人的购物车
////90.订单模块接口编写 91创建OrderController.java  OrderService.java  OrderServiceImpl.java
        List<CartVO> cartVOList = cartService.selectAllOrNot(UserFilter.currentUser.getId(), selected);
        return ApiRestResponse.success(cartVOList);
    }
com/imooc/mall/service/impl/CartServiceImpl.java
    @Override
    public List<CartVO> selectOrNot(Integer userId, Integer productId, Integer selected){
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        if (cart==null){
            //88.这个商品之前不在购物车里,无法选择/不选中
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }else {
            //这个商品已经在购物车里了,则可以选中/不选中
            cartMapper.selectOrNot(userId, productId, selected);
        }//返回购物车列表
        return this.list(userId);
    }

    @Override
    public List<CartVO> selectAllOrNot(Integer userId, Integer selected){
        //89.这里填null是对mapper中的sql进行等于null判断的语句 改变选中状态
        cartMapper.selectOrNot(userId, null, selected);
        return this.list(userId);
    }
com/imooc/mall/service/CartService.java
package com.imooc.mall.service;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.model.request.AddCategoryReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.model.vo.CategoryVO;

import java.util.List;

/**
 * 79.购物车service 增加CartServiceImpl
 */
public interface CartService {

    //87.补全list实现类
    List<CartVO> list(Integer userId);

    List<CartVO> add(Integer userId, Integer productId, Integer count);

    List<CartVO> update(Integer userId, Integer productId, Integer count);

    List<CartVO> delete(Integer userId, Integer productId);

    List<CartVO> selectOrNot(Integer userId, Integer productId, Integer selected);

    List<CartVO> selectAllOrNot(Integer userId, Integer selected);
}

总结购物车模块

  • 重难点
    • MyBatis返回非标准对象、后期计算单样商品的总价 [数据库无法直接查到]
    • 添加商品到购物车时,根据是否已经存在该商品,有不同逻辑
  • 常见错误:不做越权判断 [不允许前端传入Id]

创建订单接口 - 主流程框架搭建

  • 登录 → 浏览商品 → 加入购物车 → 下单
    • ​ → 取消订单
    • ​ → 扫码支付 → 发货 → 收获 → 订单完结

生成订单 —— 用户下单

  • 入参
  • 从购物车中查找已经勾选的商品
  • 判断商品是否正在售卖中
  • 判断库存,保证不超卖,扣库存
  • 数据库事务(实操演示效果)
  • 删除购物车中对应的商品
  • 生成订单
  • 订单号生成规则
  • 循环保存每个商品到order_item表
  • 进入Coding阶段
com/imooc/mall/controller/OrderController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.service.OrderService;
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.RestController;

/**
 * 91.订单Controller 新建一个根据pojo的Order → CreateOrderReq 新建一个OrderService
 */
@RestController
public class OrderController {
    @Autowired
    OrderService orderService;

    @PostMapping("order/create")
    public ApiRestResponse create(@RequestBody CreateOrderReq createOrderReq){
        return null;
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.filter.UserFilter;
import com.imooc.mall.model.dao.CartMapper;
import com.imooc.mall.model.dao.CategoryMapper;
import com.imooc.mall.model.dao.ProductMapper;
import com.imooc.mall.model.pojo.Cart;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.service.CartService;
import com.imooc.mall.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

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

/**
 * 91.★★ 订单Service实现类 ★★
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    CartService cartService;

    public String create(CreateOrderReq createOrderReq) {
        //拿到用户ID
        Integer userId = UserFilter.currentUser.getId();
        //从购物车查找已经勾选的商品 CartVO里面包含着任何信息
        List<CartVO> cartVOList = cartService.list(userId);
        //勾选的单独拿出来
        ArrayList<CartVO> cartVOListTemp = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO = cartVOList.get(i);
            if (cartVO.getSelected().equals(Constant.Cart.CHECKED)) {
                cartVOListTemp.add(cartVO);
            }
        }
        cartVOList = cartVOListTemp;
        //如果购物车已勾选的为空,报错
        if (CollectionUtils.isEmpty(cartVOList)){
            throw new ImoocMallException(ImoocMallExceptionEnum.CART_EMPTY);
        }
    }
    //判断商品是否存在、上下架状态、库存
    //把购物车对象转换为订单item对象
    //扣库存
    //把购物车中的已勾选商品删除
    //生成订单
    //生成订单号,有独立的规则
    //循环保存每个商品的order_item表
    //把结果返回
}
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
package com.imooc.mall.exception;

/**
 * 异常枚举
 */
//13.编写异常枚举 注意类是enum噢  14返回ApiRestResponse
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    NEED_ADMIN(10009,"无管理员权限"),
    NAME_NOT_NULL(10010,"参数不能为空"),
    CREATE_FAILED(10011,"新增失败"),
    REQUEST_PARAM_ERROR(10012,"参数错误"),
    DELETE_FAILED(10013,"删除失败"),
    MKDIR_FAILED(10014,"文件夹创建失败"),
    UPLOAD_FAILED(10015,"图片上传失败"),
    NOT_SALE(10016,"商品状态不可售"),
    NOT_ENOUGH(10017,"商品库存不足"),
    CART_EMPTY(10018, "购物车已勾选的商品为空"),
    SYSTEM_ERROR(20000,"系统异常");


    //异常码
    Integer code;
    //异常信息
    String msg;

    ImoocMallExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

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

创建订单 —— 对象转换与扣库存

com/imooc/mall/controller/OrderController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.service.OrderService;
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.RestController;

/**
 * 91.订单Controller 新建一个根据pojo的Order → CreateOrderReq 新建一个OrderService
 */
@RestController
public class OrderController {
    @Autowired
    OrderService orderService;

    @PostMapping("order/create")
    @ApiOperation("创建订单")
    public ApiRestResponse create(@RequestBody CreateOrderReq createOrderReq){
        //95.调用orderService
        String orderNo = orderService.create(createOrderReq);
        return ApiRestResponse.success(orderNo);
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.filter.UserFilter;
import com.imooc.mall.model.dao.*;
import com.imooc.mall.model.pojo.Order;
import com.imooc.mall.model.pojo.OrderItem;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.service.CartService;
import com.imooc.mall.service.OrderService;
import com.imooc.mall.util.OrderCodeFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

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

/**
 * 91.★★ 订单Service实现类 ★★
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    CartService cartService;
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CartMapper cartMapper;
    @Autowired
    OrderMapper orderMapper;
    @Autowired
    OrderItemMapper orderItemMapper;
    @Override
    public String create(CreateOrderReq createOrderReq) {
        //拿到用户ID
        Integer userId = UserFilter.currentUser.getId();
        //从购物车查找已经勾选的商品 CartVO里面包含着任何信息
        List<CartVO> cartVOList = cartService.list(userId);
        //勾选的单独拿出来
        ArrayList<CartVO> cartVOListTemp = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO = cartVOList.get(i);
            if (cartVO.getSelected().equals(Constant.Cart.CHECKED)) {
                cartVOListTemp.add(cartVO);
            }
        }
        cartVOList = cartVOListTemp;
        //如果购物车已勾选的为空,报错
        if (CollectionUtils.isEmpty(cartVOList)){
            throw new ImoocMallException(ImoocMallExceptionEnum.CART_EMPTY);
        }
        //判断商品是否存在、上下架状态、库存
        validSaleStatusAndStock(cartVOList);
        //把购物车对象转换为订单item对象
        List<OrderItem> orderItemList = cartVOListToOrderItemList(cartVOList);
        //扣库存
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            Product product = productMapper.selectByPrimaryKey(orderItem.getProductId());
            int stock = product.getStock() - orderItem.getQuantity();
            if (stock < 0){
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
            }
            product.setStock(stock);
            productMapper.updateByPrimaryKeySelective(product);
        }
        //把购物车中的已勾选商品删除
        cleanCart(cartVOList);
        //生成订单
        Order order = new Order();
        //93.创建一个Util/OrderCodeFactory
        //生成订单号,有独立的规则
        String orderNo = OrderCodeFactory.getOrderCode(Long.valueOf(userId));
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setTotalPrice(totalPrice(orderItemList));
        order.setReceiverName(createOrderReq.getReceiverName());
        order.setReceiverMobile(createOrderReq.getReceiverMobile());
        order.setReceiverAddress(createOrderReq.getReceiverAddress());
        order.setOrderStatus(Constant.OrderStatusEnum.NOT_PAID.getCode()); //94.去Constant定义订单状态
        //循环保存每个商品的order_item表
        order.setPostage(0);
        order.setPaymentType(1);
        //插入到Order表
        orderMapper.insertSelective(order);
        //循环保存每个商品到order_item表
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            orderItem.setOrderNo(order.getOrderNo());
            orderItemMapper.insertSelective(orderItem);
        }
        //把结果返回
        return orderNo;
        //95.回到Controller调用
    }
//92.生成相似代码并改造
    private void validSaleStatusAndStock(List<CartVO> cartVOList) {
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            Product product = productMapper.selectByPrimaryKey(cartVO.getProductId());
            //判断商品是否存在,商品是否上架
            if (product == null || product.getStatus().equals(Constant.SaleStatus.NOT_SALE)) {
                //82. 1是上架 1不明确要形成代码让顾客知道 Constant.java
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_SALE);
            }
            //判断商品库存 如果要买的比库存多 就买不了
            if (cartVO.getQuantity() > product.getStock()) {
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
            }
        }
    }
    private List<OrderItem> cartVOListToOrderItemList(List<CartVO> cartVOList) {
        List<OrderItem> orderItemList = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            OrderItem orderItem = new OrderItem();
            orderItem.setProductId(cartVO.getProductId());
            //记录商品快照信息
            orderItem.setProductName(cartVO.getProductName());
            orderItem.setProductImg(cartVO.getProductImage());
            orderItem.setUnitPrice(cartVO.getPrice());
            orderItem.setQuantity(cartVO.getQuantity());
            orderItem.setTotalPrice(cartVO.getTotalPrice());
            orderItemList.add(orderItem);
        }
        return orderItemList;
    }
    private void cleanCart(List<CartVO> cartVOList){
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            cartMapper.deleteByPrimaryKey(cartVO.getId());

        }
    }
    private Integer totalPrice(List<OrderItem> orderItemList){
        Integer totalPrice = 0;
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
}
com/imooc/mall/util/OrderCodeFactory.java
package com.imooc.mall.util;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * 描述:     生成订单No工具类
 */
public class OrderCodeFactory {

    /**
     * 订单类别头
     */
    private static final String ORDER_CODE = "1";
    /**
     * 随机编码
     */
    private static final int[] r = new int[]{7, 9, 6, 2, 8, 1, 3, 0, 5, 4};
    /**
     * 用户id和随机数总长度
     */
    private static final int maxLength = 5;

    /**
     * 更具id进行加密+加随机数组成固定长度编码
     */
    private static String toCode(Long id) {
        String idStr = id.toString();
        StringBuilder idSb = new StringBuilder();
        for (int i = idStr.length() - 1; i >= 0; i--) {
            idSb.append(r[idStr.charAt(i) - '0']);
        }
        return idSb.append(getRandom(maxLength - idStr.length())).toString();
    }

    /**
     * 生成时间戳
     */
    private static String getDateTime() {
        DateFormat sdf = new SimpleDateFormat("HHmmss");
        return sdf.format(new Date());
    }

    /**
     * 生成固定长度随机码
     *
     * @param n 长度
     */
    private static long getRandom(long n) {
        long min = 1, max = 9;
        for (int i = 1; i < n; i++) {
            min *= 10;
            max *= 10;
        }
        long rangeLong = (((long) (new Random().nextDouble() * (max - min)))) + min;
        return rangeLong;
    }

    /**
     * 生成不带类别标头的编码
     */
    private static synchronized String getCode(Long userId) {
        userId = userId == null ? 10000 : userId;
        return getDateTime() + toCode(userId);
    }

    /**
     * 生成订单单号编码
     */
    public static String getOrderCode(Long userId) {
        return ORDER_CODE + getCode(userId);
    }
}
com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

import com.google.common.collect.Sets;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Set;


/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
@Component //让spring帮注入value
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
    public static String FILE_UPLOAD_DIR;
    //64.为了解决上传图片系统异常报错 注入失败的原因是上方是static普通变量 set方法把静态变量赋值
    @Value("${file.upload.dir}")
    public void setFileUploadDir(String fileUploadDir){
        FILE_UPLOAD_DIR = fileUploadDir;
    }//65.打开ImoocMallWebMvcConfig 加一个映射规则

    //74.排序处理 去Constant定义支持的排序模式和手段
    public interface ProductListOrderBy{
        Set<String> PRICE_ASC_DESC = Sets.newHashSet("price desc","price asc");
    }
    //83.创建商品上下架的状态 创建CartMapper[Cart selectCartByUserIdAndProductId] 写sql 84再去CartServiceImpl
    public interface SaleStatus{
        int NOT_SALE = 0; //商品下架状态
        int SALE = 1; //商品上架状态
    }
    public interface Cart{
        int UN_CHECKED = 0; //购物车未选中状态
        int CHECKED = 1; //购物车选中状态
    }
    public enum OrderStatusEnum{
        CANCELED(0, "用户已取消"),
        NOT_PAID(10, "未付款"),
        PAID(20, "已付款"),
        DELIVERED(30, "已发货"),
        ;

        private String value;
        private int code;
        //94.去Constant定义订单状态
        OrderStatusEnum(int code,String value){
            this.value = value;
            this.code = code;
        }
        public static OrderStatusEnum codeOf(int code){
            for (OrderStatusEnum orderStatusEnum : values()){
                if (orderStatusEnum.getCode() == code){
                    return orderStatusEnum;
                }
            }
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ENUM);
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        public int getCode() {
            return code;
        }

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

添加数据库事务

//95.写数据库事务代码 @Transactional (要么全对全要 要么全不要)
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String create(CreateOrderReq createOrderReq) {
     ......
    }

订单详情

com/imooc/mall/service/impl/OrderServiceImpl.java
/* 95.继续实现Service实现类 拷贝一个Order成OrderVo 新增两个属性
        private String orderStatusName;
        private List<OrderItemVo> orderItemVoList;
        因为没有<OrderItemVo>要根据需求去创建
        拷贝OrderItem成OrderItemVO 更改属性
        之后回来补写代码
     */
    public OrderVO detail(String orderNo){

    }
com/imooc/mall/model/vo/OrderItemVO.java
public class OrderItemVO {
    private Integer id;

    private String orderNo;

    private Integer productId;

    private String productName;

    private String productImg;

    private Integer unitPrice;

    private Integer quantity;

    private Integer totalPrice;
com/imooc/mall/model/vo/OrderVO.java
public class OrderVO {
    private Integer id;

    private String orderNo;

    private Integer userId;

    private Integer totalPrice;

    private String receiverName;

    private String receiverMobile;

    private String receiverAddress;

    private Integer orderStatus;

    private Integer postage;

    private Integer paymentType;

    private Date deliveryTime;

    private Date payTime;

    private Date endTime;

    private Date createTime;

    private Date updateTime;
订单状态
  • 0 用户已取消
  • 10 未付款(下单后的初始状态)
  • 20 已付款
  • 30 已发货
  • 40 交易完成
com/imooc/mall/service/impl/OrderServiceImpl.java
 /* 95.继续实现Service实现类 拷贝一个Order成OrderVo 新增两个属性
        private String orderStatusName;
        private List<OrderItemVo> orderItemVoList;
        因为没有<OrderItemVo>要根据需求去创建
        拷贝OrderItem成OrderItemVO 更改属性
        之后回来补写代码
     */
   @Override
    public OrderVO detail(String orderNo){
        //96.去创建mapper的方法后回来补写代码 97写订单controller
        Order order = orderMapper.selectByOrderNo(orderNo);
        //订单不存在,则报错
        if (order == null){
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //订单存在,需要判断所属
        Integer userId = UserFilter.currentUser.getId();
        if (!order.getUserId().equals(userId)) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_YOUR_ORDER);
        }
        OrderVO orderVO = getOrderVo(order);
        return orderVO;
    }

    private OrderVO getOrderVo(Order order) {
        OrderVO orderVO = new OrderVO();
        BeanUtils.copyProperties(order,orderVO);
        //获取订单对应的orderItemVOList
        List<OrderItem> orderItemList = orderItemMapper.selectByOrderNo(order.getOrderNo());
        //OrderItem和OrderItem相比少了几个字段
        List<OrderItemVO> orderItemVOList = new ArrayList<>();
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem = orderItemList.get(i);
            OrderItemVO orderItemVO = new OrderItemVO();
            BeanUtils.copyProperties(orderItem, orderItemVO);
            orderItemVOList.add(orderItemVO);
        }
        orderVO.setOrderItemVOList(orderItemVOList);
        //com/imooc/mall/common/Constant.java的列举枚举
        orderVO.setOrderStatusName(Constant.OrderStatusEnum.codeOf(orderVO.getOrderStatus()).getValue());
        return orderVO;
    }
com/imooc/mall/model/dao/OrderMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Order;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Order record);

    int insertSelective(Order record);

    Order selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Order record);

    int updateByPrimaryKey(Order record);

    Order selectByOrderNo(String orderNo);
}

=====================================================
 <select id="selectByOrderNo" resultMap="BaseResultMap" parameterType="java.lang.String">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_order
    where order_no = #{orderNo}
  </select>
com/imooc/mall/controller/OrderController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.model.vo.OrderVO;
import com.imooc.mall.service.OrderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 91.订单Controller 新建一个根据pojo的Order → CreateOrderReq 新建一个OrderService
 */
@RestController
public class OrderController {
    @Autowired
    OrderService orderService;

    @PostMapping("order/create")
    @ApiOperation("创建订单")
    public ApiRestResponse create(@RequestBody CreateOrderReq createOrderReq){
        //95.调用orderService
        String orderNo = orderService.create(createOrderReq);
        return ApiRestResponse.success(orderNo);
    }

    @GetMapping("order/detail")
    @ApiOperation("前台订单详情")
    public ApiRestResponse detail(@RequestParam String orderNo){
        //95.调用orderService  Service层具体实现
        OrderVO orderVO = orderService.detail(orderNo);
        return ApiRestResponse.success(orderVO);
    }
}
==========================================
生成订单详情 GET:127.0.0.1:8083/order/detail?orderNo=100233359639

订单列表

com/imooc/mall/controller/OrderController.java
//97.写一个订单列表controller
    @GetMapping("order/list")
    @ApiOperation("前台订单列表")
    public ApiRestResponse list(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        //95.调用list   Service  Service层具体实现
        PageInfo pageInfo = orderService.listForCustomer(pageNum, pageSize);
        return  ApiRestResponse.success(pageInfo);
    }
===============================================
订单列表:GET:127.0.0.1:8083/order/list?pageNum=1&pageSize=10
com/imooc/mall/service/impl/OrderServiceImpl.java
  //97.前台订单实现类 搞个OrderMapper中的selectForCustomer
    @Override
    public PageInfo listForCustomer(Integer pageNum, Integer pageSize){
        Integer userId = UserFilter.currentUser.getId();
        PageHelper.startPage(pageNum, pageSize);
        List<Order> orderList = orderMapper.selectForCustomer(userId);
        List<OrderVO> orderVOList = orderListToOrderVOList(orderList);
        //新建一个pageinfo返回 方便前端查看
        PageInfo pageInfo = new PageInfo<>(orderList);
        pageInfo.setList(orderVOList);
        return pageInfo;
    }

    private List<OrderVO> orderListToOrderVOList(List<Order> orderList) {
        List<OrderVO> orderVOList = new ArrayList<>();
        for (int i = 0; i < orderList.size(); i++) {
            Order order =  orderList.get(i);
            OrderVO orderVO = getOrderVo(order);
            orderVOList.add(orderVO);
        }
        return  orderVOList;
    }
com/imooc/mall/model/dao/OrderMapper.java
 List<Order> selectForCustomer(Integer userId);
===============================================
mappers/OrderMapper.xml
  <select id="selectForCustomer" resultMap="BaseResultMap" parameterType="integer">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_order
    where user_id = #{userId}
    order by create_time desc
  </select>

取消订单

com/imooc/mall/controller/OrderController.java
//98.写一个订单取消controller
    @PostMapping("order/cancel")
    @ApiOperation("前台取消订单")
    public ApiRestResponse cancel(@RequestParam String orderNo){
        orderService.cancel(orderNo);
        return  ApiRestResponse.success();
    }
com/imooc/mall/service/impl/OrderServiceImpl.java
//98.写一个订单取消实现类
    @Override
    public void cancel(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null){
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //验证用户身份
        //订单存在,需要判断所属
        Integer userId = UserFilter.currentUser.getId();
        if (!order.getUserId().equals(userId)) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_YOUR_ORDER);
        }
        //没有付款才可以取消
        if (order.getOrderStatus().equals(Constant.OrderStatusEnum.NOT_PAID.getCode())) {
            order.setOrderStatus(Constant.OrderStatusEnum.CANCELED.getCode());
            order.setEndTime(new Date());
            //更新状态
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }

二维码接口开发

在线生成 QR Code (oschina.net)
Springboot 上传图片到项目路径下不能访问,需要重启_springboot能上传图片到win11中但无法访问图片-CSDN博客

com/imooc/mall/controller/OrderController.java
//99.生成支付二维码
    @GetMapping("order/qrcode")
    @ApiOperation("前台取消订单")
    public ApiRestResponse qrcode(@RequestParam String orderNo){
        orderService.cancel(orderNo);
        return  ApiRestResponse.success();
    }
com/imooc/mall/service/impl/OrderServiceImpl.java
 //99.生成支付二维码 pom.xml生成新的二维码依赖 util新建一个QRCodeGenerator类
       @Override
    public String qrcode(String orderNo) {
        //得到请求相关的信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();//不能直接拿到内网的信息 要经过多层转发才可以
        //去application.properties配置一下可以访问的ip  去上面String ip
        String address = ip + ":" + request.getLocalPort();//拼接一下 这下生成的是整个地址
        String payUrl = "http://" + address + "/pay?orderNo=" + orderNo;
        System.out.println(address);
        System.out.println(payUrl);
        try {
            QRCodeGenerator.generateQRCodeImage(payUrl, 350, 350, Constant.FILE_UPLOAD_DIR + orderNo + ".png");
        } catch (WriterException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        //这个图片可以通过这个url访问到
        String pngAddress = "http://" + address + "/images/" + orderNo + ".png";
        return pngAddress;
    }
com/imooc/mall/util/QRCodeGenerator.java
package com.imooc.mall.util;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;

/**
 * 99.生成二维码工具
 */
public class QRCodeGenerator {
    public static void generateQRCodeImage(String text, int width, int height, String filePath) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height);
        Path path = FileSystems.getDefault().getPath(filePath);
        MatrixToImageWriter.writeToPath(bitMatrix,"PNG",path);
    }

    public static void main(String[] args) {
        try {
            generateQRCodeImage("PANCHUNYAO", 350,350,"/Users/Pluminary/Desktop/idea_Space/mall_file_upload/QRTest.png");
        } catch (WriterException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
application.properties
file.upload.dir=/Users/Pluminary/Desktop/idea_Space/mall_file_upload/
file.upload.ip=127.0.0.1
com/imooc/mall/service/impl/OrderServiceImpl.java
/**
 * 91.★★ 订单Service实现类 ★★
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    CartService cartService;
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CartMapper cartMapper;
    @Autowired
    OrderMapper orderMapper;
    @Autowired
    OrderItemMapper orderItemMapper;
    @Value("${file.upload.ip}")
    String ip;
............
}

后台订单列表接口

com/imooc/mall/controller/OrderAdminController.java
package com.imooc.mall.controller;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.service.OrderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 100.订单后台管理Controller
 */
@RestController
public class OrderAdminController {
    @Autowired
    OrderService orderService;

    @GetMapping("admin/order/list")
    @ApiOperation("管理员订单列表")
    public ApiRestResponse listForAdmin(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        PageInfo pageInfo = orderService.listForAdmin(pageNum, pageSize);
        return ApiRestResponse.success(pageInfo);
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
 //100.实现类
    @Override
    public PageInfo listForAdmin(Integer pageNum, Integer pageSize) {//想看到所有订单
        PageHelper.startPage(pageNum, pageSize);
        List<Order> orderList = orderMapper.selectAllForAdmin();
        List<OrderVO> orderVOList = orderListToOrderVOList(orderList);
        //新建一个pageinfo返回 方便前端查看
        PageInfo pageInfo = new PageInfo<>(orderList);
        pageInfo.setList(orderVOList);
        return pageInfo;
    }
com/imooc/mall/model/dao/OrderMapper.java
List<Order> selectAllForAdmin();
===========================================
mappers/OrderMapper.xml 
<select id="selectAllForAdmin" resultMap="BaseResultMap" parameterType="integer">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_order
    order by create_time desc
  </select>

支付

根据订单号生成支付二维码 → 扫码支付 → 访问支付URL, 完成支付
com/imooc/mall/controller/OrderController.java
 //101.支付接口
    @GetMapping("pay")
    @ApiOperation("支付接口")
    public ApiRestResponse pay(@RequestParam String orderNo){
        orderService.pay(orderNo);
        return ApiRestResponse.success();
    }
com/imooc/mall/service/impl/OrderServiceImpl.java
//101.支付
    @Override
    public void pay(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //支付前的判断 未付款才可以付款
        if (order.getOrderStatus() == Constant.OrderStatusEnum.NOT_PAID.getCode()) {
            order.setOrderStatus(Constant.OrderStatusEnum.PAID.getCode());
            order.setPayTime(new Date());
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }

完结订单

com/imooc/mall/controller/OrderAdminController.java
package com.imooc.mall.controller;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.service.OrderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 100.订单后台管理Controller
 */
@RestController
public class OrderAdminController {
    @Autowired
    OrderService orderService;

    @GetMapping("admin/order/list")
    @ApiOperation("管理员订单列表")
    public ApiRestResponse listForAdmin(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        PageInfo pageInfo = orderService.listForAdmin(pageNum, pageSize);
        return ApiRestResponse.success(pageInfo);
    }

    /**
     * 102.发货。订单状态流程:0用户已取消,10未付款,20已付款,30已发货,40交易完成
     */
    @GetMapping("admin/order/delivered")
    @ApiOperation("管理员发货")
    public ApiRestResponse delivered(@RequestParam String orderNo){
        orderService.deliver(orderNo);
        return ApiRestResponse.success();
    }

    /**
     * 103.完结订单。订单状态流程:0用户已取消,10未付款,20已付款,30已发货,40交易完成
     * 管理员和用户都可以调用
     */
    @GetMapping("order/finish")
    @ApiOperation("完结订单")
    public ApiRestResponse finish(@RequestParam String orderNo){
        orderService.finish(orderNo);
        return ApiRestResponse.success();
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
 //102.管理订单实现类开发 发货
    @Override
    public void deliver(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //支付前的判断 判断已付款的
        if (order.getOrderStatus() == Constant.OrderStatusEnum.PAID.getCode()) {
            order.setOrderStatus(Constant.OrderStatusEnum.DELIVERED.getCode());
            order.setDeliveryTime(new Date());
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }
    //103.完结订单
    @Override
    public void finish(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //有可能管理员调用 有可能用户调用 如果是普通用户, 要去校验一下订单所属
        if (!userService.checkAdminRole(UserFilter.currentUser) && !order.getUserId().equals(UserFilter.currentUser.getId())) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_YOUR_ORDER);
        }
        //支付前的判断 判断已付款的 //发货后可以完结订单
        if (order.getOrderStatus() == Constant.OrderStatusEnum.DELIVERED.getCode()) {
            order.setOrderStatus(Constant.OrderStatusEnum.FINISHED.getCode());
            order.setEndTime(new Date());
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }

全流程测试

  • Postman实操
  • 登录 → 浏览商品 → 加入购物车 → 下单
    • ​ → 取消订单
    • ​ → 扫码支付 → 发货 → 收获 → 订单完结
总结订单模块
  • 重难点

    • VO的封装、裁剪
    • 一个订单内包括多个商品
    • 订单状态流转
    • 二维码生成
  • 常见错误:把POJO返回到前端

上线前准备工作

阿里云部署

把每个 com/imooc/mall/model/request/… 类都加上**toString()**方法 方便调试
后台、前台、获取类的行为 的目录列表都是 GetMapping

前端准备工作

把static里的前端文件导入项目中
com/imooc/mall/config/ImoocMallWebMvcConfig.java
//104.ImoocMallWebMvcConfig.java 根据路由配置相关文件 以admin开头的文件会被路由到下面的Locations
package com.imooc.mall.config;

import com.imooc.mall.common.Constant;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 42.配置地址映射 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
 * 44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
    public void addResourceHandles(ResourceHandlerRegistry registry){
        //104.根据路由配置相关文件 以admin开头的文件会被路由到下面的Locations
        registry.addResourceHandler("/admin/**").addResourceLocations("classpath:/static/admin");
        //65.增加一个registry  66.新增接口继续开发 ProductAdminController
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + Constant.FILE_UPLOAD_DIR);
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}

部署云服务器

  • 阿里云简介
  • 选择云服务器并购买
  • 环境配置
  • Spring Boot部署[包括maven打包 sql文件也要上传]

云服务器管理控制台 (aliyun.com)

免费试用三个月
账号: root
密码: panchunyao123!

复制服务器的公网ip:47.98.225.105
cmd本地电脑 → ssh root@47.98.225.105

在阿里云购买Linux服务器,配置宝塔环境,全图文,最最详细图解,保姆级教学_阿里云服务器怎么直接买宝塔-CSDN博客

【宝塔安装jdk1.8(yum安装)】

1、检索检索1.8的列表

yum list java-1.8*

2、安装1.8.0的所有文件

yum install java-1.8.0-openjdk* -y

3、使用命令检查是否安装成功

java -version

登录宝塔 → 绑定宝塔 → 下载软件
宝塔服务器[http://47.98.225.105:22003/]

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆ 宝塔BaoTa ☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
========================面板账户登录信息=========================
 外网面板地址: http://47.98.225.105:22003/1259ebfa
 内网面板地址: http://172.24.91.49:22003/1259ebfa
 username: grltvl7d
 password: 46303f93


======================== mysql-5.7 ===========================
root密码成功修改为: Panchunyao123![mysql-8.0]

[root@iZbp1dssknxftmjczbtpndZ ~]# mysql -uroot -pPanchunyao123!
mysql> create database imooc_mall;
mysql> use imooc_mall;
Database changed
导入sql文件:imooc_mall_online.sql
从本机将其上传到服务器在cmd中运行:
scp /Users/Pluminary/Desktop/imooc_mall_online.sql root@47.98.225.105:22003:/root
输入服务器密码:panchunyao123!

回到服务器端输入:
mysql> use imooc_mall
Database changed
mysql> source /root/imooc_mall_online.sql
mysql> show tables;
+-----------------------+
| Tables_in_imooc_mall  |
+-----------------------+
| imooc_mall_cart       |
| imooc_mall_category   |
| imooc_mall_order      |
| imooc_mall_order_item |
| imooc_mall_product    |
| imooc_mall_user       |
+-----------------------+
#为数据库赋予权限!!!
mysql> grant all privileges on imooc_mall.* to 'root'@'127.0.0.1' identified by 'Panchunyao123!';
--------------------------------------------------------------
MySql8.0
CREATE USER 'root'@'47.98.225.105' IDENTIFIED BY 'Panchunyao123!';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'47.98.225.105' WITH GRANT OPTION;
--------------------------------------------------------------
mysql> quit
Bye
[root@iZbp1dssknxftmjczbtpndZ ~]# mysql -uroot -pPanchunyao123! -h127.0.0.1
去云服务器 安全组 访问规则 入方向 手动添加一个 目的:8081/8081 源:0.0.0.0/0
==============================================================

整理文件上传到服务器 先**Build → Rebuild Project **
打包 Maven → Lifecycle → clean[之前的内容删除 不容易出错] → package[打包操作]
spring打包报错:Java Runtime (class file version 55.0), class file versions up to 52.0_there was an error in the forked process org/testn-CSDN博客

[INFO] Building jar: C:\Users\Pluminary\Desktop\idea_Space\mall\target\mall-0.0.1-SNAPSHOT.jar
在本机cmd中运行把导好的包发送給服务器
C:\Users\Pluminary>scp C:\Users\Pluminary\Desktop\idea_Space\mall\target\mall-0.0.1-SNAPSHOT.jar root@c:/root
#rm -rf images/ 删除images文件夹
新建一个文件夹
[root@iZbp1dssknxftmjczbtpndZ ~]# pwd
[root@iZbp1dssknxftmjczbtpndZ ~]# mkdir image

图片上传到服务器中[图片尽量在没有根目录空格的文件夹下 比如Spring Boot 中间有空格]
C:\Users\Pluminary>scp C:/Users/Pluminary/Desktop/images/. root@47.98.225.105:/root/images/

在Xshell7中阿里云服务器
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
images  imooc_mall_online.sql  install.sh  mall-0.0.1-SNAPSHOT.jar

每一次部署之前要把当前的java停止掉
[root@iZbp1dssknxftmjczbtpndZ images]# lsof -i:8081
[root@iZbp1dssknxftmjczbtpndZ images]# kill -9 12345 //按照PID杀死端口号
-bash: kill: (12345) - No such process

部署到云服务器 [/root/null 2>&1 &]将日志输出到哪里 这里是丢弃的意思
[root@iZbp1dssknxftmjczbtpndZ ~]# nohup java -jar -Dserver.port=8081 -Dspring.profiles.active=prod /root/mall-0.0.1-SNAPSHOT.jar > /root/null 2>&1 &
[1] 18656
如果没有别名就一个本来的application.properties
[root@iZbp1dssknxftmjczbtpndZ ~]# nohup java -jar -Dserver.port=8081 /root/mall-0.0.1-SNAPSHOT.jar > /root/null 2>&1 &


 #代表当前程序的进程号
[root@iZbp1dssknxftmjczbtpndZ ~]# lsof -i:8081   //查询端口号
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    18656 root   19u  IPv6 195648      0t0  TCP *:tproxy (LISTEN)

============================================================================

若打不开请检查网页报错 寻找传入的application.prod.properties的 
spring.datasource.url地址和spring.datasource.username && password数据库用户和密码

查看服务器的端口: mysql> show global variables like 'port';

阿里云控制台:
https://ecs.console.aliyun.com/server/i-bp1dssknxftmjczbtpnd/detail?regionId=cn-hangzhou

如何查看linux服务器安装了tomcat:
rpm -qa|grep tomcat

宝塔安装目录: $cd /www/server/.....
[root@iZbp1dssknxftmjczbtpndZ data]# cd /www/server/tomcat/bin
#找到文件后启动tomcat
[root@iZbp1dssknxftmjczbtpndZ bin]# ./startup.sh
#看进程,如果返回一大串东西,说明tomcat启动成功。
[root@iZbp1dssknxftmjczbtpndZ ~]# ps -ef|grep tomcat

运行SpringBoot: 
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
Desktop  images  imooc_mall_online.sql  install.sh  logs  mall-0.0.1-SNAPSHOT.jar  null
[root@iZbp1dssknxftmjczbtpndZ ~]# java -jar mall-0.0.1-SNAPSHOT.jar 

===============================================
当把SpringBoot项目的jar包部署到linux服务器中,启动SpringBoot项目,却无法正常访问,这是怎么回事呢?
主要的原因是端口号被Linux的firewall防火墙拦截掉了
解决办法:
1、查询已开启的端口列表:firewall-cmd --list-ports 
不出意外的话,是啥也没有,这也表示,所有端口都未放行,当然springboot项目的80端口也被拦截掉了
2、firewall-cmd --zone=public --add-port=1-12345/tcp --permanent
将端口1~12345全都开启
3、重启防火墙
service firewalld restart
4、重新运行springboot项目
5、成功访问
===============================================

server.port=8083
spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://47.98.225.105/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=Panchunyao123!
mybatis.mapper-locations=classpath:mappers/*.xml
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
file.upload.dir=/root/images/
file.upload.ip=47.98.225.105
icode=ABCDE

SpringBoot上传图片,图片不能及时显示问题_springboot项目上传照片到uoload 不会实时更新-CSDN博客
Springboot 上传图片到项目路径下不能访问,需要重启_springboot能上传图片到win11中但无法访问图片-CSDN博客
关于springboot项目图片上传到本地,必须重启之后才能访问的解决方案_springboot上传图片需要重启-CSDN博客
关于IDEA2022开启热部署没有compiler.automake.allow.when.app.running的解决方案-CSDN博客
Linux部署SpringBoot项目无法访问问题_linux springboot项目启动外面不能访问-CSDN博客
linux服务器部署SpringBoot项目并查看项目运行日志_linux 怎么实时查看springboot 日志-CSDN博客
线上发布报数据库没有连接成功,麻烦帮忙看…-慕课网 (imooc.com)
linux安装mysql 8 数据库(保姆级)_linux安装mysql8-CSDN博客
慕慕生鲜详细步骤全部打通(从无到上线)-CSDN博客

mysql8.0密码=> root & Panchunyao123!

阅读全文

SSM开发社交网站[Freemarker+Spring+SpringMVC+Mybatis(Plus)构成SSM+Bootstrap+Art-Template+Kaptcha+SpringTask+wangEditor]

2024/1/18

慕课书评网学习内容

SSM => Spring / Spring MVC / MyBatis

  • 讲解Spring/Spring MVC/MyBatis(SSM)整合配置过程
  • 讲解MyBatis-Plus敏捷开发插件的用法
  • 开发Java Web应用”慕课书评网”

主要知识点

  • SSM整合配置
  • MyBatis-Plus配置与应用
  • Kaptcha验证码组件使用
  • 富文本编辑器wangEditor
  • Spring Task任务调度
  • 基于阿里云实现短信验证
  • 基于腾讯云的滑块验证码实现前置的人机登录检查

工程结构与开发规约 [复习知识点]

工程结构
mgallery - eclipse工程项目
 /src - java源代码目录
 /WebContent - Web资源目录
 /css - css文件目录
 /js - js文件目录
 /image - 图片资源目录
 /upload - 上传文件目录
 /WEB-INF   //jsp数据来自controller 不允许在web中直接访问 要从控制器跳转
   /jsp - jsp页面目录
   /lib - jar文件目录
   /classes - 编译后的class目录
   /web.xml web描述符文件
包结构
com.imooc.mgallery //逆命名法
    /controller - 存放Servlet控制器类 //承上启下接收参数 调用逻辑 返回处理结果
    /service - 存放处理逻辑类model[伪数据库] //完成业务逻辑 service与dao进行传递调用
    /dao - Data Access Object 数据访问对象类 数据读写的java类 数据来自xml文件
    /entity - 存放实体类 JavaBean java中的简单对象
    /utils - 通用工具类 底层通用的工具类或方法

什么是整合

  • 通过Spring IoC容器管理第三方框架对象, 让多框架形成整体
  • SSM => Spring / Spring MVC / MyBatis是业内最主流的框架搭配
    • Spring MVC提供了控制器 基于Spring的Web交互的能力
    • Spring是对应用程序的对象进行创建和管理
    • MyBatis是完成了与底层数据库实现增删改查的操作
  • SSM配置与使用是所有Java工程师必须掌握的技能

SSM整合三阶段

  • Spring与Spring MVC环境配置
  • Spring与MyBatis的整合配置
  • 整合其他组件: 声明式事务/日志/任务调度/…

Spring与Spring MVC环境配置

  • 依赖Spring-WebMVC
  • 配置DispatcherServlet
  • 启用Spring MVC注解模式
  • 配置请求与相应字符集
  • 配置FreeMarker模板引擎
  • 配置JSON序列化组件

编写pom.xml 引入导入的依赖jar包
IntelliJ IDEA 2021 自动下载pom文件中的依赖_pom中的自定义插件下载-CSDN博客
idea编译Java项目的部分java文件并且生成class文件_idea将java文件编译成class-CSDN博客

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
设置resource和source
方法:点击文件右键->make direction as

需要运行和引用的java文件设置为source文件,将配置文件(xml)等设置为resource文件
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

pom.xml
<?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>org.example</groupId>
    <artifactId>imooc-reader</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
<!--1.Maven依赖Spring-webmvc 第二步web.xml-->
    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--    Freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
<!--   Jackson依赖包  -->
        <dependency>
            <artifactId>jackson-core</artifactId>
            <groupId>com.fasterxml.jackson.core</groupId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.0</version>
        </dependency>
    </dependencies>
</project>
WEB-INF/web.xml    init-param变量 这个是放在servlet里面的,只有此servlet可以访问
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
<!--    2.配置DispatcherServlet 第三步applicationContext.xml-->
 <!--    DispatcherServlet是Spring MVC是最核心的对象
         DispatcherServlet用于拦截http请求        -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <!--自动加载applicationContext-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext*.xml</param-value>
        </init-param>
<!--        启动时初始化 servlet-->
        <load-on-startup>0</load-on-startup>
    </servlet>

    <servlet-mapping> <!--与上面保持一致-->
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
<!-- 4.解决中文乱码问题  只会对Post请求生效 Get只需要修改tomcat的server.xml(8.0以上是自动的) 72行增加URIEncoding="UTF-8" 第五步application-->
    <filter>
        <filter-name>characterFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param> <!--初始化参数-->
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>characterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/task
            http://www.springframework.org/schema/task/spring-task.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 3.Spring框架启用SpringMVC注解模式 第四步web.xml-->
    <context:component-scan base-package="com.imooc"/>
<!-- 真正开启SpringMVC注解 -->
<!-- https://blog.csdn.net/u011066470/article/details/112438252 -->
<!-- <mvc:annotation-driven> Spring MVC用来提供Controller请求转发,json自动转换等功能。,默认会帮我们注册默认处理请求,参数和返回值的类 -->
    <mvc:annotation-driven>
        <mvc:message-converters>
<!-- 解决相应里的中文输出 -->
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html;charset=utf-8</value>
                        <!-- 6.JSON(Jackson)序列化输出配置   -->
                        <value>application/json;charset=utf-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
<!-- 诸如css/图片/js静态资源排除在外 使得SpringMVC对url处理效率加大   -->
    <mvc:default-servlet-handler/>
<!-- 5.配置Freemarker模板引擎 脚本存放地址 Bean定义可以具有零个或多个属性。属性元素对应于bean类公开的JavaBean setter方法。 Spring支持原语,对相同或相关工厂中的其他bean的引用,列表,映射和属性。 -->
    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/ftl"/>
<!--        对于本身的参数配置  -->
        <property name="freemarkerSettings">
            <props>
<!--        默认的编码类型 UTF-8 freemarker读取指定ftl文件时采用此字符集-->
                <prop key="defaultEncoding">UTF-8</prop>
            </props>
        </property>
    </bean>
<!--    使用何种模板引擎 创建的后缀名 -->
    <bean id="ViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
        <property name="contentType" value="text/html;charset=utf-8"/>
        <property name="suffix" value=".ftl"/>
    </bean>
</beans>
WEB-INF/ftl/test.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
测试页面!!
</body>
</html>
com/imooc/controller/TestController.java
package com.imooc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

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

@Controller
public class TestController {
    @GetMapping("/test/t1")
    public ModelAndView test1(){
        return new ModelAndView("/test");
    }
    @GetMapping("test/t2")
    @ResponseBody
    public Map test2(){
        Map result = new HashMap();
        result.put("test","测试文本");
        return result;
    }
}

IDEA Web项目out/classes目录没有编译的class类文件 - IDEA环境下创建Maven WebApp_out中没有class文件夹-CSDN博客

Spring与MyBatis整合配置

MyBatisUtils封装初始化代码 如果自己new了对象就不会享受spring的福利待遇

  • 依赖mybatis-spring及驱动
  • 配置数据源与连接池
  • 配置SqlSessionFactory
  • 配置Mapper扫描器
  • 创建mybatis-config.xml
applicationContext.xml
<!-- 5.配置Freemarker模板引擎 脚本存放地址 第六步见pom.xml mybatis依赖 -->
    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/ftl"/>
<!--        对于本身的参数配置  -->
        <property name="freemarkerSettings">
            <props>
<!--        默认的编码类型 UTF-8 freemarker读取指定ftl文件时采用此字符集-->
                <prop key="defaultEncoding">UTF-8</prop>
            </props>
        </property>
    </bean>
<!--    使用何种模板引擎 创建的后缀名   -->
    <bean id="ViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
        <property name="contentType" value="text/html;charset=utf-8"/>
        <property name="suffix" value=".ftl"/>
    </bean>
<!--    7. MyBatis与Spring的整合配置   -->
      <!--②.配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/imooc_reader?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="5"/>
        <property name="maxActive" value="20"/>
    </bean>
<!--8. SqlSessionFactoryBean用于根据配置信息创建SqlSessionFactory,不再需要我们自己编码创建-->
    <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
<!--    存储sql语句的mapper.xml文件 存储在哪个目录中 去创建mapper接口-->
        <property name="mapperLocations" value="classpath:mappers/*.xml"/>
<!-- 10.  MyBatis配置文件地址 去创建一个xml  -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
<!--  9. 配置Mapper扫描器 下一步9TestMapper -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--  这样初始化mybatis时 会对所有mapper接口进行扫描 -->
    <property name="basePackage" value="com.imooc.reader.mapper"/>
</bean>
pom.xml
<!--  6. Mybatis整合步骤:①引入依赖 第七步看applicationContext.xml-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.4</version>
        </dependency>
<!--        MyBatis与Spring整合组件 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.14</version>
        </dependency>
com/imooc/reader/mapper/TestMapper.java
package com.imooc.reader.mapper;
//接口作用Mybatis通过接口自动生成实现类
public interface TestMapper {
// 系统中接口很多不是所有的都对应mapper 9.application.xml
    public void insert();
}
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
<!--        驼峰命名转换 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

整合其他组件

  • 配置logback日志输出
  • 声明式事务配置
  • 整合JUnit单元测试
pom.xml
<!--   11. 单元测试依赖 mappers加入test.xml mapper扫描接口后生成对应实现类 12test.xml-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
<!--  15导入依赖 scope打包时不会放进去 tomcat自带 但是测试时不用tomcat所以要加-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
<!--   16加入日志依赖 logback日志组件 17创建logback.xml-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>
mappers/test.xml
<?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.imooc.reader.mapper.TestMapper">
<!--   12. 与接口对应的完整类路径  ↓用于对应数据新增 对应TestMapper.java接口  13TestService -->
    <insert id="insert">
        insert into test(content) values('测试内容')
    </insert>
</mapper>
com/imooc/reader/service/TestService.java
package com.imooc.reader.service;

import com.imooc.reader.mapper.TestMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class TestService {
// 13.运行时注入生成的对象 自动生成Test 选中上面的类 Code generate 14TestServiceTest.java
    @Resource
    private TestMapper testMapper;
//      @Transactional 要么全部完成 要么什么也不做
    @Transactional
    public void batchImport(){
        for (int i = 0; i < 5; i++) {
//            if (i==3) {
//                throw new RuntimeException("预期外异常");
//            }
            testMapper.insert();
        }
    }
}
com/imooc/reader/service/TestServiceTest.java
package com.imooc.reader.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import static org.junit.Assert.*;

//14. 运行时自动初始IoC容器 + 说明配置文件在什么地方完成初始化 15依赖javaservlet pom.xml
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TestServiceTest {
    @Resource
    private TestService testService;

    @Test
    public void batchImport() {
        testService.batchImport();
        System.out.println("批量导入成功");
    }
}
logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--           17.       小时分钟秒   日志级别  线程名   说明何类何方法  对应日志消息+换行-->
<!--            18转至声明式事务 applicationContext上面的文件头加些配置信息 再去下面写-->
            <pattern>%d{HH:mm:ss} %-5level [%thread] %logger{30} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
<!--    输出级别   -->
    <root level="debug">
        <appender-ref ref="console"/>
    </root>
</configuration>
applicationContext.xml
<!--    18.声明式事务配置 控制事务的打开 提交 回滚 ↓-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!--    与之对应的注解模式! 再去TestService.java添加  @Transactional -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
@RunWith作用
@RunWith 就是一个运行器
@RunWith(JUnit4.class) 就是指用JUnit4来运行
@RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境
@RunWith(Suite.class) 的话就是一套测试集合,
@ContextConfiguration Spring整合JUnit4测试时,使用注解引入多个配置文件
单个文件
@ContextConfiguration(Locations=“classpath:applicationContext.xml”)
@ContextConfiguration(classes = SimpleConfiguration.class)

多个文件时,可用{}
@ContextConfiguration(locations = { “classpath:spring1.xml”, “classpath:spring2.xml” })

MyBatis-Plus

MyBatis-Plus (baomidou.com)

  • MyBatis-Plus(简称MP)是一个MyBatis的增强工具
  • 自动实现Mapper CRUD操作, 极致提高数据库开发效率
  • MP在MyBatis的基础上只做增强不做改变

MyBatis-Plus整合三部曲

  • pom引入mybatis-plus依赖
  • Spring XML更改配置SqlSessionFactory实现类
  • mybatis-config.xml增加MP分页插件

MyBatis-Plus开发三部曲

  • 创建实体类, @TableName/ @Tableld/ @TableField实现映射

  • 创建Mapper接口继承BaseMapper, 创建Mapper XML

  • 开发时注入Mapper对象, 通过内置API实现CRUD操作

MyBatis-Plus核心注解

  • @TableName - 将实体类与表名映射
  • @Tableld - 说明对应属性是表的主键
  • @TableField - 设置属性与列名的对应关系

BaseMapper接口核心API

方法名 用途
insert(entity) 数据新增,自动生成insert sql, 根据@Tableld决定注解生成方式
updateById(entity) 根据主键更新对应对象, 自动生成update sql
deleteById(id) 根据主键删除数据, 自动更新delete sql
selectById(id) 按主键查询对应的实体对象
selectList(queryWrapper) 根据查询生成器(QueryWrapper)的条件自动生成sql查询返回List集合
selectPage(page,queryWrapper) 分页查询方法, 自动生成分页limit子句, 返回IPage分页对象
pom.xml
<!--   19 整合Mybatis-plus依赖  修改application中65行   -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
applicationContext.xml  【替换之前的sessionFactory】
<!--  19. 原生Mybatis与Spring整合  自动实现接口的增删改查  20增加plus底层的分页查询插件 mybatis-config.xml-->
<!--    <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">-->
    <bean id="sessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
<!--    存储sql语句的mapper.xml文件 存储在哪个目录中 去创建mapper接口-->
        <property name="mapperLocations" value="classpath:mappers/*.xml"/>
<!-- 10.  MyBatis配置文件地址 去创建一个xml  第十一步pom加单元测试依赖-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
<!--  9. 配置Mapper扫描器  -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--  这样初始化mybatis时 会对所有mapper接口进行扫描 -->
    <property name="basePackage" value="com.imooc.reader.mapper"/>
</bean>
<!--    18.声明式事务配置 控制事务的打开 提交 回滚 ↓-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!--    与之对应的注解模式! 再去TestService.java添加  @Transactional -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
<!--        驼峰命名转换 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <plugins>
<!--  20.配置Mybatis-Plus分页查询     21创entity/Test.java-->
        <plugin interceptor="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor"></plugin>
    </plugins>
</configuration>
com/imooc/reader/entity/Test.java
// 21 Mybatis-plus运用   22mapper中TestMapper
@TableName("test") //说明实体对应哪一张表
public class Test {
    @TableId(type = IdType.AUTO) //数据自增
    @TableField("id") //说明属性对应哪个字段
    private Integer id;
    //如果字段名与属性名相同或者符合驼峰命名转换规则 则TableField可以省略
    @TableField("content")
    private String content;
} Getter + Setter
com/imooc/reader/mapper/TestMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Test;
//22.为了和BaseMapper里的insert()区别开 这个改名成insertSample
//接口作用Mybatis通过接口自动生成实现类 23.MyBatisPlusTest.java
public interface TestMapper extends BaseMapper<Test> {
// 系统中接口很多不是所有的都对应mapper 9.application.xml
    public void insertSample();
}
com/imooc/reader/MyBatisPlusTest.java
package com.imooc.reader;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Test;
import org.junit.runner.RunWith;
import com.imooc.reader.mapper.TestMapper;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;
import java.util.List;

//23.利用插件调用增删改查  24以Bootstrap开发前端index.ftl
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class MyBatisPlusTest {
    @Resource
    private TestMapper testMapper;

    //增
    @org.junit.Test //直接进行调用 与注解重名 故此加包名
    public void testInsert() {
        Test test = new Test();
        test.setContent("MyBatis Plus测试");
        //在接口中 com/imooc/reader/mapper/TestMapper.java
        testMapper.insert(test);
    }

    //改
    @org.junit.Test
    public void testUpdate() {
        Test test = testMapper.selectById(30);//按id号查询
        test.setContent("MyBatis Plus测试1");
        testMapper.updateById(test);
    }

    //删
    @org.junit.Test
    public void testDelete() {
        testMapper.deleteById(30);
    }

    //查
    @org.junit.Test
    public void testSelect() {
        //传入特殊包装对象
        QueryWrapper<Test> queryWrapper = new QueryWrapper<Test>();
//        queryWrapper.eq("id", 31); //eq是等值比较 若写多行语句则sql语言用and连接
        queryWrapper.gt("id", 5);//选择范围 查询大于5的数据
        List<Test> list = testMapper.selectList(queryWrapper);//返回获取7号数据的集合
        System.out.println(list.get(0));
    }
}

基于SSM开发慕课书评网

导入训练素材的imooc-reader.sql

Bootstrap入门介绍 [优秀的前端UI框架]
  • Bootstrap是全球最受欢迎的前端组件库, 由推特(TWitter)开源
  • Bootstrap用于开发响应式布局、移动设备优先的WEB项目
  • Bootstrap提供完整的HTML、CSS和JS开发工具集

Bootstrap中文网 (bootcss.com)

显示图书类别 [全部 | 前端 | 后端 | 测试 | 产品]

index.ftl [只写入顺序关键信息 在这里插入素材中的index.html并更名]
<#--在这块进行动态数据编写 24.创建entity/Category分类实体-->
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
<#--     31.对页面进行动态的数据导入       -->
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
<#--                取消最后一个的竖线-->
                <#if category_has_next>|</#if>
            </#list>
        </div>
com/imooc/reader/entity/Category.java
// 24.图书分类实体 25mapper创建一个CategoryMapper
public class Category {
    @TableId(type = IdType.AUTO)
    private Long categoryId;
// 已自动配置好了驼峰命名法 可以忽略
// @TableField("category_name")
    private String categoryName;
}Getter + Setter + 重写toString
com/imooc/reader/mapper/CategoryMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Category;
//25.BaseMapper自动提供了增删改查的功能 泛型指向Category
// 图书分类Mapper接口 26在mappers中创建一个category.xml
public interface CategoryMapper extends BaseMapper<Category> {

}
mappers/category.xml
<?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.imooc.reader.mapper.CategoryMapper">
<!-- 26.如何使用呢? 27创建一个service/CategoryService接口-->
</mapper>
com/imooc/reader/service/CategoryService.java
package com.imooc.reader.service;
//27.有了service的接口 28就要有实现实现类存放包CategoryServiceImpl

import com.imooc.reader.entity.Category;
import java.util.List;

public interface CategoryService {
    public List<Category> selectAll();
}
com/imooc/reader/service/impl/CategoryServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Category;
import com.imooc.reader.mapper.CategoryMapper;
import com.imooc.reader.service.CategoryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

//28.向接口编程的规则,注入写好的CategoryMapper接口 设置Transactional事务传播
//默认所有方法是不使用事务的 查询较多的方法不使用事务 写入方法较多下写入事务
//29生成测试类 code-generate-Test  com/imooc/reader/service/impl/CategoryServiceImplTest.java
@Service("categoryService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class CategoryServiceImpl implements CategoryService {
    @Resource
    private CategoryMapper categoryMapper;
    /**
     * 查询所有图书分类
     * @return 图书分类List
     */
    @Override
    public List<Category> selectAll() {
        //查询列表 返回多个数据 插入条件构造器[查询所有]
        List<Category> list = categoryMapper.selectList(new QueryWrapper<Category>());
        return list;
    }
}
com/imooc/reader/service/impl/CategoryServiceImplTest.java[Test]
package com.imooc.reader.service.impl;

import com.imooc.reader.entity.Category;
import com.imooc.reader.mapper.CategoryMapper;
import com.imooc.reader.service.CategoryService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import java.util.List;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class CategoryServiceImplTest {
    @Resource
    //29.保证属性和刚才的 @Service("categoryService")一致 重写Category的tostring方法重新运行selectAll
    //30进入controller开发环节 新建url与方法绑定的BookController.java
    private CategoryService categoryService;
    @Test
    public void selectAll() {
        List<Category> list = categoryService.selectAll();
        System.out.println(list);
    }
}
com/imooc/reader/controller/BookController.java
package com.imooc.reader.controller;

import com.imooc.reader.entity.Category;
import com.imooc.reader.service.CategoryService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import java.util.List;

//30.显示之前写好的index.ftl
@Controller
public class BookController {
    //传入模板数据 分类信息
    @Resource
    private CategoryService categoryService;
    //showIndex与url绑定  31继续向前推进因为index.ftl所有数据都是静态写死的
    //31要对每一个分类进行读取 List标签<#list>读取 转至index.ftl 64行
    /**
     * 显示首页
     * @return
     */
    @GetMapping("/")
    public ModelAndView showIndex(){
        ModelAndView mav = new ModelAndView("index");
        List<Category> categoryList = categoryService.selectAll();//拿到列表
        //结果放入其中 属性名,值
        mav.addObject("categoryList",categoryList);
        return mav;
    }
}

----------------------------------------------------------
index.ftl
<#-- 31.对页面进行动态的数据导入 32创建全新实体类Book -->
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
<#--   取消最后一个的竖线 -->
                <#if category_has_next>|</#if>
            </#list>
        </div>

实现图书分页查询

com/imooc/reader/entity/Book.java
// 32.创建完Book实体类  33去创建全新的BookMapper接口[mapper中]
@TableName("book")
public class Book {
    @TableId(type = IdType.AUTO)
    private Long bookId;
    private String bookName;
    private String subTitle;
    private String author;
    private String cover;
    private String description;
    private Long categoryId;
    private Float evaluationScore;
    private Integer evaluationQuantity;
}Getter + Setter
com/imooc/reader/mapper/BookMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Book;
//33.创建完Book接口  34与之对应的创建book.xml文件
public interface BookMapper extends BaseMapper<Book> {
}
mappers/book.xml
<?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.imooc.reader.mapper.BookMapper">
<!--33.完成底层与数据交互代码 34完成分页处理 是在Service中完成的创建BookService完成分页对象查询-->
</mapper>
com/imooc/reader/service/BookService.java
package com.imooc.reader.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
//图书服务
public interface BookService {
    //34.泛型<Book> 说明查询出来的每一行数据都是一个Book对象
    // 瞄准BookService快速生成实现类Alt+Enter => Implement interface
    //35编写com.imooc.reader.service.impl.BookServiceImpl
    public IPage<Book> paging(Integer page, Integer rows);
}
com/imooc/reader/service/impl/BookServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imooc.reader.entity.Book;
import com.imooc.reader.mapper.BookMapper;
import com.imooc.reader.service.BookService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

//35. 实现分类代码编写  36生成测试用例 Code generate Test => BookServiceImplTest
@Service("bookService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class BookServiceImpl implements BookService {
    @Resource
    private BookMapper bookMapper;
    /**
     * 分页查询图书
     * @param page 页号
     * @param rows 每页记录数
     * @return 分页对象
     */
    public IPage<Book> paging(Integer page, Integer rows) {
        Page<Book> p = new Page<Book>(page,rows);
        QueryWrapper<Book> queryWrapper = new QueryWrapper<Book>();
        IPage<Book> pageObject = bookMapper.selectPage(p, queryWrapper);//传入两个参数: page对象哪一页数据  每页记录数[条件构造器]
        return pageObject;
    }
}
com/imooc/reader/service/impl/BookServiceImplTest.java[Test]
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
import com.imooc.reader.service.BookService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import java.util.List;

import static org.junit.Assert.*;
//36.测试分页的代码编写  37去BookController新加一个方法
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class BookServiceImplTest {
    @Resource
    private BookService bookService;
    @Test
    public void paging() {
        //查询每页十条
        IPage<Book> pageObject = bookService.paging(1,10);
        //获取当前页数据
        List<Book> records = pageObject.getRecords();
        for (Book b:records){
            System.out.println(b.getBookId() + ":" + b.getBookName());
        }
        System.out.println("总页数:" + pageObject.getPages());
        System.out.println("总记录数:" + pageObject.getTotal());
    }
}

Ajax动态加载图书信息

com/imooc/reader/controller/BookController.java
//37.编写一个page方法 上面加个@Resource 绑定一个Ajax @GetMapping
    //http://localhost/books 非常长的JSON序列化结果  38index.ftl 83行
    /**  
     * 分页查询图书列表
     * @param p 页号
     * @return 分页对象
     */
package com.imooc.controller;

import com.imooc.reader.entity.Book;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.service.BookService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;

@Controller
public class BookController {
    @Resource
    private BookService bookService;
    
    @GetMapping("/books")
    @ResponseBody //具体数据来自客户端传入
    public IPage<Book> selectBook(Long categoryId, String order, Integer p) {
        if (p == null) {
            p = 1;
        }
        IPage<Book> pageObject = bookService.paging(categoryId, order, p, 10);
        return pageObject;
    }
}

img标签src引用网络图片,响应403的解决方法_img src引用其他网站图片-CSDN博客

index.ftl
  <#--    38.编写Json序列化格式并追加到当前网页 上方以导入Art-Template模板引擎 39将<div id="bookList">下面的代码裁切放到上面 http://localhost/ -->
    <script>
        $(function () {
            $.ajax({
                url: "/books",
                data : {p:1},
                type : "get",
                dataType : "json",
                success: function(json){
                    console.info(json);
                    var list = json.records;//获取当前分页数据
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        //动态将数据组合成html
                        var html = "<li>" + book.bookName + "</li>";
                        //jquery的id选择器选中div对象 追加
                        $("#bookList").append(html);
                    }
                }
            })
        })
    </script>

Art - Template 腾讯JS模板引擎

art-template (aui.github.io)

整段HTML模板化 静态文本不变化 动态的数据用响应表达式进行提取

两组大括号进行数据提取

index.ftl
<meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
    <script src="./resources/raty/lib/jquery.raty.js"></script>
-------------------------------------------------------------------------------------
<#--39.type="text/html"说明当前script块中包含的内容是一段一段的html浏览器不会作为javascript进行解析-->
<#-- ★ ★ ★ id是模板名字 两组大括号进行提取数据 ★ ★ ★ 使用模板引擎简化产生html过程-->
<#--    40去修改下方script代码导入的形式-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>

                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>


                    <div class="mb-2 w-100">{{subTitle}}</div>

                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"></span>
<#--                        <img alt="1" 加入了星型组件免去这些操作 -->
<#--                             src="./resources/raty/lib/images/star-on.png"-->
<#--                             title="gorgeous">&nbsp;<img alt="2"-->
<#--                                                         src="./resources/raty/lib/images/star-on.png"-->
<#--                                                         title="gorgeous">&nbsp;<img-->
<#--                                alt="3" src="./resources/raty/lib/images/star-on.png" title="gorgeous">&nbsp;<img-->
<#--                                alt="4" src="./resources/raty/lib/images/star-on.png" title="gorgeous">&nbsp;<img-->
<#--                                alt="5" src="./resources/raty/lib/images/star-on.png" title="gorgeous"><input-->
<#--                                name="score" type="hidden" value="{{evaluationScore}}" readonly=""></span>-->
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}人已评</span>
                    </p>
                </div>
            </div>
        </a>
    </script>

<#--    38.编写Json序列化格式并追加到当前网页 上方以导入Art-Template[js]模板引擎 39将<div id="bookList">下面的代码裁切放到上面-->
        <script>
<#-- 41.引入星星图片 和 评价方法 上面的script运用简便写法 42下面 $(".stars")将星星的span标签选中 raty转换成可视的星星组件-->
            $.fn.raty.defaults.path = "./resources/raty/lib/images"
            $(function () {
                $.ajax({
                    url: "/books",
                    data : {p:1},
                    type : "get",
                    dataType : "json",
                    success: function(json){
                        console.info(json);
                        var list = json.records;//获取当前分页数据
                        for (var i = 0; i < list.length; i++) {
                            var book = json.records[i];
                            // 动态将数据组合成html
                            // var html = "<li>" + book.bookName + "</li>";
                            // 40.替换导入数据形式 (传入模板id,传入数据) 将数据结合tpl模板,生成html
                            // 41当评分降低的时候顺带着星星的亮度降低 raty星型评分组件 上面已准备好raty css js引入
                            // 41在上面几行引入星型评分的script方法
                            var html = template("tpl", book);
                            console.info(html);
                            //jquery的id选择器选中div对象 追加
                            $("#bookList").append(html);
                        }
                        //42.显示星型评价组件 只读并不能修改
                        $(".stars").raty({readonly:true});
                    }
                })
            })
    </script>

实现图书列表分页查询

index.ftl
// 43.用于绑定加载更多按钮单击事件 44下面的设置页面的逻辑及处理
        $(function () {
            $("#btnMore").click(function (){
                loadMore();
            })
        })
<script>
<#-- 41.引入星星图片 和 评价方法 上面的script运用简便写法 42下面 $(".stars")将星星的span标签选中 raty转换成可视的星星组件(被注释)-->
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
        // 45.对两次Ajax的代码进行重构与梳理(定义一个loadMore()方法将下面的裁切进来底下写进递归)
        // isReset参数设置true,代表从第一页开始查询,否则按nextPage查询后续页
        // 对下面那个data: {p: 1}, 进行重构 ↓↓↓↓ isReset 下面代码已经被注释但未完全删除
        // loadMore()加载更多数据
        function loadMore(isReset){
            if (isReset == true){
                $("#nextPage").val(1);
            }
            var nextPage = $("#nextPage").val();
            $.ajax({
                url: "/books",
                //nextPage为2可以加载第二页
                data: {p: nextPage},
                type: "get",
                dataType: "json",
                success: function (json) {
                    console.info(json);
                    var list = json.records;//获取当前分页数据
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        var html = template("tpl", book);
                        console.info(html);
                        //jquery的id选择器选中div对象 追加
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readonly: true});
                    // 44.如果当前页小于总页数 下面利用val设置隐藏域的值
                    // 可能将按照字符串处理 结果是31而不是4 要强制转换
                    // 若有后续数据的话 最后几行的divNoMore需要隐藏起来
                    // 45对两次的Ajax代码进行重构与梳理 上面找到第一个script
                    if (json.current < json.pages){
                        $("#nextPage").val(parseInt(json.current + 1));
                        $("#btnMore").show();
                        $("#divNoMore").hide();
                    }else {
                        $("#btnMore").hide();
                        $("#divNoMore").show();
                    }
                }
            })
        }
     
        $(function () {
            loadMore(true);
        })

        // 43.用于绑定加载更多按钮单击事件 44上面的设置页面的逻辑及处理
        $(function () {
            $("#btnMore").click(function (){
                loadMore();
            })
        })
    </script>

实现图书多条件动态查询

index.ftl
// 43.用于绑定加载更多按钮单击事件 44上面的设置页面的逻辑及处理
        $(function(){
            $("#btnMore").click(function(){
                loadMore();
            })
            //46 增加点击的显示控件
            $(".category").click(function () {
                $(".category").removeClass("highlight"); //移除高亮
                $(".category").addClass("text-black-50");//增添灰色
                $(this).addClass("highlight");//增加高亮
                //49添加数量与热度 ↓点击的超链接 在下面也要添加order
                var categoryId = $(this).data("category"); //214行定义了data-category
                $("#categoryId").val(categoryId);
                loadMore(true); //每次点完要重新查询
            })//在排序处显示控件 47点击 全部|前端|后端 产生数据联动BookService上

            $(".order").click(function(){
                $(".order").removeClass("highlight"); //移除高亮
                $(".order").addClass("text-black-50");//增添灰色
                $(this).addClass("highlight");//增加高亮
                //~49.提取设置到隐藏域中 发送请求到Ajax服务器上最上面 50var categoryId
                var order = $(this).data("order");
                $("#order").val(order); //点击不同隐藏域时为其赋值
                loadMore(true);
            })
        })
com/imooc/reader/service/BookService.java
package com.imooc.reader.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
//图书服务
public interface BookService {
    //34.泛型<Book> 说明查询出来的每一行数据都是一个Book对象
    // 瞄准BookService快速生成实现类Alt+Enter => Implement interface
    //35编写com.imooc.reader.service.impl.BookServiceImpl

    /**
     * 分页查询图书
     * @param categoryId 分类编号
     * @param order 排序方式
     * @param page 页号
     * @param rows 每页记录数
     * @return 分页对象
     */
//    47增加两个变量且在BookServiceImpl中也对应增加上
    public IPage<Book> paging(Long categoryId, String order, Integer page, Integer rows);
}
package com.imooc.reader.service.impl.BookServiceImpl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imooc.reader.entity.Book;
import com.imooc.reader.mapper.BookMapper;
import com.imooc.reader.service.BookService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

//35. 实现分类代码编写  36生成测试用例 Code generate Test => BookServiceImplTest
@Service("bookService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class BookServiceImpl implements BookService {
    @Resource
    private BookMapper bookMapper;

    /**
     * 分页查询图书
     *
     * @param categoryId 分类编号
     * @param order      排序方式
     * @param page       页号
     * @param rows       每页记录数
     * @return 分页对象
     */
    public IPage<Book> paging(Long categoryId, String order, Integer page, Integer rows) {
        Page<Book> p = new Page<Book>(page, rows);
        QueryWrapper<Book> queryWrapper = new QueryWrapper<Book>();
        //47.编写匹配条件 48去BookController更改调用参数
        if (categoryId != null && categoryId != -1) {//代表传入了有效分类编号
            queryWrapper.eq("category_id", categoryId);//查询的where子句
        }
        if (order != null) {
            if (order.equals("quantity")) { //前台必须传入↓ 评价人数排序
                queryWrapper.orderByDesc("evaluation_quantity");//降序排序
            } else if (order.equals("score")) {
                queryWrapper.orderByDesc("evaluation_score");//评分降序
            }
        }
        IPage<Book> pageObject = bookMapper.selectPage(p, queryWrapper);//传入两个参数: page对象哪一页数据  每页记录数[条件构造器]
        return pageObject;
    }
}
com/imooc/reader/controller/BookController.java
package com.imooc.reader.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
import com.imooc.reader.entity.Category;
import com.imooc.reader.service.BookService;
import com.imooc.reader.service.CategoryService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import java.util.List;

//30.显示之前写好的index.ftl
@Controller
public class BookController {
    //传入模板数据 分类信息
    @Resource
    private CategoryService categoryService;
    @Resource
    private BookService bookService;
    //showIndex与url绑定  31继续向前推进因为index.ftl所有数据都是静态写死的
    //31要对每一个分类进行读取 List标签<#list>读取 转至index.ftl 64行

    /**
     * 显示首页
     *
     * @return
     */
    @GetMapping("/")
    public ModelAndView showIndex() {
        ModelAndView mav = new ModelAndView("index");
        List<Category> categoryList = categoryService.selectAll();//拿到列表
        //结果放入其中 属性名,值
        mav.addObject("categoryList", categoryList);
        return mav;
    }
//37.编写一个page方法 上面加个@Resource 绑定一个Ajax @GetMapping
    //http://localhost/books 非常长的JSON序列化结果  38index.ftl 40行

    /**
     * 分页查询图书列表
     *
     * @param p 页号
     * @return 分页对象
     */
    //48.更改调用参数 更改BookServiceImplTest.java调用参数 49回到index.ftl 增加function第二个
    @GetMapping("/books")
    @ResponseBody //具体数据来自客户端传入
    public IPage<Book> selectBook(Long categoryId, String order, Integer p) {
        if (p == null) {
            p = 1;
        }
        IPage<Book> pageObject = bookService.paging(categoryId, order, p, 10);
        return pageObject;
    }
}
index.ftl
<script>
        <#-- 41.引入星星图片 和 评价方法 上面的script运用简便写法 42下面 $(".stars")将星星的span标签选中 raty转换成可视的星星组件(被注释)-->
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
        // 45.对两次Ajax的代码进行重构与梳理(定义一个loadMore()方法将下面的裁切进来底下写进递归)
        // isReset参数设置true,代表从第一页开始查询,否则按nextPage查询后续页
        // 对下面那个data: {p: 1}, 进行重构 ↓↓↓↓ isReset 下面代码已经被注释但未完全删除
        // loadMore()加载更多数据 46下方整理点击时的高亮显示 [全部]
        function loadMore(isReset){
            if(isReset == true){
                $("#bookList").html("");
                $("#nextPage").val(1);
            }
            // 50.让Ajax获取数据在下方data填写 下方$(".category和.order")都要重新调用递归
            // 细节清空再显示 上方$("#bookList").html("");
            var nextPage = $("#nextPage").val();
            var categoryId= $("#categoryId").val();
            var order = $("#order").val();
            //接下来就是发送数据时 组织成参数发送服务器
            $.ajax({
                url : "/books" ,
                //nextPage为2可以加载第二页  ↓字符串:变量
                data : {p:nextPage,"categoryId":categoryId , "order":order},
                type : "get" ,
                dataType : "json" ,
                success : function(json){
                    console.info(json);
                    var list = json.records;
                    for(var i = 0 ; i < list.length ; i++){
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName + "</li>";
                        //将数据结合tpl模板,生成html
                        var html = template("tpl" , book);
                        console.info(html);
                        //jquery的id选择器选中div对象 追加
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readOnly:true});
                    // 44.如果当前页小于总页数 下面利用val设置隐藏域的值
                    // 可能将按照字符串处理 结果是31而不是4 要强制转换
                    // 若有后续数据的话 最后几行的divNoMore需要隐藏起来
                    // 45对两次的Ajax代码进行重构与梳理 上面找到第一个script
                    if(json.current < json.pages){
                        $("#nextPage").val(parseInt(json.current) + 1);
                        $("#btnMore").show();
                        $("#divNoMore").hide();
                    }else{
                        $("#btnMore").hide();
                        $("#divNoMore").show();
                    }
                }
            })
        }

图书详情页-读取图书信息

img标签src引用网络图片,响应403的解决方法_img src引用其他网站图片-CSDN博客

com/imooc/reader/service/BookService.java
public IPage<Book> paging(Long categoryId, String order, Integer page, Integer rows);
    /**
     * 根据图书编号查询图书对象
     * @param bookId 图书编号
     * @return 图书对象
     */
    //51.增加一个查询书籍的接口 再去BookServiceImpl中实现
    public Book selectById(Long bookId);
}
com/imooc/reader/service/impl/BookServiceImpl.java
 //51实现后再向上推进 BookController
    @Override
    public Book selectById(Long bookId) {
        Book book = bookMapper.selectById(bookId);
        return book;
    }
com/imooc/reader/controller/BookController.java
//51.进行页面的绑定 获取读书编号 52把所有ftl相对路径改成绝对路径 ./前面的.删掉
    //加上了./就是  localhost/book/......   删掉就是 localhost/......
    //52更改detail.ftl将静态页面改成动态页面
    @GetMapping("/book/{id}") //↓ 路径变量 和 上面的保持一致
    public ModelAndView showDetail(@PathVariable("id") Long id){
        Book book = bookService.selectById(id);
        ModelAndView mav = new ModelAndView("/detail");
        mav.addObject("book", book);
        return mav;
    }
detail.ftl
<#-- 52.更改detail.ftl将静态页面改成动态页面 <title> 还有下面图书细节 -->
<#--  53.来源于数据底层的description描述字段   54显示动态评论列表 数据库有evaluation表 创建一个entity/Evaluation-->

    <meta charset="UTF-8">
    <meta name="referrer" content="no-referrer"> <#--防止网页图片加载不出来-->
    <title>${book.bookName}</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="/resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="/resources/raty/lib/jquery.raty.css">
    <script src="/resources/jquery.3.3.1.min.js"></script>
    <script src="/resources/bootstrap/bootstrap.min.js"></script>
    <script src="/resources/art-template.js"></script>
    <script src="/resources/raty/lib/jquery.raty.js"></script>

-------------------------------------------------------------------
<div class="row p-2 description">
        <#-- 53.来源于数据底层的description描述字段     -->
        ${book.description}
    </div>

图书详情页-显示评论列表

com/imooc/reader/entity/Member.java
//59.创建Member实体后完成MemberMapper接口
@TableName("member")
public class Member {
    @TableId(type = IdType.AUTO)
    private Long memberId;
    private String username;
    private String password;
    private Integer salt;
    private String nickname;
    private Date createTime;
}Getter+Setter
com/imooc/reader/entity/Evaluation.java
package com.imooc.reader.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.util.Date;
//54.创造了评论相关的底层entity 55定义一个EvaluationMapper.java接口
//再定义一个 evaluation.xml  再service上创建一个新的接口EvaluationService.java
@TableName("evaluation")
public class Evaluation {
//  id是主键
    @TableId(type= IdType.AUTO)
    private Long evaluationId;
    private Long bookId;
    private String content;
    private Integer score;
    private Long memberId;
    private Date createTime;
    private Integer enjoy;
    private String state;
    private String disableReason;
    private Date disableTime;
    @TableField(exist = false) //58.说明book属性没有对应字段,不会参与到sql自动生成
    private Book book; //Book的关联字段 加注解不存在 不会参与sql语句自动生成中 59创建会员entity对象Member实体
    @TableField(exist = false)
    private Member member; //59.获取集合后查询每个评论的信息 EvaluationServiceImpl中
}Getter + Setter
com/imooc/reader/mapper/EvaluationMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Evaluation;
//55
public interface EvaluationMapper extends BaseMapper<Evaluation> {
}
mappers/evaluation.xml
<?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.imooc.reader.mapper.EvaluationMapper">
<!--55-->
</mapper>
com/imooc/reader/service/EvaluationService.java
package com.imooc.reader.service;

import com.imooc.reader.entity.Evaluation;

import java.util.List;
//55.再创建一个它的实现类 Alt+回车 com/imooc/reader/service/impl/EvaluationServiceImpl.java
public interface EvaluationService{
    /**
     * 按图书编号查询有效短评
     * @param bookId 图书编号
     * @return 评论列表
     */
    public List<Evaluation> selectByBookId(Long bookId);
}
com/imooc/reader/service/impl/EvaluationServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Book;
import com.imooc.reader.entity.Evaluation;
import com.imooc.reader.entity.Member;
import com.imooc.reader.mapper.BookMapper;
import com.imooc.reader.mapper.EvaluationMapper;
import com.imooc.reader.mapper.MemberMapper;
import com.imooc.reader.service.EvaluationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;
//55.按图书编号查询有效短评   56BookController得到对应的图书编号以后 基于service查询对应短评信息
@Service("evluationService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class EvaluationServiceImpl implements EvaluationService {
    @Resource
    private EvaluationMapper evaluationMapper;
    //59
    @Resource
    private MemberMapper memberMapper;
    @Resource
    private BookMapper bookMapper;
    /**
     * 按图书编号查询有效短评
     * @param bookId 图书编号
     * @return
     */
    @Override
    public List<Evaluation> selectByBookId(Long bookId) {
        Book book = bookMapper.selectById(bookId); //59根据参数查询book对象
        QueryWrapper<Evaluation> queryWrapper = new QueryWrapper<Evaluation>();
        queryWrapper.eq("book_id", bookId);
        queryWrapper.eq("state","enable");
        queryWrapper.orderByDesc("create_time");
        List<Evaluation> evaluationList = evaluationMapper.selectList(queryWrapper);
        //59.查询每个评论的信息
        for(Evaluation eva:evaluationList){
            Member member = memberMapper.selectById(eva.getMemberId());//59获得会员对象要使用接口咯
            eva.setMember(member);
            eva.setBook(book);//59循环的时候为每一个Evaluation设置一个book对象 60回到detail.ftl
        }
        return evaluationList;
    }
}
detail.ftl
 <div class="alert alert-primary w-100 mt-2" role="alert">短评
        <button type="button" id="btnEvaluation" class="btn btn-success btn-sm text-white float-right"
                style="margin-top: -3px;">
            写短评
        </button>
    </div>
    <div class="reply pl-2 pr-2">
<#--  57.对短评进行动态页面的整合与梳理 进行循环遍历  58注意下面的慕粉-126对应着数据库的会员表 再重来一遍 Evaluation-->
        <#list evaluationList as evaluation>
            <div>
                <div>
                    <span class="pt-1 small text-black-50 mr-2">${evaluation.createTime?string('MM-dd')}</span>
<#--   60.修改 慕粉-126 变为动态的-->
                    <span class="mr-2 small pt-1">${evaluation.member.nickname}</span>
                    <span class="stars mr-2" data-score="${evaluation.score}"></span>

                    <button type="button" data-evaluation-id="${evaluation.evaluationId}"
                            class="btn btn-success btn-sm text-white float-right" style="margin-top: -3px;">
                        <img style="width: 24px;margin-top: -5px;" class="mr-1"
                             src="https://img3.doubanio.com/f/talion/7a0756b3b6e67b59ea88653bc0cfa14f61ff219d/pics/card/ic_like_gray.svg"/>
                        <span>${evaluation.enjoy}</span>
                    </button>
                </div>

                <div class="row mt-2 small mb-3">
                    ${evaluation.content}
                </div>
                <hr/>
            </div>
        </#list>
    </div>

会员注册与登录

Kaptcha验证码的配置与使用
  • Kaptcha 是谷歌开源的可高度配置的使用验证码生成工具
  • 通过 Kaptcha 可阻拦大多数机器人脚本操作
  • Kaptcha 典型应用于注册、登录、重要信息提交等用户交互
Kaptcha 使用步骤
  • Kaptcha 配置验证码生成参数
  • 开发 KaptchaController 生成验证码图片
  • 将前台输入验证码与session保存的验证码进行比对
pom.xml
<!--   61.增加Kaptcha验证码组件依赖 applicationContext增加一个bean  -->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
applicationContext.xml
<!--  61.配置Kaptcha 的bean  62创造KaptchaController.java-->
    <bean id="KaptchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha">
        <property name="config">
            <bean class="com.google.code.kaptcha.util.Config">
                <constructor-arg>
                    <props>
                        <!-- 验证码图片不生成边框 -->
                        <prop key="kaptcha.border">no</prop>
                        <!-- 验证码图片宽度为120像素  -->
                        <prop key="kaptcha.image.width">120</prop>
                        <!-- 验证码图片字体颜色为蓝色  -->
                        <prop key="kaptcha.textproducer.font.color">blue</prop>
                        <!-- 每个字符最大占用40像素  -->
                        <prop key="kaptcha.textproducer.font.size">40</prop>
                        <!-- 验证码包含4个字符  -->
                        <prop key="kaptcha.textproducer.char.length">4</prop>
                    </props>
                </constructor-arg>
            </bean>
        </property>
    </bean>
com/imooc/reader/controller/KaptchaController.java
package com.imooc.reader.controller;

import com.google.code.kaptcha.Producer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
//62.完成图片的随机生成与控制台的输出相同 63将原型里的 注册页 复制到项目工程register.ftl
@Controller
public class KaptchaController {
    @Resource
    private Producer kaptchaProducer;
//    Ioc动态注入 因为验证码组件设计的时候没有考虑过SpringMVC的集成 要使用原生的请求与响应
    @GetMapping("/verify_code")
    public void createVerifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //响应立即过期
        response.setDateHeader("Expires",0);
        //不存储 不缓存任何图片数据
        response.setHeader("Cache-Control","no-store,no-cache,must-revalidate");
        response.setHeader("Cache-Control","post-check=0,pre-check=0");
        response.setHeader("Pragma","no-cache");
        response.setContentType("image/png");
        //生成验证码字符文本
        String verifyCode = kaptchaProducer.createText();
        request.getSession().setAttribute("kaptchaVerifyCode",verifyCode);
        System.out.println( request.getSession().getAttribute("kaptchaVerifyCode"));
        BufferedImage image = kaptchaProducer.createImage(verifyCode);//创建验证码图片 二进制图片
        //二进制用getOutputStream() 字符用getWritter()
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(image,"png",out);//输出图片流
        out.flush(); //立即输出
        out.close(); //关闭输出流

    }
}

Kaptcha验证码的使用与对比 [运用到前端]

register.ftl
<div class="col-4 p-0 pl-2 pt-0">
   <!-- 63.验证码图片 src要显示从服务器后台动态生成的验证码 KaptchaController生成的url粘贴在这里 64弄一个MemberController-->
   <img id="imgVerifyCode" src="/verify_code"
   style="width: 120px;height:50px;cursor: pointer">
</div>
com/imooc/reader/controller/MemberController.java
package com.imooc.reader.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

//64.嵌入验证码 65编写单机验证码刷新register.ftl中的function reloadVerifyCode()
@Controller
public class MemberController {
    @GetMapping("/register.html")
    public ModelAndView showRegister() {
        return new ModelAndView("/register");
    }
    //67. 验证码匹配比对 request拿到对象  68编写会员服务MemberService.java
    @PostMapping("/registe")
    @ResponseBody
    public Map registe(String vc, String username, String password, String nickname, HttpServletRequest request){
        //接收kaptchaController.java中的request.getSession().setAttribute响应
        //正确验证码
        String verityCode  = (String)request.getSession().getAttribute("kaptchaVerifyCode");
        //验证码比对  后面的是大小写对比
        Map result = new HashMap();
        if (vc == null || verityCode == null || !vc.equalsIgnoreCase(verityCode)){
            result.put("code","VC01");
            result.put("msg","验证码错误");
        }else {
            result.put("code","0");
            result.put("msg","success");
        }
        return result;
    }
}
register.ftl
<script>
    //控制错误信息的显示与隐藏
    function showTips(isShow, css, text) {
        if (isShow) {
            $("#tips").removeClass("d-none")
            $("#tips").hide();
            $("#tips").addClass(css);
            $("#tips").text(text);
            $("#tips").fadeIn(200);
        } else {
            $("#tips").text("");
            $("#tips").fadeOut(200);
            $("#tips").removeClass();
            $("#tips").addClass("alert")
        }
    }
    //65.重新发送请求,刷新验证码 66验证码与后台绑定[比对校验]
    function reloadVerifyCode(){
        //请在这里实现刷新验证码 ts timestamp是时间戳 增加时间戳取消缓存
        $("#imgVerifyCode").attr("src","/verify_code?ts=" + new Date().getTime())
    }
    
    //点击验证码图片刷新验证码
    $("#imgVerifyCode").click(function () {
        reloadVerifyCode();
    });
    
    // 66.点击提交按钮,向/registe发起ajax请求
    //提交请求包含四个参数
    //vc:前台输入验证码  username:用户名 password:密码 nickname:昵称
    $("#btnSubmit").click(function () {
        //表单校验
        var username = $.trim($("#username").val());
        var regex = /^.{6,10}$/;
        if (!regex.test(username)) {
            showTips(true, "alert-danger", "用户名请输入正确格式(6-10位)");
            return;
        } else {
            showTips(false);
        }

        var password = $.trim($("#password").val());

        if (!regex.test(password)) {
            showTips(true, "alert-danger", "密码请输入正确格式(6-10位)");
            return;
        } else {
            showTips(false);
        }

        $btnReg = $(this);

        $btnReg.text("正在处理...");
        $btnReg.attr("disabled", "disabled");
        
        //66.发送ajax请求 67编写MemberController.java实现url
        $.ajax({
            url: "/registe",
            type: "post",
            dataType: "json",
            data: $("#frmLogin").serialize(),
            success: function (data) {
                //结果处理,根据服务器返回code判断服务器处理状态
                //服务器要求返回JSON格式:
                //{"code":"0","msg":"处理消息"}
                console.info("服务器响应:" , data);
                if (data.code == "0") {
                    //显示注册成功对话框
                    $("#exampleModalCenter").modal({});
                    $("#exampleModalCenter").modal("show");
                } else {
                    //服务器校验异常,提示错误信息
                    showTips(true, "alert-danger", data.msg);
                    reloadVerifyCode();
                    $btnReg.text("注    册");
                    $btnReg.removeAttr("disabled");
                }
            }
        });
        return false;
    });
</script>
============================
67在上面MemberController.java

实现会员注册功能

500 (Internal Server Error)_谷歌浏览器 internal server error-CSDN博客

com/imooc/reader/service/MemberService.java
package com.imooc.reader.service;

import com.imooc.reader.entity.Member;
//68.创建一个实现类MemberServiceImpl.java[Alt + Enter]
public interface MemberService {
    /**
     * 会员注册,创建新会员
     * @param username
     * @param password
     * @param nickname
     * @return
     */
    public Member createMember(String username, String password, String nickname);
}
com/imooc/reader/service/exception.java
package com.imooc.reader.service.exception;

/**
 * 68.业务逻辑异常 回到MemberServiceImpl编写用户名异常 69返回MemberServiceImpl
 */
public class BussinessException extends RuntimeException{
    private String code;
    private String msg;

    //构造方法
    public BussinessException(String code, String msg) {
        super(msg); //继承自运行时异常
        this.code = code;
        this.msg = msg;
    }

    public String getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}
com/imooc/reader/utils/MD5Utils.java
package com.imooc.reader.utils;

import org.apache.commons.codec.digest.DigestUtils;
//70.生成MD5方法 返回MemberServiceImpl编写随机数
public class MD5Utils {
    public static String md5Digest(String source, Integer salt){
        char[] ca = source.toCharArray();//获取字符数组
        for (int i = 0; i < ca.length; i++) {
            ca[i] = (char) (ca[i] + salt);
        }
        String target = new String(ca);
        String md5 = DigestUtils.md5Hex(target);
        return md5;
    }
}
com/imooc/reader/service/impl/MemberServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Member;
import com.imooc.reader.mapper.MemberMapper;
import com.imooc.reader.service.exception.BussinessException;
import com.imooc.reader.service.MemberService;
import com.imooc.reader.utils.MD5Utils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Random;

//68.完成与会员的交互[写操作居多] 完成运行异常service/BussinessException.java
@Service("memberService")
@Transactional
public class MemberServiceImpl implements MemberService {
    @Resource
    private MemberMapper memberMapper;

    /**
     * 会员注册,创建新会员
     *
     * @param username 用户名
     * @param password 密码
     * @param nickname 昵称
     * @return 新会员对象
     */
    public Member createMember(String username, String password, String nickname) {
        QueryWrapper<Member> queryWrapper = new QueryWrapper<Member>();
        queryWrapper.eq("username", username);
        List<Member> memberList = memberMapper.selectList(queryWrapper);
        //判断用户名是否已存在 68定义一个异常BussinessException.java
        //69.续写异常  70增加加密组件pom.xml
        if (memberList.size() > 0) {
            throw new BussinessException("M01", "用户名已存在");
        }
        Member member = new Member();
        member.setUsername(username);
        member.setNickname(nickname);
        //70. 随机数md5  71生成测试用例MemberServiceImplTest
        int salt = new Random().nextInt(1000)+1000; //盐值
        String md5 = MD5Utils.md5Digest(password, salt);
        member.setPassword(md5);
        member.setSalt(salt);
        member.setCreateTime(new Date()); //创建时间
        memberMapper.insert(member);
        return member;
    }
}
com/imooc/reader/service/impl/MemberServiceImplTest.java
package com.imooc.reader.service.impl;

import com.imooc.reader.service.MemberService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

//71.生成测试用例 72MemberController后续的工作
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class MemberServiceImplTest {
    @Resource
    private MemberService memberService;
    @Test
    public void createMember() {
        memberService.createMember("s123457", "123456", "测试");
    }
}
com/imooc/reader/controller/MemberController.java
package com.imooc.reader.controller;

import com.imooc.reader.service.exception.BussinessException;
import com.imooc.reader.service.MemberService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

//64.嵌入验证码 65编写单机验证码刷新register.ftl中的function reloadVerifyCode()
@Controller
public class MemberController {
    //72后续工作 进行注入
    @Resource
    private MemberService memberService;

    @GetMapping("/register.html")
    public ModelAndView showRegister() {
        return new ModelAndView("/register");
    }

    //67. 验证码匹配比对 request拿到对象  68编写会员服务MemberService.java
    @PostMapping("/registe")
    @ResponseBody
    public Map registe(String vc, String username, String password, String nickname, HttpServletRequest request) {
        //接收kaptchaController.java中的request.getSession().setAttribute响应
        //正确验证码
        String verityCode = (String) request.getSession().getAttribute("kaptchaVerifyCode");
        //验证码比对  后面的是大小写对比
        Map result = new HashMap();
        if (vc == null || verityCode == null || !vc.equalsIgnoreCase(verityCode)) {
            result.put("code", "VC01");
            result.put("msg", "验证码错误");
        } else {
            //72.调用过程  73将登录页复制进去 74MemberServiceImpl
            try {
                memberService.createMember(username, password, nickname);
                result.put("code", "0");
                result.put("msg", "success");
            } catch (BussinessException ex) {
                ex.printStackTrace();
                result.put("code", ex.getCode());
                result.put("msg", ex.getMsg());
            }
        }
        return result;
    }
}
http://localhost/register.html

实现会员登录功能

 com/imooc/reader/service/impl/MemberServiceImpl.java
 /**
     * 登陆检查
     * @param username 用户名
     * @param password 密码
     * @return 登录对象
     */
    //74.登录方法的接口实现 校验密码 75再续方法 MemberController.java
    public Member checkLogin(String username, String password) {
        QueryWrapper<Member> queryWrapper = new QueryWrapper<Member>();
        queryWrapper.eq("username", username);
        Member member = memberMapper.selectOne(queryWrapper);
        if (member == null){
            throw new BussinessException("M02", "用户不存在");
        }//校验密码
        String md5 = MD5Utils.md5Digest(password, member.getSalt());
        if (!md5.equals(member.getPassword())){
            throw new BussinessException("M03", "输入密码有误");
        }
        return member;
    }
com/imooc/reader/controller/MemberController.java
// 75.HttpSession session 登录校验后会将这个会员信息存放到session中
    // 76登录校验成功以后会返回一个member对象 存放在session中
    @PostMapping("/check_login") //前台url
    @ResponseBody
    public Map checkLogin(String username, String password, String vc, HttpSession session){
        //正确验证码                     ↓为了得到session对象
        String verityCode = (String)session.getAttribute("kaptchaVerifyCode");
        //验证码比对  后面的是大小写对比
        Map result = new HashMap();
        if (vc == null || verityCode == null || !vc.equalsIgnoreCase(verityCode)) {
            result.put("code", "VC01");
            result.put("msg", "验证码错误");
        }else {
            try {//处理成功
                Member member = memberService.checkLogin(username, password);
                //76.member存放在session中  77去index.ftl下方登录块改造
                session.setAttribute("loginMember",member);
                result.put("code", "0");
                result.put("msg", "success");
            } catch (BussinessException ex) { //处理失败
                ex.printStackTrace();
                result.put("code", ex.getCode());
                result.put("msg", ex.getMsg());
            }
        }
        return result;
    }
index.ftl 
<#--   77.将获取到的member的session数据替换成登录右上角的动态数据-->
  <#--  两个问号代表前面的属性是存在的情况下输出其中的html -->
<#-- 78新增entity/MemberReadState.java会员阅读状态 -->
        <#if loginMember??>
            <h6 class="mt-1">
                <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">${loginMember.nickname}
            </h6>
            <#else> <#--上面是已登录 下面是未登录-->
                <a href="/login.html" class="btn btn-light btn-sm">
                    <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
                </a>
        </#if>

获取会员阅读状态

实现会员交互功能
  • 想看/看过的阅读状态变更
  • 为图书写短评
  • 为喜欢的短评点赞
com/imooc/reader/entity/MemberReadState.java
package com.imooc.reader.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;

import java.util.Date;

/**
 * 会员阅读状态实体
 */
//78.搞实体 再去创MemberReadStateMapper.java接口
public class MemberReadState {
    @TableId(type = IdType.AUTO)
    private Long rsId;
    private Long bookId;
    private Long memberId;
    private Integer readState;
    private Date createTime;
}
com/imooc/reader/mapper/MemberReadStateMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.MemberReadState;

//78接口创完 再去搞个Member_Read_State.xml
public interface MemberReadStateMapper extends BaseMapper<MemberReadState> {
}
mappers/member_read_state.xml
<?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.imooc.reader.mapper.MemberReadStateMapper">
<!--  78. 去MemberService 阅读状态-->
</mapper>
com/imooc/reader/service/MemberService.java 
//78.获取阅读状态 再去实现这个方法

    /**
     * 获取阅读状态
     *
     * @param memberId 会员编号
     * @param bookId   图书编号
     * @return 阅读状态对象
     */
    public MemberReadState selectMemberReadState(Long memberId, Long bookId);
com/imooc/reader/service/impl/MemberServiceImpl.java
 //78.新增方法实现那个方法 再去上方进行方法注入 79根据会员编号进行查询BookController

    /**
     * 获取阅读状态
     * @param memberId 会员编号
     * @param bookId 图书编号
     * @return 阅读状态对象
     */
    public MemberReadState selectMemberReadState(Long memberId, Long bookId){
        QueryWrapper<MemberReadState> queryWrapper = new QueryWrapper();
        queryWrapper.eq("book_id", bookId);
        queryWrapper.eq("member_id", memberId);
        MemberReadState memberReadState = memberReadStateMapper.selectOne(queryWrapper);
        return memberReadState;
    }
com/imooc/reader/controller/BookController.java
//51.进行页面的绑定 获取读书编号 52把所有ftl相对路径改成绝对路径 ./前面的.删掉
    //加上了./就是  localhost/book/......   删掉就是 localhost/......
    //52更改detail.ftl将静态页面改成动态页面
    @GetMapping("/book/{id}") //↓ 路径变量 和 上面的保持一致
    //79.新增一个参数 获取当前用户登录信息
    public ModelAndView showDetail(@PathVariable("id") Long id, HttpSession session) {
        Book book = bookService.selectById(id);
        //56.写完并放入 mav.addObject 57得到了对象就要去detail.ftl渲染循环遍历[写短评下方的div]
        List<Evaluation> evaluationList = evaluationService.selectByBookId(id);
        //79.将之前设置的用户信息拿出来 两种情况 ①会员没登录就null ②会员登录了member对象存在了 注入memberService
        Member member = (Member)session.getAttribute("loginMember");
        ModelAndView mav = new ModelAndView("/detail");
        if (member != null) { //获取会员阅读状态    会员编号 图书编号  80 detail.ftl想看[1]与看过[2] 页面上方找script块
            MemberReadState memberReadState = memberService.selectMemberReadState(member.getMemberId(), id);
            mav.addObject("memberReadState", memberReadState);

        }
        mav.addObject("book", book);
        mav.addObject("evaluationList", evaluationList);
        return mav;
    }
detail.ftl
 <script>
        $.fn.raty.defaults.path = '/resources/raty/lib/images';
        $(function () {
            $(".stars").raty({readOnly: true});
        })
       // 80 想看与看过 freemark脚本 如果它存在(想看/看过) 81产生对应的状态数据 MemberService
        $(function () {
            <#if memberReadState ??>
            // 重选阅读状态回填
            $("*[data-read-state='${memberReadState.readState}']").addClass("highlight");
            </#if>
            <#if !loginMember ??>
                $("*[data-read-state]").click(function () {
                    // 利用jquery选择div的对话框函数 显示需要登录
                    $("#exampleModalCenter").modal("show");
                })
            </#if>
        })
    </script>

更新会员阅读状态

com/imooc/reader/service/MemberService.java
 /**
     * 更新阅读状态
     * @param memberId 会员编号
     * @param bookId 图书编号
     * @param readState 阅读状态
     * @return 阅读对象状态
     */         //81定义方法 MemberServiceImpl实现状态
    public MemberReadState updateMemberReadState(Long memberId, Long bookId, Integer readState);
com/imooc/reader/service/impl/MemberServiceImpl.java
/**
 * 更新阅读状态
 * @param memberId 会员编号
 * @param bookId 图书编号
 * @param readState 阅读状态
 * @return 阅读对象状态
 */
//81.编写阅读状态代码 编写完打开MemberController
public MemberReadState updateMemberReadState(Long memberId, Long bookId, Integer readState){
    QueryWrapper<MemberReadState> queryWrapper = new QueryWrapper<MemberReadState>();
    queryWrapper.eq("book_id", bookId);
    queryWrapper.eq("member_id", memberId);
    MemberReadState memberReadState = memberReadStateMapper.selectOne(queryWrapper);
    //空的代表没有点过按钮 数据是空的
        //无则新增,有则更新
        if(memberReadState == null){
            memberReadState = new MemberReadState();
            memberReadState.setMemberId(memberId);
            memberReadState.setBookId(bookId);
            memberReadState.setReadState(readState);
            memberReadState.setCreateTime(new Date());
            memberReadStateMapper.insert(memberReadState);
        }else{
            memberReadState.setReadState(readState);
            memberReadStateMapper.updateById(memberReadState);
        }

        return memberReadState;
    }
com/imooc/reader/controller/MemberController.java
//81. 完成更新状态的事务  82去完成detail.ftl后面的loginMember存在的代码
    @PostMapping("/update_read_state")
    @ResponseBody
    public Map updateReadState(Long memberId, Long bookId, Integer readState){
        Map result = new HashMap();
        try {
            memberService.updateMemberReadState(memberId, bookId, readState);
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }
detail.ftl
//82 完成登录状态
            <#if loginMember ??>
            /**
             * 更新会员阅读状态
             */
            $("*[data-read-state]").click(function () {
                //会员阅读状态
                var readState = $(this).data("read-state");
                //发送请求
                $.post("/update_read_state", {
                    memberId: ${loginMember.memberId},
                    bookId: ${book.bookId},
                    readState: readState
                }, function (json) {
                    if (json.code == "0") { //服务器处理成功
                        $("*[data-read-state]").removeClass("highlight");//高亮的清除
                        $("*[data-read-state='" + readState + "']").addClass("highlight");//状态值放入其中
                    }
                }, "json")
            });
            </#if>

实现写短评功能

detail.ftl
 //83.短评功能  下面短评联动现象
            $("#btnEvaluation").click(function () {
                // 选中id=score的标签 转换为星型组件
                $("#score").raty({});
                $("#dlgEvaluation").modal("show");//显示短评对话框
            })
---------------------------------------------------------------
<!-- Modal -->
<div class="modal-content">
            <div class="modal-body">
<#-- 83. 短评联动   84 MemberService.java 短评实现代码 -->
                <h6>为${book.bookName}写短评</h6>
                <form id="frmEvaluation">
                    <div class="input-group  mt-2 ">
                        <span id="score"></span>
                    </div>
                    <div class="input-group  mt-2 ">
                        <input type="text" id="content" name="content" class="form-control p-4"
                               placeholder="这里输入短评">
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" id="btnSubmit" class="btn btn-primary">提交</button>
            </div>
        </div>
com/imooc/reader/service/MemberService.java
//84.短评代码!  MemberServiceImpl 组一个全新的对象
    public Evaluation evaluate(Long memberId, Long bookId, Integer score, String content);
com/imooc/reader/service/impl/MemberServiceImpl.java
/**
     * 发布新的短评
     * @param memberId 会员编号
     * @param bookId 图书编号
     * @param score 评分
     * @param content 短评内容
     * @return
     */ // 84 完成评论信息的更新 85在MemberController编写与web交互功能
    public Evaluation evaluate(Long memberId, Long bookId, Integer score, String content) {
        Evaluation evaluation = new Evaluation();
        evaluation.setMemberId(memberId);
        evaluation.setBookId(bookId);
        evaluation.setScore(score);
        evaluation.setContent(content);
        evaluation.setCreateTime(new Date());
        evaluation.setState("enable");
        evaluation.setEnjoy(0); //注入evaluationMapper 完成数据新增
        evaluationMapper.insert(evaluation);
        return evaluation;
    }
com/imooc/reader/controller/MemberController.java
//85.与web交互功能 前台请求传入数据 86回到detail.ftl写
    @PostMapping("/evaluate")
    @ResponseBody
    public Map evaluate(Long memberId, Long bookId, Integer score, String content){
        Map result = new HashMap();
        try {
//    Evaluation eva = memberService.evaluate(memberId, bookId, score, content);
            memberService.evaluate(memberId, bookId, score, content);
            result.put("code", "0");
            result.put("msg", "success");
//          result.put("evaluation", eva);
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }
detail.ftl
//86.评论对话框提交数据  87完成点赞核心实现 MemberService
            $("#btnSubmit").click(function () {
                var score = $("#score").raty("score");//获取评分
                var content = $("#content").val();
                if (score == 0 || $.trim(content) == ""){ //没有进行选择 或 删除前后空格
                    return; //禁止提交方法中断
                }
                $.post("/evaluate",{
                    score : score,
                    bookId: ${book.bookId},
                    memberId: ${loginMember.memberId},
                    content: content
                },function (json) {
                    if (json.code == "0") {//处理成功
                        window.location.reload();//列表进行刷新
                    }
                },"json")
            })

完成会员短评点赞核心实现

com/imooc/reader/service/MemberService.java
 /**
     * 短评点赞
     * @param evaluationId 短评编号
     * @return 短评对象
     */
//    87完成点赞核心实现 MemberServiceImpl
    public Evaluation enjoy(Long evaluationId);
com/imooc/reader/service/impl/MemberServiceImpl.java
// 87.短评点赞 自增+1 Membercontroller
    public Evaluation enjoy(Long evaluationId) {
        Evaluation evaluation = evaluationMapper.selectById(evaluationId);
        evaluation.setEnjoy(evaluation.getEnjoy()+1);
        evaluationMapper.updateById(evaluation);
        return evaluation;
    }
com/imooc/reader/controller/MemberController.java
 //87.实现controller 88让客户端页面发送ajax请求
    @PostMapping("/enjoy")
    @ResponseBody
    public Map evaluate(Long evaluationId) {
        Map result = new HashMap();
        try {
//    Evaluation eva = memberService.evaluate(memberId, bookId, score, content);
            Evaluation eva = memberService.enjoy(evaluationId);
            result.put("code", "0");
            result.put("msg", "success");
            result.put("evaluation", eva); //包含最新点赞数
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }
detail.ftl
// 88.评论点赞
            $("*[data-evaluation-id]").click(function(){
                var evaluationId = $(this).data("evaluation-id");
                $.post("/enjoy",{evaluationId:evaluationId},function(json){
                    if(json.code == "0"){
                        $("*[data-evaluation-id='" + evaluationId + "'] span").text(json.evaluation.enjoy);
                    }
                },"json")
            })

Spring Task定时任务 [eg: 闹钟命令]

  • Spting Task 是 Spring 3.0后推出的定时任务模块
  • Spring Task的职责是按周期后台自动执行任务
  • Spring Task可利用Cron表达式实现灵活的定时处理
Cron表达式实例
小时 星期
0 * * * *
0,30 0-5 * * * ? 2000
0 0 9-18 ? * WED
利用Spring-Task自动计算图书评分 [所有图书评价信息重算]
mappers/book.xml
<!--    89.增加平均评分增加信息  增加接口BookMapper -->
    <update id="updateEvaluation">
        update book b SET evaluation_score = (
            select ifnull(avg(score),0) from evaluation where book_id = b.book_id and state='enable'
        ),evaluation_quantity = (
            select ifnull(count(*),0) from evaluation where book_id = b.book_id and state='enable'
        )
    </update>
com/imooc/reader/mapper/BookMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Book;
//33.创建完Book接口  34与之对应的创建book.xml文件
//89. 更新图书评分  BookService去定义
public interface BookMapper extends BaseMapper<Book> {
    /**
     * 更新图书评分/评价数量
     */
    public void updateEvaluation();
}
com/imooc/reader/service/BookService.java
 /**
     * 更新图书评分/评价数量
     */
    //89. 定义完去impl定义实现类
    public void updateEvaluation();
}
com/imooc/reader/service/impl/BookServiceImpl.java
/**
     * 更新图书评分/评价数量  Controller直接面向Service Service面向Mapper
     */
    //89. 更新操作需要声明式事务 开启  90applicationContext.xml开启task
    @Transactional
    public void updateEvaluation() {
        bookMapper.updateEvaluation();
    }
applicationContext.xml
<!--  61.配置Kaptcha 的bean  62创造KaptchaController.java-->
    <bean id="KaptchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha">
        <property name="config">
            <bean class="com.google.code.kaptcha.util.Config">
                <constructor-arg>
                    <props>
                        <!-- 验证码图片不生成边框 -->
                        <prop key="kaptcha.border">no</prop>
                        <!-- 验证码图片宽度为120像素  -->
                        <prop key="kaptcha.image.width">120</prop>
                        <!-- 验证码图片字体颜色为蓝色  -->
                        <prop key="kaptcha.textproducer.font.color">blue</prop>
                        <!-- 每个字符最大占用40像素  -->
                        <prop key="kaptcha.textproducer.font.size">40</prop>
                        <!-- 验证码包含4个字符  -->
                        <prop key="kaptcha.textproducer.char.length">4</prop>
                    </props>
                </constructor-arg>
            </bean>
        </property>
    </bean>
    <!-- 90.开启Spring Task定时任务的注解模式 创建一个reader/task/ComputeTask.java-->
    <task:annotation-driven/>
</beans>
com/imooc/reader/task/ComputeTask.java
package com.imooc.reader.task;

import com.imooc.reader.service.BookService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

//90.组件注解 不确定是哪个类 但是会被扫描实例化和管理
@Component
public class ComputeTask {
    @Resource
    private BookService bookService;
//  每分钟0秒时候 执行一次Cron表达式
    @Scheduled(cron = "0 * * * * ?")
    public void updateEvaluation(){
        bookService.updateEvaluation();
        System.out.println("已更新所有图书评分");
    }
}

后台管理功能-图书管理

富文本编辑器wangEditor使用入门
  • 基于Javascript与css开发的Web富文本编辑器
test.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 91.引入wangEditor  创建WEB-INF/ftl/management/book.ftl-->
    <script src="/resources/wangEditor.min.js"></script>
</head>
<body>
<div>
    <button id="btnRead">读取内容</button>
    <button id="btnWrite">写入内容</button>
</div>
<div id="divEditor" style="width: 800px;height:600px"></div>
<script>
    var E = window.wangEditor;
    var editor = new E("#divEditor");//完成富文本编辑器初始化
    editor.create();//创建富文本编辑器,显示在页面上
    document.getElementById("btnRead").onclick = function(){
        var content = editor.txt.html();//获取编辑器现有的html内容
        alert(content);
    }
    document.getElementById("btnWrite").onclick = function(){
        var content = "<li style='color:red'>我是<b>新内容</b></li>";
        editor.txt.html(content);
    }
</script>
</body>
</html>

实现图书管理功能

在训练素材中导入图书管理页
book.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<#-- 91.创建后台图书管理控制器com/imooc/reader/controller/management/MBookController.java   -->
    <title>图书管理功能</title>
    <style>
        #dlgBook{
            padding: 10px
        }
    </style>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">

    <script src="/resources/wangEditor.min.js"></script>


    <script type="text/html" id="toolbar">
        <div class="layui-btn-container">
            <button class="layui-btn layui-btn-sm" id="btnAdd" onclick="showCreate()">添加</button>
        </div>
    </script>

</head>
<body>


<div class="layui-container">
    <blockquote class="layui-elem-quote">图书列表</blockquote>
    <!-- 数据表格 -->
    <table id="grdBook" lay-filter="grdBook"></table>
</div>
<!--表单内容-->
<div id="dialog" style="padding: 10px;display: none">
    <form class="layui-form" >
        <div class="layui-form-item">
            <!-- 图书类别 -->
            <select id="categoryId" name="categoryId" lay-verify="required" lay-filter=
            "categoryId">
                <option value=""></option>
                <option value="1">前端</option>
                <option value="2">后端</option>
                <option value="3">测试</option>
                <option value="4">产品</option>
            </select>

        </div>
        <div class="layui-form-item">
            <!-- 书名 -->
            <input type="text" id="bookName" name="bookName" required lay-verify="required" placeholder="请输入书名"
                   autocomplete="off" class="layui-input">
        </div>


        <div class="layui-form-item">
            <!-- 子标题 -->
            <input type="text" id="subTitle" name="subTitle" required lay-verify="required" placeholder="请输入子标题"
                   autocomplete="off" class="layui-input">
        </div>

        <div class="layui-form-item">
            <!-- 作者 -->
            <input type="text" id="author" name="author" required lay-verify="required" placeholder="请输入作者信息"
                   autocomplete="off" class="layui-input">
        </div>

        <div style="margin-top: 30px;font-size: 130%">图书介绍(默认第一图将作为图书封面)</div>
        <div class="layui-form-item" >
            <!-- wangEditor编辑器 -->
            <div id="editor" style="width: 100%">

            </div>
        </div>
        <!-- 图书编号 -->
        <input id="bookId" type="hidden">
        <!-- 当前表单操作类型,create代表新增 update代表修改 -->
        <input id="optype"  type="hidden">
        <div class="layui-form-item" style="text-align: center">
            <!-- 提交按钮 -->
            <button class="layui-btn" lay-submit="" lay-filter="btnSubmit">立即提交</button>
        </div>
    </form>
</div>
<script src="/resources/layui/layui.all.js"></script>
<script>

    var table = layui.table; //table数据表格对象
    var $ = layui.$; //jQuery
    var editor = null; //wangEditor富文本编辑器对象
    //初始化图书列表
    table.render({
        elem: '#grdBook'  //指定div
        , id : "bookList" //数据表格id
        , toolbar: "#toolbar" //指定工具栏,包含新增添加
        , url: "/management/book/list" //数据接口
        , page: true //开启分页
        , cols: [[ //表头
            {field: 'bookName', title: '书名', width: '300'}
            , {field: 'subTitle', title: '子标题', width: '200'}
            , {field: 'author', title: '作者', width: '200'}
            , {type: 'space', title: '操作', width: '200' , templet : function(d){
                    //为每一行表格数据生成"修改"与"删除"按钮,并附加data-id属性代表图书编号
                    return "<button class='layui-btn layui-btn-sm btn-update'  data-id='" + d.bookId + "' data-type='update' onclick='showUpdate(this)'>修改</button>" +
                        "<button class='layui-btn layui-btn-sm btn-delete'  data-id='" + d.bookId + "'   onclick='showDelete(this)'>删除</button>";
                }
            }
        ]]
    });
    //显示更新图书对话框
    //obj对应点击的"修改"按钮对象
    function showUpdate(obj){
        //弹出"编辑图书"对话框
        layui.layer.open({
            id: "dlgBook", //指定div
            title: "编辑图书", //标题
            type: 1, 
            content: $('#dialog').html(), //设置对话框内容,复制自dialog DIV
            area: ['820px', '730px'], //设置对话框宽度高度
            resize: false //是否允许调整尺寸
        })

        var bookId = $(obj).data("id"); //获取"修改"按钮附带的图书编号
        $("#dlgBook #bookId").val(bookId); //为表单隐藏域赋值,提交表单时用到

        editor = new wangEditor('#dlgBook #editor'); //初始化富文本编辑器
        editor.customConfig.uploadImgServer = '/management/book/upload' //设置图片上传路径
        editor.customConfig.uploadFileName = 'img'; //图片上传时的参数名
        editor.create(); //创建wangEditor
        $("#dlgBook #optype").val("update"); //设置当前表单提交时提交至"update"更新地址

        //发送ajax请求,获取对应图书信息
        $.get("/management/book/id/" + bookId , {} , function(json){
            //文本框回填已有数据
            $("#dlgBook #bookName").val(json.data.bookName);//书名
            $("#dlgBook #subTitle").val(json.data.subTitle); //子标题
            $("#dlgBook #author").val(json.data.author);//作者
            $("#dlgBook #categoryId").val(json.data.categoryId); //分类选项
            editor.txt.html(json.data.description); //设置图文内容
            layui.form.render();//重新渲染LayUI表单
        } , "json")



    }
    //显示新增图书对话框
    function showCreate(){
        //弹出"新增图书"对话框
        layui.layer.open({
            id: "dlgBook",
            title: "新增图书",
            type: 1,
            content: $('#dialog').html(),
            area: ['820px', '730px'],
            resize: false
        })
        //初始化wangEditor
        editor = new wangEditor('#dlgBook #editor');
        editor.customConfig.uploadImgServer = '/management/book/upload';//设置图片上传地址
        editor.customConfig.uploadFileName = 'img';//设置图片上传参数
        editor.create();//创建wangEditor

        layui.form.render(); //LayUI表单重新
        $("#dlgBook #optype").val("create");//设置当前表单提交时提交至"create"新增地址

    };

    //对话框表单提交
    layui.form.on('submit(btnSubmit)', function(data){
        //获取表单数据
        var formData = data.field;
        
        //判断是否包含至少一副图片,默认第一图作为封面显示
        var description = editor.txt.html();
        if(description.indexOf("img") == -1){
            layui.layer.msg('请放置一副图片作为封面');
            return false;
        }
        //获取当前表单要提交的地址
        //如果是新增数据则提交至create
        //如果是更新数据则提交至update
        var optype = $("#dlgBook #optype").val();
        
        if(optype == "update"){
            //更新数据时,提交时需要附加图书编号
            formData.bookId=$("#dlgBook #bookId").val();
        }
        //附加图书详细描述的图文html
        formData.description = description;
        //向服务器发送请求
        $.post("/management/book/" + optype , formData , function(json){
            if(json.code=="0"){
                //处理成功,关闭对话框,刷新列表,提示操作成功
                layui.layer.closeAll();
                table.reload('bookList');
                layui.layer.msg('数据操作成功,图书列表已刷新');
            }else{
                //处理失败,提示错误信息
                layui.layer.msg(json.msg);
            }
        } ,"json")
        return false;
    });
    //删除图书
    function showDelete(obj){
        //获取当前点击的删除按钮中包含的图书编号
        var bookId = $(obj).data("id");
        //利用layui的询问对话框进行确认
        layui.layer.confirm('确定要执行删除操作吗?', {icon: 3, title:'提示'}, function(index){
                    
                //确认按钮后发送ajax请求,包含图书编号
                $.get("/management/book/delete/" + bookId, {}, function (json) {
                    if(json.code=="0"){
                        //删除成功刷新表格
                        table.reload('bookList');
                        //提示操作成功
                        layui.layer.msg('数据操作成功,图书列表已刷新');
                        //关闭对话框
                        layui.layer.close(index);
                    }else{
                        //处理失败,提示错误信息
                        layui.layer.msg(json.msg);
                    }
                }, "json");
            
        });

    }

</script>
</body>
</html>
com/imooc/reader/controller/management/MBookController.java
package com.imooc.reader.controller.management;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

//http://localhost/management/book/index.html
//91.后台管理系统 编写富文本编辑器的图片上传功能book.ftl已全实现
//editor.customConfig.uploadImgServer = '/management/book/upload';//设置图片上传地址
//92提供文件上传地址的接口 pom.xml增加文件上传依赖 apache
@Controller
@RequestMapping("/management/book")
public class MBookController {
    @GetMapping("/index.html")
    public ModelAndView showBook(){
        return new ModelAndView("/management/book");
    }

    /**
     * wangEditor文件上传
     * @param file 上传文件
     * @param request 原生请求对象
     * @return
     * @throws IOException
     */
    //93. 接收提交的文件 上传的文件保存到upload目录中 代码编写
    @PostMapping("/upload")
    @ResponseBody
    public Map upload(@RequestParam("img") MultipartFile file, HttpServletRequest request) throws IOException {
        //得到上传目录
        String uploadPath = request.getServletContext().getResource("/").getPath() + "/upload/";//在out里 运行时执行获取路径
        //文件名
        String fileName = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        //原始文件扩展名
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//最后一次获得点
        //保存文件到upload目录
        file.transferTo(new File(uploadPath + fileName + suffix));
        Map result = new HashMap();
        result.put("errno", 0);
        result.put("data", new String[]{"/upload/" + fileName + suffix});
        return result;
    }
}
pom.xml
<!-- 92.增加文件上传依赖 SpringMVC文件上传底层依赖 93去applicationContext.xml激活-->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>
applicationContext.xml
<!--  93.激活文件上传功能 回到MBookController-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>
com/imooc/reader/controller/management/MBookController.java
 /**
     * wangEditor文件上传
     * @param file 上传文件
     * @param request 原生请求对象
     * @return
     * @throws IOException
     */
    //93. 接收提交的文件 上传的文件保存到upload目录中 代码编写 94book.ftl对话框表单提交
    @PostMapping("/upload")
    @ResponseBody
    public Map upload(@RequestParam("img") MultipartFile file, HttpServletRequest request) throws IOException {
        //得到上传目录
        String uploadPath = request.getServletContext().getResource("/").getPath() + "/upload/";//在out里 运行时执行获取路径
        //文件名
        String fileName = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        //原始文件扩展名
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//最后一次获得点
        //保存文件到upload目录
        file.transferTo(new File(uploadPath + fileName + suffix));
        Map result = new HashMap();
        result.put("errno", 0);
        result.put("data", new String[]{"/upload/" + fileName + suffix});
        return result;
    }

实现图书新增功能

book.ftl
// 94.对话框表单提交 95增加全新方法声明BookService
    layui.form.on('submit(btnSubmit)', function(data){
        //获取表单数据
        var formData = data.field;
        
        //判断是否包含至少一副图片,默认第一图作为封面显示
        var description = editor.txt.html();
        if(description.indexOf("img") == -1){
            layui.layer.msg('请放置一副图片作为封面');
            return false;
        }
        //获取当前表单要提交的地址
        //如果是新增数据则提交至create
        //如果是更新数据则提交至update
        var optype = $("#dlgBook #optype").val();
        
        if(optype == "update"){
            //更新数据时,提交时需要附加图书编号
            formData.bookId=$("#dlgBook #bookId").val();
        }
        //附加图书详细描述的图文html
        formData.description = description;
        //向服务器发送请求
        $.post("/management/book/" + optype , formData , function(json){
            if(json.code=="0"){
                //处理成功,关闭对话框,刷新列表,提示操作成功
                layui.layer.closeAll();
                table.reload('bookList');
                layui.layer.msg('数据操作成功,图书列表已刷新');
            }else{
                //处理失败,提示错误信息
                layui.layer.msg(json.msg);
            }
        } ,"json")
        return false;
    });
com/imooc/reader/service/BookService.java
 //95.创建新的图书 BookServiceImpl
    public Book createBook(Book book);
com/imooc/reader/service/impl/BookServiceImpl.java
 //95.数据增加 MBookController
    @Transactional
    public Book createBook(Book book) {
        bookMapper.insert(book);
        return book;
    }
com/imooc/reader/controller/management/MBookController.java
//95.获取书籍数据
    @PostMapping("/create")
    @ResponseBody
    public Map createBook(Book book){
        Map result = new HashMap();
        try {
            book.setEvaluationQuantity(0);
            book.setEvaluationScore(0f);
            Document doc = Jsoup.parse(book.getDescription()); //一个个片段被解析
            Element img = doc.select("img").first();//选中所有标签提取第一个 获取图书详情第一图的元素对象
            String cover = img.attr("src");//获取当前元素指定值
            book.setCover(cover); //95.来自于description描述的第一幅图
            //用html解析器jsoup.jorg从前台的描述里截取图片位置 96加入依赖
            bookService.createBook(book); //此处可以打点debug
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }

实现图书分页查询[显示图书列表]

com/imooc/reader/controller/management/MBookController.java
 //96.设计列表显示加载 97回到book.ftl
    @GetMapping("/list")
    @ResponseBody
    public Map list(Integer page, Integer limit){
        if (page == null){
            page = 1;
        }
        if (limit == null){
            limit = 10;
        }
        IPage<Book> pageObject = bookService.paging(null, null, page, limit);//返回paging的分页对象
        Map result = new HashMap();
        result.put("code", "0");
        result.put("msg", "success");
        //layUI返回值必须要求写的
        result.put("data", pageObject.getRecords()); //当前页面数据
        result.put("count", pageObject.getTotal()); //未分页时记录总数
        return result;
    }

实现图书修改更新功能 [原有数据调整 回填数据]

book.ftl
// 94.对话框表单提交 95增加全新方法声明BookService
    layui.form.on('submit(btnSubmit)', function(data){
        //获取表单数据
        var formData = data.field;
        
        //判断是否包含至少一副图片,默认第一图作为封面显示
        var description = editor.txt.html();
        if(description.indexOf("img") == -1){
            layui.layer.msg('请放置一副图片作为封面');
            return false;
        }
        //获取当前表单要提交的地址
        //如果是新增数据则提交至create
        //如果是更新数据则提交至update
        var optype = $("#dlgBook #optype").val();
     // 97.从隐藏域拿数据   98 MBookController
        if(optype == "update"){
            //更新数据时,提交时需要附加图书编号
            formData.bookId=$("#dlgBook #bookId").val();
        }
        //附加图书详细描述的图文html
        formData.description = description;
        //向服务器发送请求
        $.post("/management/book/" + optype , formData , function(json){
            if(json.code=="0"){
                //处理成功,关闭对话框,刷新列表,提示操作成功
                layui.layer.closeAll();
                table.reload('bookList');
                layui.layer.msg('数据操作成功,图书列表已刷新');
            }else{
                //处理失败,提示错误信息
                layui.layer.msg(json.msg);
            }
        } ,"json")
        return false;
    });
com/imooc/reader/controller/management/MBookController.java
//98 前台ajax发送的格式保持一致 /managemet/book/id/ + bookId
    @GetMapping("/id/{id}")
    @ResponseBody
    public Map selectById(@PathVariable("id") Long bookId) {
        Book book = bookService.selectById(bookId);
        Map result = new HashMap();
        result.put("code", "0");
        result.put("msg", "success");
        result.put("data", book); //服务器返回json.data book.ftl132行
        return result;
        //99 点击修改后的确认提交 增添更新操作 BookService.java
    }
com/imooc/reader/service/BookService.java
/**
     * 更新图书
     * @param book 新图书数据
     * @return 更新后的数据
     */
    //99.创建新的更新 BookServiceImpl
    public Book updateBook(Book book);
com/imooc/reader/service/impl/BookServiceImpl.java
/**
     * 更新图书
     * @param book 新图书数据
     * @return 更新后的数据
     */
    //99.更新数据 100 MBookController
    @Transactional
    public Book updateBook(Book book) {
        bookMapper.updateById(book);
        return null;
    }
com/imooc/reader/controller/management/MBookController.java
    /**
     * 更新图书数据
     * @param book
     * @return
     */
    //100. 更新代码 不要轻易对Book book数据直接更新 根据传入的book编号把数据库记录查出来
//101 删除方法BookService
    @PostMapping("/update")
    @ResponseBody
    public Map updateBook(Book book){
        Map result = new HashMap();
        try {
            Book rawBook = bookService.selectById(book.getBookId());
            //通过前台数据获取
            rawBook.setBookName(book.getBookName());
            rawBook.setSubTitle(book.getSubTitle());
            rawBook.setAuthor(book.getAuthor());
            rawBook.setCategoryId(book.getCategoryId());
            rawBook.setDescription(book.getDescription());
            Document doc = Jsoup.parse(book.getDescription());
            String cover = doc.select("img").first().attr("src");
            rawBook.setCover(cover);
            bookService.updateBook(rawBook);
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode()); ///成功时
            result.put("msg", ex.getMsg());//失败时
        }
        return result;
    }

实现图书删除功能

http://localhost/management/book/index.html
http://localhost/

com/imooc/reader/service/BookService.java
 //101.删除方法 BookServiceImpl
    public void deleteBook(Long book);
com/imooc/reader/service/impl/BookServiceImpl.java
//101.删除相关数据 上面增加memberReadStateMapper evaluationMapper
    //一次性三个表删除 有声明式事务不会删一半 102 MBookController.java
    /**
     * 删除图书及其数据
     * @param bookId 图书编号
     */
    @Transactional
    public void deleteBook(Long bookId) {
        bookMapper.deleteById(bookId);//单独删除id效率太低 写个构造器
        QueryWrapper<MemberReadState> mrsQueryWrapper = new QueryWrapper<MemberReadState>();
        mrsQueryWrapper.eq("book_id", bookId);
        memberReadStateMapper.delete(mrsQueryWrapper);
        QueryWrapper<Evaluation> evaluationQueryWrapper = new QueryWrapper<Evaluation>();
        evaluationQueryWrapper.eq("book_id", bookId);
        evaluationMapper.delete(evaluationQueryWrapper);
    }
com/imooc/reader/controller/management/MBookController.java
//102. 封装 以及 code输出  103加入后台首页=>index.ftl 后增加ManagementController
    @GetMapping("/delete/{id}")
    @ResponseBody
    public Map deleteBook(@PathVariable("id") Long bookId) {
        Map result = new HashMap();
        try {
            bookService.deleteBook(bookId);
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode()); ///成功时
            result.put("msg", ex.getMsg());//失败时
        }
        return result;
    }
增加后台框架

慕课书评网数据管理系统

com/imooc/reader/controller/management/ManagementController.java
package com.imooc.reader.controller.management;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
//103.后台管理系统控制器 104 修改index.ftl的静态地址
/**
 * 后台管理系统控制器
 */
@Controller
@RequestMapping("/management") //功能启动后台所需
public class ManagementController {
    @GetMapping("/index.html")
    public ModelAndView showIndex(){
        return new ModelAndView("/management/index");
    }
}
index.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>慕课书评网数据管理系统</title>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">
</head>

<body class="layui-layout-body">
<!-- Layui后台布局CSS -->
<div class="layui-layout layui-layout-admin">
    <!--头部导航栏-->
    <div class="layui-header">
        <!--系统标题-->
        <div class="layui-logo" style="font-size:18px">慕课书评网数据管理系统</div>
        <!--右侧当前用户信息-->
        <ul class="layui-nav layui-layout-right">
            <li class="layui-nav-item">
                <a href="javascript:void(0)">
                    <!--图标-->
                    <span class="layui-icon layui-icon-user" style="font-size: 20px">
                    </span>
                    <!--用户信息-->
                    admin
                </a>
            </li>
            <!--注销按钮-->
            <li class="layui-nav-item"><a href="/management/logout">注销</a></li>
        </ul>
    </div>
    <!--左侧菜单栏-->
    <div class="layui-side layui-bg-black">
        <!--可滚动菜单-->
        <div class="layui-side-scroll">
            <!--可折叠导航栏-->
            <ul class="layui-nav layui-nav-tree">


                    <li class="layui-nav-item layui-nav-itemed">
                        <a href="javascript:void(0)">数据管理</a>
                        <dl class="layui-nav-child module" data-node-id="xxx">
<#--       104.静态地址改为动态地址-->
                            <dd><a href="/management/book/index.html" target="ifmMain">图书管理</a></dd>
                            <dd><a href="短评管理.html" target="ifmMain">短评管理
                                </a></dd>
                        </dl>
                    </li>
            </ul>
        </div>
    </div>
    <!--主体部分采用iframe嵌入其他页面-->
    <div class="layui-body" style="overflow-y: hidden">
        <iframe name="ifmMain" style="border: 0px;width: 100%;height: 100%" src="/management/book/index.html"></iframe>
    </div>
    <!--版权信息-->
    <div class="layui-footer">
        Copyright © imooc. All Rights Reserved.
    </div>
</div>
<!--LayUI JS文件-->
<script src="/resources/layui/layui.all.js"></script>
<script>
    //将所有功能根据parent_id移动到指定模块下
    layui.$(".function").each(function () {
        var func = layui.$(this);
        var parentId = func.data("parent-id");
        layui.$("dl[data-node-id=" + parentId + "]").append(func);
    });
    //刷新折叠菜单
    layui.element.render('nav');
</script>
</body>
</html>
阅读全文

软件设计师刷题考点

2024/1/16

计算机工作原理

@@ 如果”2x”的补码是”90H”, 那么x的真值是 -56
90H 拆开根据8 4 2 1由十六进制转换为二进制:10010000;补码最高位为符号位, 1代表负号, 所以说明此数为负数,可以根据口诀直接得源码(注意符号位不变):1111 0000 即 -112;故2X=-112 X=-56

@@ 计算机中CPU对其访问速度最快的是通用寄存器
计算机系统中的CPU内部对通用寄存器的存取操作是速度最快的 其次是Cache 再次是内存 最慢的是作为外存的硬盘

@@ 属于CPU中算术逻辑单元的部位是**加法器**
控制单元部件:令寄存器、序计数器、地寄存器、据寄存器[紫橙子树] 加法器是算数逻辑运算单元的部件

@@ 在CPU中,常用来为ALU执行算数逻辑运算提供数据并暂存运算结果的寄存器是累加寄存器[是专门存放算术或逻辑运算的一个操作数和运算结果的寄存器(加 减 读出 移位)]

@@ 所谓定点数,就是表示数据时小数点的位置固定不变。
小数点的位置通常由两种约定方式:
①定点整数(纯整数, 小数点在最低有效数值位之后)
②定点小数(纯小数, 小数点在最高有效数值位之前)
当机器字长为n时, 定点数的补码和移码可表示$2^n$个数, 而其源码和反码只能表示$2^n-1$个数(0占用了两个编码), 所以定点数所能表示的数值范围比较小, 运算中很容易因结果超出范围而溢出
数的浮点表示的一般形式为:N=F×$2^E$, 其中E称为阶码,F为尾数。
尾数部分F的位数越多↑,数的精度越高↑;指数部分E的位数越多↑,能表示的范围越大↑
阶码通常为带符号的纯整数
尾数为带符号的纯小数
在尾数用补码表示时,规格化浮点数应满足尾数最高数位与符号位不同
1/2 ≤ |F|< 1时,应有0.1xx…x形式;
当**-1 ≤ M < -1/2时,应有1.0xx…x形式
M=-1/2**时,对于源码来说是规格化数,而对于补码来说不是规格化数

@@ 寄存器既可以用来存放数据地址,也可以存放控制信息CPU工作时的状态
在CPU中增加寄存器的数量,可以使CPU把执行程序时所需的数据尽可能地放在寄存器中,从而减少访问内存的次数,提高其运行速度

@@ 程序计数器(PC)用于存放下一条指令所在单元的地址的地方

@@ 计算机系统中采用补码来表示和运算数据,原因是采用补码可以简化计算机运算部件的设计
使用补码表示数据时,可以将符号位和其他位统一处理,减法也可按加法来处理,从而简化运算部件的设计

@@ 为了使一个系统能检查和纠正一个差错,间码最小距离必须至少是3
海明码是一种可以纠正一位差错的编码,是利用奇偶性来检错和纠错的一种校验方法;海明码的基本意思是給传输的数据增加r个校验位,从而增加两个合法消息的不同位的个数。海明码利用奇偶性进行检错纠错;其码距最小为2n+1 [海明校验码只有在代码中只存在一个错误的前提下,才能实现纠错 ]
**循环冗余校验码(CRC)**编码方法是在k位信息码后再拼接r位的校验码,形成长度位n位的编码,其特点是检错能力极强且开销小,用于编码器检测电路实现
奇偶校验是由若干位有效信息,再加上1 一个二进制位(校验位)组成校验码;奇偶校验只能查错不能纠错,只有奇数个数发生错误,才能发现错误;若有奇数个数据位出错,则可以检测出该错误但无法纠正错误

@@ 冗余技术:
1.结构冗余,按其工作方法可以分位静态动态混合冗余
2.信息冗余,指的是为了检测纠正信息在运算或传输中的错误另外附加的一部分信息
3.时间冗余,指的是以重复执行指令或程序来消除顺时错误带来的影响
4.冗余附件技术,指的是为实现上述冗余技术所需的资源和技术

@@ CPU依据指令周期的不同阶段来区分在内存中以二进制编码形式存放的指令和数据
指令周期是执行一条指令所需时间,一般由若干个机器周期组成,是从取指令、分析指令到执行完所需的全部时间

@@ 浮点数相加,对阶时,小数向大数看齐;对阶是通过较小数的尾数右移实现的。将小阶向大阶对齐,同时将尾数右移n位

@@ 在CPU的寄存器中,指令寄存器对用户是完全透明的

@@ 程序计数器是用于存放下一条指令所在单元的地址的地方

@@ 属于CPU中算数逻辑单元的部件是加法器

@@ 高速缓存地址是为了高速缓存的读取和写入而使用的地址,在CPU和高速缓存之间进行数据传输时使用的;当CPU需要读取数据时,DMA控制器和中断CPU发出的数据地址是主存的物理地址[中断向量提供的是中断服务程序的入口地址];硬盘的扇区地址是用于访问硬盘上指定扇区的地址,并不直接与CPU进行数据交互;虚拟地址,是指在操作系统中使用的地址,它需要通过地址映射和页表等机制转换为物理地址才能被CPU使用

@@ 相联存储器是指按内容访问的存储器
构成存储器的材料:磁存储器、半导体存储器、光存储器
按存储器的工作方式:读写存储器、只读存储器
访问方式:按地址访问的存储器、按内容访问的存储器
按**[寻]地址**访问的存储器:随机存储器、顺序存储器、直接存储器

@@ ±0编码相同的是补码和移码

@@ 计算机感染特洛伊木马后的典型现象是有未知程序试图建立网络连接

@@ 再微型计算机中,管理键盘最适合采用的1/0控制方式是中断方式

@@ 在移臂调度算法中,先来先服务最短寻找时间优先(根据当前磁臂到要请求访问磁道的距离,誰短满足誰的请求)算法可能会随时改变移动臂的运动方向

@@ 计算机中,系统总线用于CPU、主存及外设部件连接

@@ 双核是指在一个CPU中集成两个运算核心以提高运算能力

@@ 已知某高级语言源程序A经编译后得到机器C上的目标程序B,则对B进行反编译,不能还原出源程序A

@@ 在程序执行过程中,Cache与主存的地址映射是由硬件自动完成的

@@ 计算机系统的主存主要是由DRAM构成的。
随机访问存储器(RAM)有两类:静态的(SRAM) 和 动态的(DRAM)

@@ 存储系统采用Cache技术的主要目的是提高存储器的访问速度,因此是由硬件自动完成Cache与主存之间的操作

@@ 主存与Cache的地址映射方式中,全相联方式可以实现主存任意一块装入Cache中任意位置,只有装满才需要替换
全相联地址映射:主存的任意一块可以映射到Cache中的任意一块
直接相联映射:主存中一块只能映射到到Cache的一个特定的块中
组相联的映射:各区中的某一块只能存入缓存的同组号的空间内,但组内各块地址之间则可以任意存放。即从主存的组到Cache的组之间采用直接映像方式,在两个对应的组内部采用全相联映像方式

@@ 常用的虚拟存储器由主存-赋存两级存储器构成
虚拟存储技术使辅助存储器和主存储器密切配合。虚拟存储器的地址称为虚地址或逻辑地址

@@ 虚拟存储体系由主存-赋存两级存储器构成
一般计算机系统中主要由两种存储体系:
Cache存储体系Cache主存储器构成,主要目的是提高存储器速度,对系统程序员以上均透明
虚拟存储体系由主存储器和在线磁盘存储器等辅存构成,主要目的是扩大存储器容量,对应用程序员透明

@@ Cache的地址映像方式中,发生块冲突次数最小的是全相联映像
Cache的地址映射方法主要有三种:全相联地址映射、直接相联地址映射、组相联地址映射
**全相联地址映射 **[意味着主存的任意一块可以映射到Cache中的任意一块,特点:块冲突概率低,Cache空间利用率高,但相联目录表容量大导致成本高、查表速度慢]
直接映像方式 [指主存的每一块只能映像到Cache的一个特定的块中,整个Cache地址与主存地址的低位部分完全相同,其特点是硬件简单,不需要相联存储器,访问速度快,Cache块冲突概率高,Cache控件利用率低]
组相联映射 [是上面两种方法的这种处理]

@@ 计算机采用分级存储体系的主要目的是为了解决存储容量、成本和速度之间的矛盾问题
高速缓存一般用SRAM[速度快,成本高]
内存一般用DRAM[集成度高,动态刷新]
外存一般用磁存储器[速度慢,容量大,价格便宜]

@@ Cache的设计思想是在合理成本下提高命中率

@@ 并行总线适合近距离高速数据传输串行总线适合长距离数据传输
在单总线结构中,CPU与主存之间、CPU与I/O设备之间、I/O设备与主存之间、各种设备之间都通过系统总线交换信息。单总线结构的优点是控制简单方便,扩充方便

@@ 总线:数(数据总线) 控(控制总线) 地(地址总线) ISA EISA PCI MCA
SCSI是一种用于计算机和智能设备之间系统级接口的独立处理器标准

@@ 总线复用方式可以减少总线中信号线的数量
总线式一组能为多个部件分时共享的信息传送线,用来连接多个部件并为之提供信息交换通路,通过总线复用方式可以减少总线中信号线的数量,以较少的信号线传输更多的消息

@@ 在计算机系统中采用总线结构,便于实现系统的积木化结构,同时可以减少总线中信号线的数量

@@ 直接主存存取(DMA)[Direct Memory Access]控制方式是在主存与外设之间直接建立数据通路进行数据的交换处理

@@ 在I/O设备与主机间进行数据传输时,CPU只需在开始和结束时作少量处理,而无需干预数据传送过程的是直接存储器存取 [DMA]方式。[直接内存存取(Direct Memory Access)方式的基本思想是通过硬件控制实现主存与**I/O设备**间的直接数据传送,数据的传递过程由DMA控制器进行控制,不需要CPU的干预。在DMA方式下由CPU启动传送过程,即向设备发出”传送一块数据”的命令,在传送过程结束时,DMAC通过中断方式通知CPU进行一些后续处理工作]

@@ 在微机系统中,BIOS(基本输入输出系统)保存在主板上的ROM中 [BIOS是一组固化到计算机内主板上一个ROM芯片上的程序,它保存着计算机最重要的基本输入输出的程序、开机后自检程序和系统自启动程序,它可从CMOS中读写系统设置的具体信息]

@@ 计算机运行过程中,遇到突发事件,要求CPU暂时停止正在运行的程序,转去为突发事件服务,服务完毕,再自动返回原程序继续执行,这个过程成为中断,其处理过程中保存现场的目的是返回去继续执行原程序

@@ CPU是再一个总线周期结束时响应DMA请求的 [DMA请求的检测点设置在每个机器周期也即总线周期结束时执行,这样使得总线利用率最高]

@@ 计算机中CPU的中断响应时间指的是从发出中断请求到开始进入中断处理程序的时间

@@ 中断向量可提供中断服务程序的入口地址[为了提高响应中断的速度,通常把所有中断服务程序的入口地址[称中断向量]汇聚为中断向量表]

@@ 为了便于实现多级中断嵌套,使用堆栈来保护断点和现场最有效 【实现中断嵌套后进先出的栈来保护断点和现场最有效(当系统中有多个中断请求时,中断系统按优先级进行排队。若在处理低级中断过程中又有高级中断申请中断,则高级中断可以打断低级中断处理,转去处理高级中断,等处理完高级中断后再返回去处理原来的低级中断,称为中断嵌套)

@@ 由I/O设备提出的中断请求是可屏蔽中断,电源掉电是不可屏蔽中断
可将中断分为两大类:
不可屏蔽中断(非屏蔽中断) 和 可屏蔽中断
不可屏蔽中断源[断电]一旦提出请求,CPU必须无条件响应,而对可屏蔽中断源[打印机中断(处于等待请求响应)]的请求,CPU可以响应,也可以不响应。

@@ 计算机在一个指令周期过程中,为从内存读取指令操作码,首先要将**程序计数器(PC)**的内容送到地址总线上
CPU首先从程序计数器获得需要执行的指令地址,从内存(或高速缓存)读取到的指令则暂存在指令寄存器(IR),然后进行分析和执行

程序计数器是PC用于存放下一条指令所在单元的地址的地方
指令寄存器是临时放置从内存里面取得的程序指令的寄存器,用于存放当前从主存储器读出的正在执行的一套指令
地址寄存器用来保存当前CPU所访问的内存单元的地址
指令译码器从内存中取出一条指令经数据总线送往指令寄存器中

@@ 对于只有单条指令的情况下,流水线方式与顺序执行时没有区别的。流水线的原理是在某一时刻可以让多个部件同时处理多条指令,避免各部件等待空闲,由此提高了各部件的利用率,也提高了系统的吞吐率

@@ 流水线的吞吐率是指单位时间流水线处理机输出的结果的数目,因此流水线的吞吐率为一个流水级时间的倒数,即最长流水级时间的倒数

@@ CPU执行算术运算或者逻辑运算时,常将源操作数和结果暂存在累加器(AC)
CPU中常设置多个寄存器,其中,
程序计数器
的作用是保存待读取指令在内存中的地址,累加器是算数逻辑运算单位中用来残存源操作数和计算结果的寄存器,指令寄存器暂存从内存读取的指令,地址寄存器暂存要访问的内存单元的地址。

@@ VLIW(Very Long Instruction Word, 超长指令字) 一种非常长的指令组合,它把许多条指令连在一起,增加了运算的速度

@@ 在机器指令的地址字段中,直接指出操作数本身的寻址方式称为立即寻址[是一种特殊的寻址方式 指令中在操作码字段后面的部分不是通常意义的操作数,而是操作数本身,也就是说数据就包含在指令中,只要取出指令,也就是去除了可以立即使用的操作数]

@@ 若CPU要执行的指令为:MOV R1,#45(即将数值45传递到寄存器R1中),则该指令中采用的寻址方式是寄存器寻址和立即寻址 [45是立即数,R1是寄存器]
立即寻址:操作数就包含在指令中
直接寻址:操作数存放在内存单元中,指令中直接給处操作数所在内存单元的地址
寄存器寻址:操作数存放在某一寄存器中,指令中给出存放操作数的寄存器名
寄存器间接寻址:操作数存放在内存单元中,操作数所在存储单元的地址在某个寄存器中
间接寻址:指令中给出操作数地址的地址
相对寻址:指令地址码给出的是一个偏移量可正可负,操作数地址等于本条指令的地址加上该偏移量
变址寻址:操作数地址等于变址寄存器的内容加偏移量

@@ CISC(Complex Instruction Set Computer)是复杂指令系统计算机的简称 其基本思想是:进一步增强原有指令的功能,用更为复杂的新指令取代原先由软件子程序完成的功能,实现软件功能的硬件化,导致机器的指令系统越来越庞大而复杂。CISC计算机一般所含的指令数目至少300条以上
RISC(Reduced Instruction Set Computer 精简指令集计算机) 其基本思想是:通过减少指令总数和简化指令功能,降低硬件设计的复杂度,使指令能单周期执行,并通过优化编译提高指令的执行速度,采用硬布线控制逻辑优化编译程序。 通常会比CISC配置更多的寄存器、RISC编译器的子程序库通常要比CISC编译器的子程序库大很多、RISC比CISC更加适合VLSI工艺的规整性要求。

@@ Flynn分类法基于信息流特征将计算机分成4类,其中MISD只有理论意义而无实例
Flynn主要根据指令流和数据流来分类:
① 单指令流单数据流机器(SISD)
② 单指令流多数据流机器(SIMD)
③ 多指令流单数据流机器(MISD)[Multi Instruction Single Data] [采用多个指令流来处理单个数据流 在实际情况中,采用多指令流处理多数据流才是最有效的方法,因此MISD只是作为理论模型出现,没有投入实际应用]
④ 多指令流多数据流机器(MIMD)

@@ 取指令时间4Δt,分析时间为2Δt,执行时间为3Δt,按顺序方式从头到尾执行完600条指令所需时间为5400Δt,使用流水线从头到尾执行完600条指令所需的时间为2405Δt [未说流水线就普通乘法计算 流水线则按公式计算]
① 指令顺序执行时,每条指令需要(4Δt+2Δt+3Δt)=9Δt 9Δt×600=5400Δt
② (4Δt+2Δt+3Δt) + (600-1) × 4 = 2405Δt

@@ 指令系统中采用不同寻址方式的目的是扩大寻址控件并提高编程灵活性

@@ 计算机指令一般包括操作码和地址码两部分,为分析执行一条指令,其操作码和地址吗都应存入指令寄存器(IR) [程序被加载到内存后开始运行,当CPU执行一条指令时,先把它从内存器取到缓冲寄存器DR中,再送入IR暂存,指令译码器根据IR的内容产生各种微操作指令,控制其他的组成部件工作,完成所有功能。]

@@ CPU产生每条指令的操作信号并将操作信号送往相应的部件进行控制、CPU中的控制器决定计算机运行过程的自动化、指令译码器是CPU控制器中的部件
CPU是计算机的控制中心,主要由运算器、控制器、寄存器组、和内部总线等部件组成。
控制器由程序计数器、指令寄存器、指令译码器、时序产生器和操作控制器组成

@@ 可靠度串并联:串联系统可靠度为R1×R2,并联系统可靠度为R1/R2=1-(1-R)

@@ 计算机系统的可靠性可以用**MTBF / (1+MTBF)**来度量,其中MTBF为平衡失效间隔时间 [又称平均无敌故障时间] 是衡量一个产品(尤其是电器产品)的可靠性指标,单位为”小时”

@@ 软件产品的可靠性并不取决于软件产品的开发方式 而取决于潜在的错误数量和位置,软件产品的使用方式;软件可靠性指的是一个系统对于给定的时间间隔内,在给定条件下**无失效运作的概率**。

@@ 软件可靠性是指一个系统在给定时间间隔内和给定条件下无失败运行的概率,一个软件系统能够按照规格说明正确运行的概率。软件可维护性是在给定的使用条件下,在规定的时间间隔内,使用规定的过程和资源完成维护活动的概率

@@ 应用级网关防火墙是内部网和外部网的隔离点,它可对应用层的通信数据流进行监控和过滤。应用级网关可以工作在OSI七层模型上的任意一层,能够检查进出的数据包,通过网关复制传递数据,防止在受信任服务器和客户机与不受信任的主机之间直接建立联系。

@@ 用户A和B要进行安全通信,通信过程需确认双方身份和消息不可否认,A和B通信时使用数字证书来对用户的身份进行认证;使用数字签名确保消息不可否认

@@ 震网病毒是一种破坏工业基础设施的恶意代码,利用系统漏洞攻击工业控制系统,是一种危害性极大的蠕虫病毒蠕虫(Worm)是一个程序或程序序列。它利用网络进行复制和传播,传染途径是通过网络、移动存储设备和电子邮件。最初的蠕虫病毒定义是在DOS环境下,病毒发作时会在频幕上出现一条类似虫子的东西,胡乱吞吃屏幕上的字母并将其改正。常见的蠕虫病毒有:快乐时光、红色代码、爱虫病毒、熊猫烧香、Nimda病毒、爱丽兹病毒CIH:系统病毒 X卧底:以木马形式传播目标为智能手机的病毒
引导区病毒:破坏的是引导盘、文件目录
宏病毒[Macro]:破坏的是OFFICE文件相关(病毒宏将自身复制至Word的通用Normal模板中)
木马:一般强调控制操控 [冰河是木马软件,主要用于远程监控。冰河木马后经其他人多次改写形成多种变种,并被用于入侵其他用户的计算机木马程序]

@@ Sniffer(嗅探器)是一种基于被动侦听原理的网络分析方式。使用这种技术方式可以监视网络的状态、数据流动情况以及网上传输的信息。它不是木马程序

@@ 属于DoS攻击的是SYN Flooding攻击 [DoS是Denial of Service的简称,即拒绝服务,造成DoS的攻击行为被称为DoS攻击,其目的是使计算机或网络无法提供正常的服务。最常见的DoS攻击有计算机网络宽带攻击和连通性攻击。]
特洛伊木马[Trojan]是附着在应用程序中或者单独存在的一些恶意程序,它可以利用网络远程控制网络的另一端的安装有服务端服务的主机,实现对被植入了木马程序的计算机的控制,或者窃取被植入了木马程序的计算机上的机密资料。
拒绝服务攻击通过网络的内外部用户来发动攻击。内部用户可以通过长时间占用系统的内存、CPU处理时间使其他用户不能及时得到这些资源,而引起拒绝服务器攻击;外部黑客也可以通过占用网络连接使其他用户得不到网络服务 SYN Flooding攻击以多个随机的源主机地址向目的路由器发送SYN包,在收到目的路由器的SYN ACK后并不回应,于是目的路由器就为这些源主机建立大量的连接队列,由于没有收到ACK一直维护着这些队列,造成了资源的大量消耗而不能向正常请求提供服务,甚至导致服务器崩溃。服务器要等待超时才能断开已分配的资源,所以SYN Flooding攻击是一种DOS攻击
端口欺骗攻击是采用端口扫描到系统漏洞从而实施攻击
IP欺骗攻击是产生的IP数据包为伪造的源IP地址,以便冒充其他系统或发件人的身份

@@ DES是共享密钥加密算法;常见的对称加密算法有:DES、三重DES、RC-5、IDEA、AES
共享密钥加密对称加密
非共享密钥加密指公开密钥加密

@@ 计算机病毒的特征:隐蔽性、传染性、潜伏性、触发性、破坏性

@@ MD5是摘要算法,对任意长度的输入计算得到的结果长度是128未散列值

@@ 攻击者通过发送一个目的主机已经接受过的报文来达到攻击目的,这种攻击方式属于重放攻击
**重放攻击(Replay Attacks)**又称重播攻击、回放攻击,是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。重放攻击可以由发起者,也可以由拦截并重发该数据的敌方进行

@@ kerberos系统中可通过在报文中加入时间戳来防止重放攻击。发送的数据包带时间戳的,服务器可以根据时间戳来判断是否为重放包,以此防止重放攻击

@@ 防火墙最基本的功能就是控制在计算机网络中,不同信任程度区域间传送的数据流。防火墙对流经它的网络通信进行扫描,这样能够过滤掉一些攻击,以免其在目标计算机上被执行。防火墙还可也关闭不适用的端口,屏蔽内部细节。所有的访问都经过防火墙,防火墙就能记录下这些访问并做出日志记录,同时也能提供网络使用情况的统计数据

@@ 防火墙的性能及特点:
工作层次:决定防火墙效率及安全的主要因素,工作层次越低,则工作效率越高,其安全性就越低
防火墙机制:采用代理机制,则防火墙具有内部信息隐藏的特点,相对而言,安全性越高,效率越低。如果采用过滤机制,则效率高,安全性却降低

@@ 包过滤防火墙和代理服务防火墙的叙述中正确的是包过滤技术对应用和用户是透明的。包过滤技术是一种基于网络层、传输层的安全技术,优点是简单实用,实现成本较低同时,包过滤操作对于应用层来说是透明的,使用此类防火墙时外部网络与内部网络之间不存在直接连接,即使防火墙发生了问题,外部网络也无法被保护的网络连接。

@@ 包过滤防火墙对数据包的过滤依据包括源IP地址、源端口号、目标IP地址、目标端口号

@@ 防火墙通常分为内网、外网、DMZ三个区域
按照受保护程序[安全级别]从高到低正确排列顺序依次为内网、DMZ、外网
① 内网可以访问外网、内网可以访问DMZ
② 外网可以访问DMZ、外网不能访问内网
③ DMZ不能访问外网、DMZ不能访问内网

@@ 入侵检测技术包括专家系统、模型检测、简单匹配 [入侵检测流量控制属于网络安全措施]

@@ 这四种加密算法均能对明文进行加密。
RSA是一种非对称加密算法,由于加密和解密的密钥不同,因此便于密钥管理和发放,同时用户或机构之间进行身份认证方面有较好的应用 [基于大数定律,通常用于对消息摘要进行签名(数字签名算法) 而IDEA和RC4适用于对数据传输加密]
SHA-1是一种安全散列算法,常用于对接接收到的明文输入产生固定长度的输出,来确保明文在传输过程中不会被篡改
MD5是一种使用最为广泛的报文摘要算法(摘要算法)
RC5是一种用于对明文进行加密的算法,在加密速度和强度上均较为合适,适用于大量明文进行加密并传输

@@ 假定用户A、B分别为I1和I2两个CA处取得了各自的证书,下面I1、I2互换公钥是A、B互信的必要条件

@@ MIME是一个互联网标准,扩展了电子邮件标准,使其能够支持,与安全无关。
SSLHTTPS涉及到邮件传输过程的安全
PGP是一套用于信息加密、验证的应用程序,可用于加密电子邮件内容

@@ 数字签名用于通信的A、B双方,使得A向B发送签名的消息P,提供以下服务:
① B可以验证消息P确实是来源于A
② A不能否认发送过消息P
③ B不能便在或修改消息P
数字签名首先需要生成消息摘要,使用非对称加密算法以及私钥对摘要进行加密。接收方使用发送方的公钥对消息摘要进行验证。

@@ 用户B收到用户A带数字签名的消息M,为了验证M的真实性,首先需要从CA获取用户A的数字证书,并利用CA的公钥验证该证书的真伪,然后利用A的公钥验证M的真实性 [验证证书的真伪需要用CA的公钥验证CA的签名,验证M的真实性需要用用户A的公钥验证用户A的签名]

@@ 从认证中心CA获取用户B的数字证书,该证书用CA的私钥数字签名从用户B的数字证书中可以获得B的公钥CA公钥→合法性、CA签名→真伪、CA私钥→数字签名

@@ 公钥体系中,私钥用于解密和签名,公钥用于加密和认证

@@ 某网站向CA申请了数字证书,用户通过CA的签名来验证网站的真伪,通过使用CA的公钥来确定该网站的合法性

@@ 设置防雷击属于物理线路安全措施、入侵检测, 流量控制属于网络安全措施、漏洞发现于补丁管理属于系统安全措施

@@ 网络攻击主动攻击被动攻击两类,主动攻击是指通过一系列方法主动向被攻击对象试试破坏的一种攻击方式,主动攻击有:重放攻击、IP地址欺骗、拒绝服务流量分析攻击[被动攻击]是通过持续检测现有网络中的流量变化或者变化趋势,而得到相应信息的一种被动攻击方式

@@ 端口443明确用于HTTPS服务,因此是HTTPS(加密)流量的标准端口。 它也称为HTTPS端口443,因此所有受保护的事务都是使用端口443进行的。 您可能会惊讶地发现,几乎95%的安全站点都使用端口443进行安全传输。80端口是HTTP超文本传输协议

Proto Local Address Foreign Address State
TCP 192.168.0.200:2011 202.100.112.12:443 ESTABLISHED
TCP 192.168.0.200:2052 128.105.129.30:80 ESTABLISHED
TCP 192.168.0.200:2038 100.29.200.110:110 TIME_WAIT

① 其中已经与主机202.100.112.12的443端口建立连接,由于443端口主要用于HTTPS服务,是提供加密和通过安全端口传输的另一种HTTP协议,是建立了安全连接
② 其中已经与主机128.105.129.30的80端口建立了普通连接。
③ 其中与主机100.29.200.110的110端口正在等待建立连接

@@ SSH(Secure Shell)是终端设备远程站点之间建立安全连接的协议,是专为远程登录会话和其他网络服务提供安全性的协议,利用SSH协议可以有效防止远程管理过程中的信息泄露问题,SSH最初是UNIX上的程序

@@ 机房安全属于物理安全、入侵检测属于网络安全、漏洞补丁管理属于系统安全、数据库安全属于应用安全

@@ 网络系统中,通常把Web服务器置于DMZ区 [DMZ是为了解决安装防火墙后外部网络不能访问内部网络服务器的问题,而设立的一个非安全系统与安全系统之间的缓冲区。这个缓冲区位于企业内部网络和外部网络之间的小网络区域内,在这个小网络区域内可以防止一些必须公开的服务器设施,如企业Web服务器、FTP服务器和论坛]

@@ 拒绝服务攻击即攻击者想办法让目标机器停止提供服务,这是黑客常用的攻击手段之一,其实对网络带宽进行的消耗性攻击只是拒绝服务攻击的一小部分,只要能够对目标造成麻烦,使某些服务被暂停甚至主机死机,都属于拒绝服务攻击。攻击者进行拒绝服务攻击,实际上让服务器实现两种效果:一是迫使服务器的缓冲区满,不接收新的请求。二是使用IP欺骗,迫使服务器把合法用户的连接复位,影响合法用户的连接

@@ PKI体制中,保证数字证书不被篡改的方法是用CA的私钥对数字证书签名[防伪造,不可抵赖]

@@ ECC、DSA、RSA属于公开密钥加密算法 DES属于私钥加密体制

@@ 利用报文摘要算法生成报文主要的目的是防止发送的报文被篡改 [报文摘要是用来保证数据完整性的,传输的数据一旦被修改,摘要就不同了,只要对比两次摘要就可以确定数据是否被修改过]

@@ TLS是安全传输层协议的简称,用于在两个通信应用程序之间提供保密性和数据完整性
SSL是安全套接层协议的简称,是一种为网络通信提供安全和数据完整性的协议,它与TLS非常相似,它们都是在传输层对网络连接进行加密
PGP是基于RSA公钥加密体系的邮件加密软件,用它可以对邮件保密以防止非授权者阅读
HTTPS即安全版的HTTP(超文本传输协议),它是在HTTP下加入SSL层,HTTPS的安全基础就是SSL
IPSec网络层的安全协议,它通过使用加密的安全服务来确保在网络上进行保密而安全的通讯

@@ 在Windows系统中,默认权限最低的用户组是everyone
用户组默认权限由高到低的顺序是administrators > power users > users > everyone

@@ IIS6.0支持的身份验证安全机制有[.NET Passport身份验证、集成Windows身份验证、摘要式身份验证、基本身份验证]安全级别最高的验证方法是集成Windows身份验证

@@ 利用漏洞扫描系统可以获取某FTP服务器中是否存在可写目录的信息 [通过与目标主机TCP/IP端口建立连接并请求某些服务(TELNET、FTP),记录目标主机的应答,搜索目标主机相关信息,从而发现目标主机某些内在的安全弱点]

@@ 防范网络监听最有效的方法是数据加密

程序语言

@@ LISP是一种通用高级计算机程序语言、C是命令式过程式语言、Java面向对象语言、C#结构化程序设计语言、Java,Python,JavaScript都是解释型程序设计语言[其中python[弱类型语言]和javascript是脚本语言(主要采用解释方式实现)]
强/弱类型指的是语言类型系统的类型检查的严格程度。弱类型相对于强类型来说类型检查更不严格, 比如允许变量类型的隐式转换,允许强制类型转换

@@ 语法制导翻译是一种静态语义分析方法。程序设计语言的语义分为静态语义动态语义,其中静态语义分析方法是语法制导翻译,其基本思想是将语言结构的语义以属性的形式赋予代表此结构的文法符号,而属性的计算以语义规则的形式赋予文法的产生式。

@@ 常用的函数参数传递方式有传值传引用两种;在传引用方式下,修改形参实质上改变了实参的值。[传值调用方式下,是将实参的值传递给形参,该传递是单方向的,结束调用后不会再将形参的值传递給实参引用调用方式下,实质上是将实参的地址传递給形参,借助指针的间接访问数据方式下(或者将形参看作是实参的别名),在被调用函数中对形参的修改实质上是对实参的修改。]

@@ 在值调用方式下是将参数的值传給形参。在引用调用方式下是将实参的地址传递给形参

@@ 在传值调用方式下,实参可以是变量,也可以是常数和表达式
引用调用方式下,可以是实现形参和实参间双向传递数据的效果

@@ 传地址方式下,实参的地址传給形参,因此,实参必须有地址

@@ 函数中print(x)执行后输出的值为40

main(){                      f(int x, int &a){
  int x = 5;                    x = x * x - 1;
  f(x+1,x);                        a = x + a;
  print(x);                        return;
}                               }

@@ 函数中return(a-x)执行后输出的值为33

@@ 编译器对高级语言源程序的处理过程:
源程序 → 词法分析 → 语法分析 → 语义分析 → 中间代码生成 → 代码优化 → 目标代码生成 → 目标代码
其中中间代码生成和代码优化并不是每个编译器都必需的,与编译器相比,解释器参与运行控制,程序执行的速度慢

词法分析:根据语言的词法规则,对源程序进行逐个字符地描述,从中识别出一个个单词符号,针对词汇的检查
语法分析:在词法分析的基础上,根据语言的语法规则将单词符号序列分解成各类语法单位;
语义分析:分析各语法结构的含义,检查源程序是否包含语义错误,主要针对句子含义的检查;

@@ 语法分析阶段的输入是记号流,若程序中的符号不匹配,则会在语法分析阶段检查出错误
记号流:词法分析的输出是记号流,也就是语法分析的输入。
源程序:词法分析的任务是把源程序的字符串转换成单词符号序列
分析树:如果没有语法错误,语法分析后就能正常的构造出语法树

@@ 语法错误是指语言结构上的使用错误,是指编译时所发现的程序错误,如单词拼写错误、标点符号错误、表达式中缺少操作数、符号不匹配等有关语言结构上的错误 [语法分析阶段可以发现程序中所有的语法错误、但是不能在语义分析阶段发现程序中的所有语义错误(循环越界)]

@@ 若程序在运行时陷入死循环,则情况属于动态的语义错误;在编译过程中,进行类型分析和检查是语义分析阶段的一个主要工作。**死循环,零除数,其他逻辑错误是属于动态语义错误。语义检查时不检查动态语义,因此编译正确的程序不包含语法错误**

@@ 高级程序设计语言不依赖具体的机器硬件;程序中局部变量的值在运行时可以改变,局部函数只能在函数内部使用,其作用域是从定义位置起至函数体或复合语句体结束位置。

@@ 在编写C/C++时,若表达式中引用的变量从定义到使用始终没有赋值,则该变量中的值表现为一个随机数,这样对表达式的求值结果就是不确定的了。故[可以通过编译并运行,但运行结果不一定是期望的结果]

@@ 关于高级程序设计语言翻译的叙述中正确的是目标代码生成阶段的工作与目标机器的体系结构密切相关

@@ 变量与常量:变量具有类型属性,常量则没有 [常量在程序运行过程中不能修改]

@@ 可视化程序设计是以”所见即所得“的编程思想为原创,力图实现编程工作的可视化,即随时可以看到结果,程序与结果的调整同步。可视化程序设计仅通过直观的操作方式即可完成界面的设计工作[其基于面向对象的思想,引入了控件的概念和事件驱动;程序开发遵循步骤:即先进行界面的绘制工作,再基于事件编写程序代码,以响应鼠标、键盘的各种动作]。可视化程序设计最大的有点是设计人员可以不用编写或只需要编写很少程序代码,就能完成应用程序的设计,极大提高设计人员的工作效率

@@ PHP不是标记语言,PHP,全称 Hypertext Preprocessor ,中文翻译“超文本预处理器”。是在 服务器 端执行的 脚本语言 ,尤其适用于 Web 开发并可嵌入 HTML 中。

@@ 创建指向邮箱地址的连接**<a href = "mailto:test@test.com"> test@test.com </a>** 使用**<mailto>**标签定义一个指向电子邮件地址的超级链接

@@ 指针变量可以是全局变量也可以是局部变量

@@ 若一个程序语言可以提供链表的定义和运算,则其运算时的数据空间必须采用堆存储分配策略

@@ 动态语言是指程序在运行时可以改变其结构,例如新的函数可以被引进、已有的函数可以被删除等,在结构上的变化。动态语言的类型检查是在运行时进行的。优点是方便阅读,不需要写非常多的与类型相关的代码;缺点是不方便调试,命名不规范时会读不懂、不利于理解。事实上脚本语言都是动态语言,而动态语言都是解释型语言,不管它们是否是面向对象的语言 ;C语言属于静态语言,其所有成分可在编译时确定

@@ HTML<body>元素中,vlink属性用于定义超链接被鼠标点击后所显示的颜色
alink:用于设置正在被击中的链接的颜色
vlink:用于设置已使用的链接的颜色
bgcolor:用于设置文档整体背景颜色
background:用于设置背景图片的URL

@@ XML文档语法规范中:标记之间不可以交叉嵌套

@@ 动态绑定是指在运行时把过程调用和响应调用所需要执行的代码加以结合。运行时结合动态绑定编译时结合静态绑定

@@ 汇编程序的功能是将用汇编语言写的源程序翻译成机器指令程序。汇编程序的基本工作包括将一条可执行汇编语句转换成对应的机器指令;处理源程序中出现的伪指令。由于汇编指令中形成操作数指令的部分可能出现后面才会定义的符号,所以汇编程序一般需要两次扫描源程序才能完成翻译过程

@@ 对高级语言源程序进行编译或解释处理的过程中,需要不断收集、记录和使用源程序中一些相关符号的类型和特征等信息,并将其存入符号表 [这些信息一般以表格形式存储于系统中]

@@ 将高级语言源程序翻译称目标程序的是编译程序

@@ 将高级语言源程序翻译为可在计算机上执行的形式有多种不同的方式,其中编译方式生成逻辑上与源程序等价的目标程序,解释方式不生成[为源程序中变量所分配的存储单元的地址属于逻辑地址]
编译语言是一种以编译器来实现的编程语言,它不像直译语言一样,由解释器将代码一句一句运行,而是以编译器,先将代码编译为机器码,再加以运行。将某一种程序设计语言写的程序翻译成等价的另一种语言的程序的程序,称为编译程序

@@ 将高级语言源程序通过编译或解释方式进行翻译时,可以先生成与源程序等价的某种中间代码。后缀式和三地址码是常用的中间代码。[不同高级程序语言可以产生同一种中间代码]
中间代码的作用是可使程序的结构再逻辑上更为简单明确(有利于进行与机器无关的优化处理,优化一般建立在对程序的控制流和数据流分析的基础之上, 与机器无关),特别是可以使目标代码的优化比较容易实现。中间代码由很多形式,常见的由**逆波兰记号(后缀式)、四元式、三元式(三地址码)、语法树,它们的共同特点是与具体的机器无关,不依赖于具体的计算机。中间代码**是源程序的一种内部表示,称中间语言。中间代码不依赖于具体的机器、使用中间代码可提高编译程序的可移植性、中间代码可以用树或图表示

@@ 编译程序不参加与用户程序的运行控制,而解释程序则参与。编译程序(编译器)则是将源程序翻译成目标语言程序,然后再计算机上运行目的程序。再解释方式下。翻译源程序时不生成独立的目标程序,而编译器则将源程序翻译成独立保存的目标程序。

@@ 编译和解释是实现高级程序设计语言翻译的两种基本形式,在编译方式下,必须进行词法、语法和语义分析,然后再产生源程序的目标代码

@@ 对高级语言源程序进行编译的过程可分为多个阶段,分配寄存器的工作再目标代码生成阶段进行。[目标代码生成是编译器工作的最后一个阶段,这一阶段的任务是把中间代码变换成特定机器上的绝对指令代码、了重定位的指令代码或汇编指令代码,这个阶段的工作与具体的机器密切相关,因此在目标代码生成阶段分配寄存器。]

@@ 汇编程序输入的是用汇编语法书写的源程序,输出的是用机器语言表示的目标程序[汇编语言源程序中的指令语句将被翻译成机器代码汇编程序以汇编语言源程序为输入,以机器语言表示的目标程序为输出汇编语言的指令语句必须具有操作码字段,可以没有操作数字段]

@@ 编译过程中,对高级语言程序语句的翻译主要考虑声明语句和可执行语句。对声明语句,主要是将所需要的信息正确地填入合理组织的符号表中;对可执行语句,则是翻译成中间代码或目标代码

@@ 编译解释是语言处理的两种基本方式:
编译过程包括(词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成等阶段),以及**符号表管理出错处理模块**
解释过程在词法、语法和语义分析方面与编译程序的工作原理基本相同,但是在运行用户程序时,它直接执行源程序或源程序的内部形式
这两种语言处理程序的根本区别:在编译方式下,机器上运行的是与源程序等价的目标程序,源程序和编译程序都不再参与目标程序的执行过程;而在解释方式下,解释程序和源程序(某种等价表示)要参与到程序的运行过程中,运行程序的控制权在解释程序。解释器翻译源程序时不产生独立的目标程序,而编译器则需要将源程序翻译成独立的目标程序

@@ 移进—归约分析法是编译程序(解释程序)对高级语言源程序进行语法分析的一种方法,属于自下而上的语法分析方法
递归下降分析法、预测分析法 → 是自上而下分析法
移进-归约分析法 → 是**自下而上**分析法

@@ 已知某高级语言源程序A经编译后得到机器C上的目标程序B,则对B进行反编译,不能还原出源程序A
编译是将高级语言源程序翻译成机器语言程序,反编译是编译的逆过程反编译通常不能把可执行文件还原成高级语言源程序,只能转换成功能上等价的汇编程序

@@ 对于表达式a or ((c<d) and b),当a为true时可进行短路计算,最后计算的是or,对 或运算 ,只要有一个真则结果为真,此题当a为true时,可进行短路计算,直接得到后面的结果

@@ 文法分为4种类型,程序设计语言的大多数语法现象可用乔姆斯基上下文无关文法描述

@@ 在仅由字符a、b构成的所有字符串种,其中以b结尾的字符串集合可用正则表达式为
$(b|ab)^b$表示的字符串集合{b,bb,abb,bbb,abab,bbbb,abbb,babb,...}除了以b结尾, 还要求每个a后面至少有1个b 不符合题意只要有b结尾的!!
$(a|b)^
b$ 表示的字符串集合{b,ab,bb,aab,abb,bab,bbb,aaab,aabb,abab,abbb,baab,babb,bbab,...}

@@ 表达式采用逆波兰式表示时,利用进行求值
后缀式(逆波兰式)表达方式把运算符写在运算对象后面,**把a+b写成ab+**,所以也称为后缀式。借助栈可以方便地对后缀式进行求职。

@@ 与算术表达式**(a+(b-c))*d**对应的树是 求值运算处理顺序是:先进行b-c,然后与a相加,最后再与d相乘[中序遍历二叉树即可得出]

@@ 算数表达式**(a-b)(c+d)的后缀式是ab-cd+** 后缀式(逆波兰式)

@@ 算术表达式*a+(b-C)*d**的后缀式是abc-d+** 利用逆波兰式,但是要把得到的步骤式子当成整体
原式=a+bC-*d =a+bC-d*=abC-d*+ 其中的着重点在于把bC-变成一个整体bC- * d再用逆波兰式 bC-d*
算术表达式**(a-b)c+d的后缀式是ab-cd+**

@@ 逻辑表达式"a^bvc^(bvx>0)" 的后缀式是**ab^cbx0>v^v**
此题有大坑 ^是"与"门的意思、v是异"或"门的意思
逻辑与运算的优先级高于逻辑或运算
逻辑与运算“ 表达式 “x^y“的短路求值逻辑是:若x为假,则可知”x^y“的值为假,无需再对y求值。因此只有在x为真时继续对y求值
逻辑或运算“ 表达式 “xvy“的短路求值逻辑是:若x为真,则可知”xvy“的值为真,无需再对y求值。因此只有在x为假时继续对y求值
对于逻辑表达式"a^bvc^(bvx>0)",从运算符的优先级方面考虑需先对”a^b“求值。然后对”c^(b^x>0)“求值,最后进行对"v"运算,因此后缀式是**ab^cbx0>v^v**

@@ 递归下降分析方法是一种自上而下的语法分析
程序式设计语言的绝大多数语法规则可以采用上下文无关文法进行描述。语法分析方法有多种,根据产生语法树的方向,可分为自底向上[算符优先分析法、LR分析法] 自顶向下[递归下降分析法、预测分析法]两类。

@@ 对于大多数通用程序设计语言,用上下文无关文法描述其语法即可
文法分成四种类型:0型、1型、2型、3型。
0型文法也称为短语文法,其能力相当于图灵机,任何0型语言都是递归可枚举的;反之,递归可枚举集也必定是一个0型语言
1型文法也称为上下文有关文法,这种文法意味着对非终结符的替换必须考虑上下文
2型文法也称为上下文无关文法,非终结符的替换无需考虑上下文【通用程序设计语言绝大多数使用上下文无关文法(此文法拥有足够强的表达能力来表示大多数程序设计语言的语法)】
3型文法等价于正规式,因此也被称为正规文法或线性文法。

@@ 在c/c++程序种,整型变量a的值为0且应用在表达式”c=b/a”种,则最可能发生的情形是运行时产生异常
c=b/a符合c/c++语言的语法逻辑,编译时不会报错,运行时,代入a的值,发生错误

@@ 对高级语言源程序进行编译过程中,有限自动机(NFA或DFA)是进行词法分析的适当工具
词法分析的任务是把构成源程序的字符串转换成单词符号序列。有限自动机是一种识别装置的抽象概念,它能准确地识别正规集。有限自动机分为两类:确定的有限自动机(DFA)不确定的有限自动机(NFA)

@@ L={$a^n$$b^n$|n>=1}不能用正规式表示,也不能通过有限自动机识别[此字符串的特点是a的个数与b的个数相同,且所有的a都在b之前],因为该集合不是正规集,不能用正规式表示。而语言**L={$a^m$$b^n$|m≥0, n≥1}**的正规式表达式式a*bb* 因为在此集合中表示,若干个a之后跟若干个b,a可以不出现,b至少出现1次。

@@ 包含8个成员的开发小组的沟通路径最多有28条 [任意小组成员之间均可能有沟通路径,则可用完全连通图来对开发小组的沟通路径建模,最多的沟通路径为完全连通图的边数,即n个成员的开发小组的沟通路径是n(n-1)/2,因此8个成员的开发小组沟通路径最多有28条]

@@ 一个文法的语言是从文法能产生的句子的集合。一个文法产生的句子是从文法开始符号发出推导出的所有终结符号串

操作系统

@@ 从减少成本和缩短研发周期考虑,要求嵌入式操作系统能运行在不同的微处理器平台上,能针对硬件变化进行结构与功能上的配置。该要求体现了嵌入式操作系统的可定制性
嵌入式操作系统的特点:
微型化:从性能和成本角度考虑,希望占用的资源和系统代码量少
可定制:从减少成本和缩短研发周期考虑,要求嵌入式操作系统能运行在不同的微处理器平台上,能针对硬件变化进行结构与功能上的配置,以满足不同应用的需求
实时性:嵌入式操作性系统主要应用于过程控制、数据采集、传输通信、多媒体信息及关键要害领域需要迅速响应的场合,所以对实时性要求较高
可靠性:系统构建、模块和体系结构必须达到应有的可靠性,对关键要害应用还要提供容错和防故障措施
可移植性:为了提高系统的易移植性,通常采用硬件抽象层板级支撑包的底层设计技术

@@ 在Linux中,要更改一个文件的权限设置可使用chmod命令,修改文件夹名可使用modify命令
Linux中只有一个根目录,用”/“表示

@@ 计算机系统由硬件软件两部分组成,通常把未配置软件的计算机称为裸机。操作系统目的是为了填充人与机器之间的鸿沟,即建立用户与计算机之间的接口,而为裸机配置的一种系统软件。从用户角度看,当计算机配置了操作系统后,用户不再直接使用计算机系统硬件,而是使用操作系统所提供的命令和服务去操纵计算机,操作系统已称为现代计算机系统中必不可少的最重要的系统软件,因此操作系统用户计算机之间接口

@@ 实时操作系统主要用于实时要求的过程控制等领域,实时系统对于来自外部的事件必须在被操控对象规定的事件内做出及时响应并对其进行处理
实时操作系统分时操作系统的第一点区别就是:
① 交互性强弱不同,分时系统交互型强,实时系统交互型弱,但可靠性要求高
② 响应事件的敏感性强,对随机发生的外部事件必须在被控制对象规定的时间做出及时响应并对其进行处理
③ 系统的设计目的不同,分时系统是设计称一个多用方的通用系统,交互能力强;而实时系统大多数都是专用系统。

@@ 嵌入式系统初始化过程分为3个主要环节,按照自底向上从硬件到软件的次序依次为:片级初始化板级初始化系统级初始化。系统初始化主要任务是以软件初始化为主,主要进行操作系统的初始化
片级初始化:片级初始化完成嵌入式微处理器的初始化,包括设置嵌入式微处理器的核心寄存器和控制寄存器、嵌入式微处理器核心工作模式和嵌入式微处理器的局部总线模式等。片级初始化把嵌入式微处理器从上电时的默认状态逐步设置成系统所要求的工作状态。这是一个纯硬件的初始化过程。
板级初始化:板级初始化完成嵌入式微处理器以外的其他硬件设备的初始化。另外,还需设置某些软件的数据结构和参数,为随后的系统级初始化和应用程序的运行建立硬件和软件环境。这是一个同时包含软硬件两部分在内的初始化过程。
系统初始化:系统初始化过程以软件初始化为主,主要进行操作系统的初始化。BSP将对嵌入式微处理器的控制权转交给嵌入式操作系统,由操作系统完成余下的初始化操作,包含加载和初始化与硬件无关的设备驱动程序,建立系统内存区,加载并初始化其他系统软件模块,如网络系统、文件系统等。最后,操作系统创建应用程序环境,并将控制权交给应用程序的入口。

@@ 设计操作系统时不需要考虑的问题是语言编译器的设计实现
操作系统有两个重要作用:
① 通过资源管理提高计算机系统的效率,操作系统是计算机的资源管理者,它含有对系统软/硬件资源实施管理的一组程序
② 改善人机界面,向用户提供有好的工作环境

@@ 磁盘格式化是指把一张空白的盘划分称一个个小区域并编号,以供计算机存储和读取数据,格式化是一种纯物理操作,实在磁盘的所有数据区上写0的操作过程,同时对硬盘介质做一致性检测,并且标记出不可读和坏的扇区。由于大部分硬盘在出厂时已经格式化过,所以有在硬盘介质产生错误时才需要进行格式化。
磁盘分区是将磁盘划分成一块块的存储区域。在传统的磁盘管理中,将一个硬盘分为两大类分区:主分区和扩展分区。主分区是能够安装操作系统、能够进行计算机启动的分区,这样的分区可以直接格式化,然后安装系统,直接存放文件。
磁盘里的文件都是按存储时间先后来排列的,理论上文件之间都是紧凑排列而没有空隙的。但是,用户常常会对文件进行修改,而且新增加的内容并不是直接加到原文件的位置的,而是放在磁盘存储空间的最末尾,系统会在这两段之间加上联系标识。当有多个文件被修改后,磁盘里就会有很多不连续的文件。一旦文件被删除,所占用的不连 续空间就会空着,并不会被自动填满,而且,新保存的文件也不会放在这些地方,这些 空着的磁盘空间,就被称作”磁盘碎片”。因此,硬盘的每个分区里都会有碎片。碎片太多,其他的不连续文件相应也多,系统在执行文件操作时就会因反复寻找联系标识,工作效率大大降低,直接的反映就是感觉慢。磁盘清理将删除计算机上所有不需要的文件(这些文件由用户或系统进行确认)。
磁盘碎片整理就是通过系统软件或者专业的磁盘碎片整理软件对电脑磁盘在长期使用过程中产生的碎片和凌乱文件重新整理,释放出更多的磁盘空间,可提高电脑的整体性能和运行速度。

@@ 在WinXP操作系统中,用户利用”磁盘管理“程序可以对磁盘进行初始化、创建卷,可以选择使用FAT、FAT32或NTFS文件系统格式化券,通常将"C:\Windows\myprogram.exe"文件设置成只读隐藏属性,以便控制用户对该文件的访问。这一级安全管理称之为文件级安全管理。
文件级安全管理是通过系统管理员或文件组对文件设置来控制用户对文件的访问。用户对文件的访问,将由用户访问权、目录访问权、文件属性三者的权限所确定。

@@ 假设磁盘臂位于15号柱面上,进程的请求序列如下表表示,如果采用最短移臂调度算法,那么系统的响应序列应为⑤①②④③⑥

请求序列 柱面号 磁头号 扇区号
12 8 9
19 6 5
23 9 6
19 10 5
12 8 4
28 3 10

最短移臂调度算法,即有限响应距离比较近磁道的申请
1.当磁头位于15号柱面 (柱面号即磁道编号),请求序列分别位于12号柱面(①⑤)、19号柱面(②④)、23号柱面(③)、28号柱面(⑥)
2.距离15号柱面最近的应该是12号柱面(①⑤),优先响应(①⑤),次序不限;

@@ P(Pass)(通过)消耗一个信号量 V(释放)发送一个信号量
V(S1)唤醒P(S1):收银员进程操作只有等待着购书者拿出去前台后才能触发

@@ PV操作是操作系统提供的具有特定功能的原语,利用PV操作可以实现资源的互斥使用
软考必考题型之PV操作_pv操作中p和v各代表什么-CSDN博客

@@ 某计算机系统中互斥资源R的可用数为8,系统中有3个进程P1、P2和P3竞争R,且每个进程都需要i个R,该系统可能会发生死锁的最小i值为:4
本题对于R资源可用数为8,分配到3个进程中,为了让最后的i值最小,所以每个进程尽量平均分配,可以得到3、3、2的分配情况,此时如果假设i的取值为3,则必定不会发生死锁,当i>3时系统会产生死锁,此时取整,即最小i值为4

@@ 常用的进程控制块的组织方式有链接方式索引方式
采用链接方式是把具有同一状态的PCB,用其中的链接字链接成一个队列。这样可以形成就绪队列、若干个阻塞队列空白队列等。其中的就绪队列常按进程优先级的高低排列,把优先级高的进程的PCB排在队列前面。此外,也可根据阻塞原因的不同而把处于阻塞状态的进程的PCB排成等待I/O操作完成的队列和等待分配内存的队列等。
采用索引方式是系统根据所有进程的状态建立若干索引表。例如,就绪索引表、阻塞索引表,并把各索引表在内存的首地址记录在内存的一些专用单元中。在每个索引表的表目中,记录具有相应状态的某个PCB在PCB表中的地址。

@@ 某系统中有3个并发进程竞争资源R,每个进程都需要5个R,那么至少有13个R,才能保证系统不会发生死锁。[每个进程所需资源数-1,再相加,就是发生死锁的临界资源数]
3(5-1)+1<=y 所以y=13个

某系统有n个进程,每个进程需要x个同类资源,系统不会产生死锁的最少资源是多少?
★ 设:最少资源数位y个,满足如下表达式即可:★
n(x-1)+1<=y
每个进程需要x个资源,初始时給每个进程x-1个资源,最后再給一个格外的资源,每当一个进程运行完后马上释放其占有的资源,其它进程可以申请资源继续运行

@@ 某系统中仅有5个并发进程竞争某类资源,且都需要3个该类资源,那么至少有11个该类资源,才能保证系统不会发生死锁。5×(3-1)+1=11

@@ 某计算机系统页面大小为4k,进程的页面变换表如下。若进程的逻辑地址为2D16H。该地址经过变换后,其物理地址应为4D16H

页号 物理块号
0 1
1 3
2 4
3 6

根据题意,页面大小为4k,逻辑地址2D16H所在页号为2,页内地址为D16H,查看表后可知物理块号为4,该地址i过变换后,其物理地址应为物理块号4拼接上页内地址D16H,即十六进制4D16H

@@ 让我们来看一下这道13年上半年的软设选择题: 图(a)中B

图(b)中C
A. P1、P2、P3都是非阻塞节点,该图可以化简,是非死锁的
B. P1、P2、P3都是阻塞节点,该图不可以化简,是死锁的
C. P2是阻塞节点,P1、P3是非阻塞节点,该图可以化简,是非死锁的
D. P1、P2是非阻塞节点,P3是阻塞节点,该图不可以化简,是死锁的

★ 如何看进程资源图呢?
P:进程    R:一类资源     R中的圆圈数:该类资源有几个
★ 字母箭头都是啥意思?
R→P(R指向P):分配一份R类资源給进程P
P→R(P指向R):进程P申请一份R类资源
★ 判断一个进程节点是否堵塞?
读图时,先看资源分配R→P,再看资源申请P→R
【注意】
读图时,不要将同时存在R→P、P→R双向箭头的情况理解成:
P先申请一个资源,R再分配一个资源给P!

可能存在的情况:
① R中所有资源分配出去了(R→P),而此时还有进程P向R申请资源(P→R)
此时申请资源R的进程P:成为阻塞节点
② R中所有资源没有全部都分配出去了(R→P),而此时还有进程P向R申请资源(P→R)
此时申请资源R的进程P:成为非阻塞节点

★ 判断一个进程资源图是否是死锁的?
如果所有节点都是阻塞的———此进程图不可以化简,是死锁的
如果有节点不是阻塞的————此进程图可以花间,是非死锁的
★[将非阻塞节点周围的箭头删除,只保留阻塞节点的箭头,此时观察在图中原来的阻塞节点是否阻塞]★
★[将非阻塞节点周围的箭头删除,只保留阻塞节点的箭头,此时观察在图中原来的阻塞节点是否阻塞]★
★[将非阻塞节点周围的箭头删除,只保留阻塞节点的箭头,此时观察在图中原来的阻塞节点是否阻塞]★
图a
R1一共有2个资源,给P1、P2各分配一个,已经无可分配资源
此时P2还向R1申请1个资源,因为没有资源可以申请了,所以P2会阻塞;

同理,R2一共有3个资源,给P1分配1个、P2分配2个,已经无可分配资源
此时P1还向R2申请1个资源,因为没有资源可以申请了,所以P1也会阻塞;

因为P1、P2节点都阻塞了,所以此图无法化简,是死锁的
图b
R1一共有2个资源,给P1、P3各分配一个,已经无可分配资源
此时P2还向R1申请1个资源,因为没有资源可以申请了,所以P2会阻塞;

R2一共有3个资源,给P2、P3各分配一个,还剩1个可分配资源
此时P1向R2申请1个资源,因为还有可分配资源,所以P1不会阻塞;
(此时P3也向R2申请1个资源,同理P3也不会阻塞)
【这里要特别注意:P1、P3虽然同时都在申请剩下的1个资源,但是并不意味着这个资源申请已经被响应了,从而造成死锁的现象】
--------------------------------------------------------------------------
R1出去两个资源,R1资源数为0,此时P2申请R1资源,P2阻塞。R2总资源数3,出去2个资源。P1,P3申请R2资源,P1,P3非阻塞。[R2资源有3个,已分配2个,P3申请1个R2资源可以得到满足,故进程P3可以进行完毕释放其占有的资源。这样可以使得P1、P3都变成非阻塞节点,得到所需资源进行完毕,因此,该进程资源图是可以化简的]

@@ 在支持多线程的操作系统中,假设进程P创建了若干个线程,那么该进程中某线程的栈指针是不能被这些线程共享的。在同一进程中的各个线程都可以共享进程所拥有的资源,如访问进程地址空间中的每一个虚地址;访问进程所拥有的已打开文件、定时器、信号量机构等,但是不能共享进程中某线程的栈指针

@@ 假设系统采用PV操作实现进程同步与互斥。若n个进程共享两台打印机,那么信号量S的取值范围为 -(n-2) ~ 2。信号量初值等于资源数量,即为2,由于同时最多有2个进程访问打印机,其余进程必须处理等待状态,故S的最小值为 -(n-2)

·每个进程开始的时候执行P操作(信号量减一操作),结束的时候执行V操作(信号量加一操作)。P作挂起阻塞,V为激活唤醒。信号量的原则是从左到右,从上到下。
·假设系统采用PV操作实现进程同步与互斥,若n个进程共享两台打印机,那么信号量S的取值范围为-(n-2)~2。两台打印机,所以最多等待状态的进程数就是(n-2),
★★如果没有进程申请打印机,打印机的信号量就是2★★
信号量的值小于0,表示没有可用的资源,其绝对值表示阻塞队列中等待该资源的进程数。

·同步信号量,值为可用资源的个数。信号量的值小于0,则线程进行等待;信号量的值大于0,表示值为可用资源的个数。初始值为0。
·互斥信号量,只有两个值:0和1。0表示资源被占用,线程等待;1表示资源没有被占用,线程可以进入。初始值为1。

@@ 假设系统中有n个进程共享3台扫描仪,并采用PV操作实现进程同步与互斥。若系统信号量S的当前值为-1,**进程P1、P2又分别执行了一次P(S)操作,那么信号量S的值应为___。
[当有进程运行时,其他进程访问信号量,信号量就会减1。S=-1-2]
[当进程执行P(S)操作时,它试图将信号量S的值减去1。因此,如果
进程P1和P2都执行了一次P(S)操作**,并且信号量S的初始值为-1,那么它们会尝试将S的值减去1两次,结果是S的值变为-3。这是因为当信号量S的值为-1时,两个进程都尝试将其减去1,每次减去1后,S的值分别变为-2和-3。所以最终S的值为-3]

@@ 假设系统采用PV操作实现进程同步与互斥,若有n个进程共享一台扫描仪,那么当信号量S的值为-3时,表示系统中有3个进程等待使用扫描仪
[信号量不用减1是因为本题并没有进程运行]

@@ 假设某分时系统采用简单时间片轮转法,当系统中的用户数为n、时间片为q时,系统对每个用户的相应时间T=(n*q) [在分时系统中是将把CPU的时间分成很短的时间片轮流地分配給各个终端用户]

@@ 如果系统采用信箱通信方式,当进程调用Send原语被设置为”等信箱”状态时,其原因是指定的信箱中存满了信件;因为Send原语是发送原语,如果系统采用信箱通信方式,那么当进程调用Send原语被设置成”等信箱“状态时,意味着指定的信箱存满了信件,无可用空间

@@ 绝对路径从根目录\开始,本题book2.doc的绝对路径为 \MyDrivers\user2\
相对路径从当前目录下一级开始,本题book2.doc的相对路径为 user2\

@@ 直接索引即索引直接指向物理块,可用表示逻辑块号范围:0-4号
一级索引即索引节点指向的物理块用来存放地址项,可用表示256个地址项,即256个物理块
二级索引即索引节点指向的物理块,存放的是一级索引的地址块地址。一共有256个地址块用来存放以及索引

@@ 某文件系统采用多级索引结构。若磁盘块的大小为1k字节,每个块号占3字节,那么采用二级索引时的文件最大长度为116281k字节 。
磁盘块的大小为1KB,每个块号需占3B,因此一个磁盘物理块可存放1024/3=341个块号(取整)
采用一级索引时的文件最大长度为:341×1024/1024=341KB
采用二级索引时的文件最大长度为:341×341×1024/1024=116281KB

@@ 若系统正在将系统目录文件修改的结果写回磁盘时系统发生崩溃,则对系统的影响相对较大。
影响文件系统可靠性因素之一是文件系统的一致性问题。很多文件系统是先读取磁盘块到主存,在主存进行修改,修改完毕再写回磁盘。 一致性检查包括 块的一致性检查文件的一致性检查

@@ 双击”xxx.jpg”文件名时,系统会自动通过建立的文件关联来决定使用什么程序打开该图像文件

文件管理

@@ 若系统正在将目录文件修改的结果写回磁盘时系统发生崩溃,则对系统的影响相对较大。很多文件系统是先读取磁盘块到主存,在主存进行修改,修改完毕再写回磁盘。

@@ 若某计算机系统的I/O接口与主存采用统一编址,则输入输出操作是通过访存指令来完成的。
常用的I/O接口编制方法有两种:
一是内存单元统一编址[将I/O接口中有关的寄存器或存储部件看作存储器单元,与主存中的存储单元统一编址。内存地址和接口地址统一在一个公共的地址空间里,对I/O接口的访问就如同对主存单元的访问一样,可以用访问内存单元的指令访问I/O接口]
二是单独编址[通过设置单独的I/O地址空间,为接口中的有关寄存器或存储部件分配地址码,需要设置专门的I/O指令进行访问。这种编址方式的优点是不占用主存的地址空间,访问主存的指令和访问接口的指令不同。]

@@ I/O设备管理软件一般分为4个层次。具体层次从上往下分别是**用户级I/O层、设备无关I/O层、设备驱动程序、中断处理程序、硬件** [用户五官射中硬件]

用户级I/O层:发出I/O调用
设备无关I/O层:设备名解析、阻塞进程、分配缓冲区
设备驱动程序:设置寄存器、检查设备状态
中断处理程序:I/O完成后唤醒设备驱动程序
硬件:完成具体的I/O操作

@@ 计算机运行过程中,cpu需要与外设进行数据交换。采用**中断方式和DMA方式控制技术时,CPU与外设可并行工作。程序查询中断方式都需要CPU来执行程序指令进行数据的输入和输出,DMA方式则不同,这是一种不经过CPU而直接从内存存取数据**的数据交换模式。[DMA模式下,CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传达,数据传达完毕再把信息反馈给CPU即可]

@@ 某磁盘有100个磁道,磁头从一个磁道移至另一个磁道需要6ms。文件在磁盘上非连续存放,逻辑上相邻数据块的平均距离为10个磁道,每块的旋转延迟时间及传输时间分别为100ms和20ms,则读取一个100块的文件需要18000ms。[访问一个数据块的时间应为寻道时间加旋转延迟时间及传输时间之和。根据题意,每块的旋转延迟时间及传输时间共需120ms,磁头从一个磁道移至另一个磁道需要6ms,但逻辑上相邻数据块的平均距离为10个磁道,即读完一个数据块到下一个数据块寻道时间需要60ms。通过上述分析,本题访问一个数据块的时间T=120ms+60ms=180ms,而读取一个100块的文件共需要18000ms。]

@@ 假设某磁盘的每个磁道划分成9个物理块,每块存放1个逻辑记录。逻辑记录R0,RI,…,R8存放在同一个磁道上,记录的安排顺序如下表所示:如果磁盘的旋转速度为27ms/周,磁头当前处在RO的开始处。若系统顺序处理这些记录,使用单缓冲区,每个记录处理时间为3ms,则处理这9个记录的最长时间为246ms:若对信息存储进行优化分布后,处理9个记录的最少时间为54ms

物理块 1 2 3 4 5 6 7 8 9
逻辑记录 R0 R1 R2 R3 R4 R5 R6 R7 R8

[因为系统使用的是单缓冲区,且顺序处理9个记录,每个记录处理时间为3ms,加上读写时间,总的时间就超过3ms了。而磁盘旋转一圈的时间为27ms,也就是说,当系统读取第0个记录后,正在处理的过程中,磁盘已经旋过了第1个记录。那么,要读取第1个记录,就需要磁盘再次旋转到第1个记录(即磁盘旋转1圈后,27+3=30ms)。同理,要读取第2个记录时,也需要等30ms。这样,要读取后面8个记录,需要8x30=240ms,同时加上处理第0个记录的时间(3ms)和处理第8个记录的时间(3ms),共需246ms。] [对于第二种情况,若对信息进行分布优化的结果如下所示:从上表可以看出,当读出记录RI并处理结束后,磁头刚好转至R2记录的开始处,立即就可以读出并处理,因此处理9个记录的总时间为:9X (3ms (读记录)+3ms (处理记录))=9X6ms=54ms]

@@ 在移臂调度算法中,先来先服务和最短寻找时间优先算法可能会随时改变移动臂的运动方向

@@ 概要设计文档的内容包括系统构架、模块划分、系统接口、数据设计四个方面

@@ 三层C/S体系结构由逻辑上互相分离的**表示层、业务层、数据层构成。其中表示层向客户提供数据,业务层实施业务相关数据规划,数据层**定义数据访问表中。

@@ 对软件过程的考虑:在整体上按照瀑布模型的流程实施项目开发,以方便对项目的管理;但在软件的实际创建中,则将软件系统按照功能分为许多增量构件,并以构件为单位逐个地创建与交付,直到全部增量构件创建完毕,并都被集成到系统之中交付用户使用。

模型 详情信息
增量模型 又称渐增模型,也称为有计划的产品改进模型,从一组给定的需求开始,通过构造一系列可执行中间版本来实施开发活动。第一个版本纳入一部分需求,下一个版本纳入更多的需求,直到系统完成。每个中间版本都需要执行必需的过程、活动和任务。增量模型是 [瀑布模型+原型] 进化模型的综合
[增量模型具有非常显著的优越性,但增量模型对软件设计有更高的技术要求,特别是对软件体系结构,要求它具有很好的开放性与稳定性,能够顺利地实现构件的集成;不必等到整个系统开发完成就可以使用可以使用较早的增量构件作为原型优先级最高的服务先交付, 这样最重要的服务接收最多的测试难点如何进行模块的划分。]
融合了瀑布模型的基本成分和原型实现迭代的特征,该模型随着日程时间的进展而交错的线性序列。
增量式开发的主要优点包括
1.由于能够在较短的时间内向用户提交一些有用的工作产品,因此能够解决用户的一些急用功能。
2.由于每次只提交用户部分功能,用户有较充分的时间学习和适应新的产品。
3.对系统的可维护性是一个极大的提高,因为整个系统是由一个个构件集成在一起的,当需求变更时只变更部分部件,而不必影响整个系统。
主要缺点包括
1.由于各个构件是逐渐并入已有的软件体系结构中的,所以加入构件必须不破坏已构造好的系统部分,这需要软件具备开放式的体系结构。
2.在开发过程中,需求的变化是不可避免的。增量模型的灵活性可以使其适应这种变化的能力大大优于瀑布模型和快速原型模型,但也很容易退化为边做边改模型,从而使软件过程的控制失去整体性。
3.增量包之间存在相交的情况且未很好处理
瀑布模型 将开发阶段描述为从一个阶段瀑布般地转换到另一个阶段的过程。将软件生存周期各个活动规定为线性顺序链接的若干阶段的模型[计划、分析、设计、编程、测试、维护]。规定了由前至后,相互衔接的固定次序,如同瀑布流水,逐级下落。是一种理想的开发模式,但缺乏灵活性,无法解决软件需求不明确或不准确的问题;不适用于开发初期对软件需求缺乏准确全面认识的情况优点:可强迫开发人员采用规范的方法;严格规定了各阶段必须提交的文档;要求每个阶段结束后,都要进行严格的审核。
原型模式 开发人员快速地构造整个系统或者系统的一部分以理解或澄清问题。增量模型是把软件产品作为一系列的增量构件来设计、编码、集成和测试,每个构件由多个相互作用的模块组成,并且能够完成特定的功能;并非所有的需求在系统开发之前都能准确地说明和定义。采用了动态定义需求的方法。适用于需求不明确的开发环境;可以有效地捕获系统需求
喷泉模型 喷泉模型是典型的面向对象生命周期模型,在开发过程中以用户需求为动力,以对象为驱动,适合于面向对象的开发方法;喷泉模型克服了瀑布模型不支持软件重用和多项开发活动集成的局限性;模型中的开发活动常常需要重复多次,在迭代过程中不断地完善软件系统;”喷泉”一词本身体现了迭代和无间隙特性。迭代意味着模型中的开发活动常常需要重复多次,在迭代过程中不断地完善软件系统;无间隙是指在开发活动之间不存在明显的边界。
螺旋模型 将开发活动和风险管理结合起来,以减小风险。将瀑布模型快速原型模型结合起来,还增加了风险分析;螺旋模型包含了四个方面的活动:制订计划、风险分析、实施工程、客户评估,螺旋模型适用于复杂的大型软件

@@ 若用户需求不清晰且经常发生变化,但系统规模不太大且不太复杂,则最适宜采用原型化开发方法
对于数据处理领域不太复杂的软件,若系统规模不太大且不太复杂,需求变化也不大,则最适宜采用结构化开发方法

@@ 软件工程的基本要素包括方法、工具、过程

@@ 在概要设计阶段选择适当的解决方案,将系统分解为若干个子系统,建立整个系统的体系结构。
软件设计的任务是基于需求分析的结果建立各种设计模型,给出问题的解决方案。
从工程管理的角度,可以将软件设计分为两个阶段:概要设计阶段详细设计阶段
概要设计阶段进行软件体系结构的设计、数据设计和接口设计;
详细设计阶段进行数据结构和算法的设计。面向对象设计方法中,概要设计阶段进行体系结构设计、初步的类设计/数据设计、结构设计:详细设计阶段进行构件设计。结构化设计和面向对象设计是两种不同的设计方法,结构化设计根据系统的数据流图进行设计,模块体现为函数、过程及子程序:面向对象设计基于面向对象的基本概念进行,模块体现为类、对象和构件等。

@@ “软件产品必须能够在3秒内对用户请求作出响应”属于软件需求中的非功能需求
软件需求是软件系统必须完成的事以及必须具备的品质。软件需求包括功能需求、非功能需求和设计约束三个方面的内容。功能需求是所开发的软件必须具备什么样的功能:非功能需求是指产品必须具备的属性或品质,如可靠性、性能、响应时间和扩展性等等;设计约束通常对解决方案的一些约束说明。”软件产品必须能够在3秒内对用户请求作出响应”主要表述软件的响应时间,属于非功能需求

@@ 软件复杂性度量是软件度量的一个重要分支。参数有很多主要包括:
规模。即指令数或者源程序行数;②难度。通常由程序中出现的操作数所决定的量来表示;
结构。通常用与程序结构有关的度量来表示;④智能度。即算法的难易程度

@@ 基于构件的软件开发,主要强调在构建软件系统时复用已有的软件”构件”,在检索到可以使用的构件后,需要针对新系统的需求对构件进行合格性检验、适应性修改,然后集成到新系统中

@@ 程序的三种基本控制结构式顺序、选择和重复

@@ 软件工程每一个阶段结束前,应该着重对可维护性进行复审。在系统设计阶段的复审期间,应该从容易修改、模块化和功能独立的目的出发,评价软件的结构和过程。可维护性式所有软件都具有的基本特点,必须在开发阶段保证软件具有可维护性的特点。

@@ I/O软件隐藏了I/O操作实现的细节。I/O软件向用户提供的式逻辑接口。I/O软件将硬件与较高层次的软件隔离开来,而最高层软件向硬件提供一个友好的、清晰的、统一的接口,方便用户使用。

阅读全文

SpringMVC[Mapping,中文乱码,Restful及跨域,JSON序列化,非简单请求,拦截器]

2024/1/15

Spring MVC

内容 说明 重要程度
Spring MVC入门 Spring MVC开发流程与环境配置 ★★★★★★
接收Web数据 Spring MVC参数接收与数据绑定 ★★★★★★
URL Mapping 讲解URL绑定过程 ★★★★★★
中文乱码问题 解决请求与相应中文乱码 ★★★★★★
拦截器 Spring MVC拦截器的使用 ★★★

Restful开发风格 [主流]

内容 说明 重要程度
Restful风格介绍 介绍Restful开发规范 ★★★★★★
Restful开发实战 实例讲解Restful在Spring MVC中的实现 ★★★★★★
JSON序列化 通过相应输出数据 ★★★★★★
Restful的跨域问题 分析跨域问题的来源与解决办法 ★★★★★★

Spring MVC[Model+View+Controller(中介)]

比servlet方便许多 简化web程序的开发

  • Spring MVC是Spring体系的轻量级Web MVC框架
  • Spring MVC的核心Controller控制器,用于处理请求,产生相应
  • Spring MVC基于Spring IOC容器运行,所有对象被IoC管理
学习向导
  • Spring MVC入门
  • Spring MVC数据绑定
  • Restful 开发风格
  • Spring MVC拦截器

Spring MVC环境配置

IDEA环境下创建Maven WebApp

Project Structure 点加号 添加Web 右侧Path是Web描述符所存储的路径
C:\Users\Pluminary\Desktop\SpringMVC\src\main\webapp\WEB-INF\web.xml
Deployment descriptor version => 3.1
下面的 Web Resource Directory【用于目录存储页面】
C:\Users\Pluminary\Desktop\SpringMVC\src\main\webapp
再次点下方的Create Artifact
右上角Type:
Web Application: Exploded 目录方式运行
Web Application: Archive 打包成war包运行
配置Tomcat Server => Deployment 添加当前工程 SpringMVC:Web exploded
下面的Application context: 设置为 / 只访问localhost即可
Server选项框里的 On ‘Update’ action: 把Restart server 改成 Update classes and resources【热部署】
当页面源代码发生变化时 不需要重启

  • Maven依赖Spring-WebMVC
  • web.xml配置DispatcherServlet
  • 配置applicationContext的mvc标记
  • 开发Controller控制器
pom.xml
<?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.imooc</groupId>
    <artifactId>first-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.28</version>
        </dependency>
    </dependencies>
</project>
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
<!--    DispatchServlet 对所有请求进行拦截  -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
<!--        DispatcherServlet是Spring MVC是最核心的对象
            DispatcherServlet用于拦截http请求
            并根据请求的URL调用与之对应的Controller方法,来完成Http请求的处理
-->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--        在Web应用启动时自动创建Spring IoC容器,并初始化DispatcherServlet -->
<!--        applicationContext.xml 初始化参数 启动时候加载-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
<!--        "/" 所有请求都要拦截-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mv="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- context:component-scan标签作用 在Spring IoC初始化过程中, 自动创建并管理com.imooc.springmvc
   及子包中拥有以下注解的对象:
       @Repository 通常存放在Dao类上 通常都是与数据发生直接交互的类
       @Service     ...Service类上 业务逻辑类
       @Controller 描述SpringMVC的控制器类
       @Component  无法确定的类型种类
   -->
    <context:component-scan base-package="com.imooc.springmvc"></context:component-scan>
<!--    启用Spring MVC的注解开发模式-->
    <mvc:annotation-driven/>
<!--    将图片/JS/CSS等静态资源排除在外, 可提高执行效率-->
    <mvc:default-servlet-handler/>
</beans>
com/imooc/springmvc/controller/TestController.java
package com.imooc.springmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {
//  将当前的方法绑定某个get方式请求的url  //localhost/t
    @GetMapping("/t")
//  直接向响应输出字符串数据,不跳转页面
    @ResponseBody
    public String test(){
        return "SUCCESS";
    }
}

添加依赖jar包进入项目工程 配置tomcat服务Run/Debug Configurations 中的 Deployment 点一下小铅笔修改 将右侧的Avaliable Elements中的依赖包全部put进入
404bug 配置都正常无误 寻找一下项目工程里的out/artifacts/WEB-INF/web.xml

Spring MVC数据绑定

URL Mapping(URL映射)
  • URL Mapping指将URL与Controller方法绑定
  • 通过将URL与方法绑定,SpringMVC便可通过Tomcat对外暴露服务
URL Mapping注解
  • @RequestMapping - 通用绑定 //在全局用则是通用请求映射访问前缀 若在方法上则不区分get/post请求
  • @GetMapping - 绑定Get请求
  • @PostMapping - 绑定Post请求

@GetMapping("/g") ====== @RequestMapping(value="/g",method=RequestMethod.GET)

pom.xml
<?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.imooc</groupId>
    <artifactId>first-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.28</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
    </dependencies>
</project>
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <!--    DispatchServlet 对所有请求进行拦截  -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <!--        DispatcherServlet是Spring MVC是最核心的对象
                    DispatcherServlet用于拦截http请求
                    并根据请求的URL调用与之对应的Controller方法,来完成Http请求的处理
        -->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--        在Web应用启动时自动创建Spring IoC容器,并初始化DispatcherServlet -->
        <!--        applicationContext.xml 初始化参数 启动时候加载-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <!--        "/" 所有请求都要拦截-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mv="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- context:component-scan标签作用 在Spring IoC初始化过程中, 自动创建并管理com.imooc.springmvc
       及子包中拥有以下注解的对象:
           @Repository 通常存放在Dao类上 通常都是与数据发生直接交互的类
           @Service     ...Service类上 业务逻辑类
           @Controller 描述SpringMVC的控制器类
           @Component  无法确定的类型种类
       -->
    <context:component-scan base-package="com.imooc.springmvc"></context:component-scan>
    <!--    启用Spring MVC的注解开发模式-->
    <mvc:annotation-driven/>
    <!--    将图片/JS/CSS等静态资源排除在外, 可提高执行效率-->
    <mvc:default-servlet-handler/>
</beans>
com/imooc/springmvc/controller/TestController.java
package com.imooc.springmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {
    @GetMapping("/t") //localhost/t
    @ResponseBody //直接向响应输出字符串数据,不跳转页面
    public String test(){
        return "Hello Spring MVC";
    }
}
com/imooc/springmvc/controller/URLMappingController.java
package com.imooc.springmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping ("/um") //类上直接使用默认访问前缀 全局通用请求映射
public class URLMappingController {
    @GetMapping("/g")
    @ResponseBody
    public String getMapping(){
        return "This is get method";
    }
//  直接访问会出错 post请求如何访问 用进行html表单提交
    @PostMapping("/p")
    @ResponseBody
    public String postMapping(){
        return "This is post method";
    }
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/um/p" method="post">
        <input type="submit" value="提交">
    </form>
</body>
</html>

Controller方法参数接收请求参数

接收请求参数的常用做法
  • 使用Controller方法参数接收
<form action="/m1" method="post">
    <input name="username"/>
    <input name="password"/>
</form>
----------------------------------------------------
@PostMapping("/m1")
@ResponseBody
public String post(String username, Long password){
    return username + ":" + password;
}

特殊的注解@RequestParam("") 接收特殊自定义的参数

com/imooc/springmvc/controller/URLMappingController.java
package com.imooc.springmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/um") //类上直接使用默认访问前缀 全局通用请求映射
public class URLMappingController {
    @GetMapping("/g")
    @ResponseBody
    public String getMapping(@RequestParam("manager_name") String managerName) {
        System.out.println("managerName:"+managerName);
        return "This is get method";
    }

    //    直接访问会出错 post请求如何访问 用进行html表单提交
    @PostMapping("/p")
    @ResponseBody
    public String postMapping(String username, String password) {
        System.out.println(username + ":" + password);
        return "This is post method";
    }
}

=========================
网页中输入 http://localhost/um/g?manager_name=lily
控制台返回 managerName:lily
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/um/p" method="post">
        <input name="username"><br/>
        <input name="password"><br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>
  • 使用Java Bean接收数据
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/um/p1" method="post">
        <input name="username"><br/>
        <input name="password"><br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>

​ 用实体对象User进行接收多个对象的创建和注入以及类型转换 [一次性完成]

com/imooc/springmvc/controller/URLMappingController.java
@PostMapping("/p1")
    @ResponseBody //只要拥有User属性和参数 就可以一起赋值
    public String postMapping1(User user, String username){
        System.out.println(user.getUsername() + ":" + user.getPassword());
        return "This is post method";
    }
User.java
//标准的java bean
public class User {
    private String username;
    private Long password;
} Setter + Getter

踩坑记录:IDEA web项目out artifacts文件夹只包含WEB-INF_out artifacts无法生成test.html-CSDN博客

综合训练:学员调查问卷

知识点
  • 利用数组或者List接收请求中的复合数据
  • 利用@RequestParam为参数设置默认值
  • 使用Map对象接收请求参数及注意事项
URI绝对路径与相对路径

相对地址的应用案例
src/main/webapp/form.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>学员调查问卷</title>
    <style>
        .container {
            position: absolute;
            border: 1px solid #cccccc;
            left: 50%;
            top: 50%;
            width: 400px;
            height: 300px;
            margin-left: -200px;
            margin-top: -150px;
            box-sizing: border-box;
            padding: 10px;
        }
        h2{
            margin: 10px 0px;
            text-align: center;
        }
        h3{
            margin: 10px  0px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>学员调查问卷</h2>
        <form action="./apply" method="post">
        <h3>您的姓名</h3>
        <input name="name" class="text"  style="width: 150px">
        <h3>您正在学习的技术方向</h3>
        <select name="course" style="width: 150px">
            <option value="java">Java</option>
            <option value="h5">HTML5</option>
            <option value="python">Python</option>
            <option value="php">PHP</option>
        </select>
        <div>
<!--复选框用数组 或 ArrayList接收-->
            <h3>您的学习目的:</h3>
            <input type="checkbox" name="purpose" value="1">就业找工作
            <input type="checkbox" name="purpose" value="2">工作要求
            <input type="checkbox" name="purpose" value="3">兴趣爱好
            <input type="checkbox" name="purpose" value="4">其他
        </div>
        <div style="text-align: center;padding-top:10px" >
            <input type="submit" value="提交" style="width:100px">
        </div>
        </form>

    </div>
</body>
</html>

如果要接收复合数据 直接用数组接收是最简单的 但使用起来并不轻松
可以使用List来接收 但是前方记得要有@RequestParam
更加高级的可以封装成一个对象 用List接收复合数据
Map只能接收单个数据 接收复合数据的时候会造成数据丢失

com/imooc/springmvc/controller/FormController.java
package com.imooc.springmvc.controller;

import com.imooc.springmvc.entity.Form;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;

@Controller
public class FormController {
//    @PostMapping("/apply")  ★★★第一次尝试★★★
    @ResponseBody
//    如果在请求中不包含默认值 => 取别名  默认值机制:ANON匿名
    public String apply(@RequestParam(value = "n", defaultValue = "ANON") String name, String course, Integer[] purpose){
        System.out.println(name);
        System.out.println(course);
        for (Integer p:purpose){
            System.out.println(p);
        }
        return "SUCCESS";
    }

//    @PostMapping("/apply")  ★★★第二次尝试★★★
    // 请求中的复合数据要被转换为List进行存储
    @ResponseBody
    public String apply(String name, String course, @RequestParam List<Integer> purpose){
        System.out.println(name);
        System.out.println(course);
        for (Integer p:purpose){
            System.out.println(p);
        }
        return "SUCCESS";
    }

    @PostMapping("/apply")  
    //★★★★★ 推荐使用 ★★★★★
    // 实体类+List 极大简化表单工作量  
    // 请求中的复合数据要被转换为List进行存储
    @ResponseBody
    public String apply(Form form){
        return "SUCCESS";
    }
    //不推荐直接使用Map导入数据 因为复合数据[数组数据]会丢失
}
com/imooc/springmvc/entity/Form.java
public class Form {
    private String name;
    private String course;
    private List<Integer> purpose;
}Setter + Getter

关联对象赋值

复杂内容表单
用户名: <input name="username">
密码:<input name="password">
--------------------------------
姓名:<input name="name">
身份证号:<input name="idno">
过期时间:<input name="expire">
面向对象设计 【关联对象赋值】
public class User{
    private String username;
    private String password;
    private IDcard idcard = new IDCard();
    //Getter + Setter
}
★★ 关联上IDcard ★★
public class IDcard{
    private String name;
    private String idno;
    private Date expire;
    //Getter + Setter
}
用户名: <input name="username">
密码:<input name="password">
--------------------------------
姓名:<input name="idcard.name">
身份证号:<input name="idcard.idno">
过期时间:<input name="idcard.expire">
src/main/webapp/form.html
 <div>
            <h3>您的学习目的:</h3>
            <input type="checkbox" name="purpose" value="1">就业找工作
            <input type="checkbox" name="purpose" value="2">工作要求
            <input type="checkbox" name="purpose" value="3">兴趣爱好
            <input type="checkbox" name="purpose" value="4">其他
        </div>
            <h3>收货人</h3>
<!--  private Delivery delivery = new Delivery() -->
            <input name="delivery.name" class="text" style="width: 150px">
            <h3>联系电话</h3>
            <input name="delivery.mobile" class="text" style="width: 150px">
            <h3>收货地址</h3>
            <input name="delivery.address" class="text" style="width: 150px">

        <div style="text-align: center;padding-top:10px" >
            <input type="submit" value="提交" style="width:100px">
        </div>
com/imooc/springmvc/entity/Form.java
public class Form {
    private String name;
    private String course;
    private List<Integer> purpose;
    private Delivery delivery = new Delivery();
} Getter + Setter
com/imooc/springmvc/entity/Delivery.java
public class Delivery {
    private String name;
    private String address;
    private String mobile;
} Getter + Setter
com/imooc/springmvc/controller/FormController.java
package com.imooc.springmvc.controller;

import com.imooc.springmvc.entity.Form;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;

@Controller
public class FormController {
//    @PostMapping("/apply")  第一次尝试
    @ResponseBody
//    如果在请求中不包含默认值 => 取别名  默认值机制:ANON匿名
    public String apply(@RequestParam(value = "n", defaultValue = "ANON") String name, String course, Integer[] purpose){
        System.out.println(name);
        System.out.println(course);
        for (Integer p:purpose){
            System.out.println(p);
        }
        return "SUCCESS";
    }

//    @PostMapping("/apply")  第二次尝试
    // 请求中的复合数据要被转换为List进行存储
    @ResponseBody
    public String apply(String name, String course, @RequestParam List<Integer> purpose){
        System.out.println(name);
        System.out.println(course);
        for (Integer p:purpose){
            System.out.println(p);
        }
        return "SUCCESS";
    }

//    @PostMapping("/apply") 第三次尝试[这个可以 但有更好]
    // 实体类+List 极大简化表单工作量
    // 请求中的复合数据要被转换为List进行存储
    @ResponseBody
    public String apply(Form form){
        return "SUCCESS";
    }

    @PostMapping("/apply")
    @ResponseBody
    public String applyDelivery(Form form){
        System.out.println(form.getDelivery().getName());
        return "SUCCESS";
    }
}

日期类型转换

①注解方法

com/imooc/springmvc/controller/URLMappingController.java
@Controller
@RequestMapping("/um") //类上直接使用默认访问前缀 全局通用请求映射
public class URLMappingController {
@PostMapping("/p1")
    @ResponseBody
    public String postMapping1(User user, String username, @DateTimeFormat(pattern = "yyyy-MM-dd") Date createTime){
        System.out.println(user.getUsername() + ":" + user.getPassword());
        return "This is post method";
    }
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/um/p1" method="post">
        <input name="username"><br/>
        <input name="password"><br/>
        <input name="createTime"><br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>


②实体对象接收数据

com/imooc/springmvc/entity/User.java
//标准的java bean
public class User {
    private String username;
    private Long password;
// SpringMVC会自动按照这个类型进行转换
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date createTime;
} Getter + Setter

③自定义转换器:全局的默认时间转换器

com/imooc/springmvc/converter/MyDateConverter.java
package com.imooc.springmvc.converter;

import org.springframework.core.convert.converter.Converter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MyDateConverter implements Converter<String, Date> {
    public Date convert(String s) { //转换工作
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        try {
            Date d = sdf.parse(s);
            return d;
        } catch (ParseException e) {
            return null;
        }
    }
}
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mv="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- context:component-scan标签作用 在Spring IoC初始化过程中, 自动创建并管理com.imooc.springmvc
       及子包中拥有以下注解的对象:
           @Repository 通常存放在Dao类上 通常都是与数据发生直接交互的类
           @Service     ...Service类上 业务逻辑类
           @Controller 描述SpringMVC的控制器类
           @Component  无法确定的类型种类
       -->
    <context:component-scan base-package="com.imooc.springmvc"></context:component-scan>
    <!--    启用Spring MVC的注解开发模式 让底下的转换类生效-->
    <mvc:annotation-driven conversion-service="conversionService"/>
    <!--    将图片/JS/CSS等静态资源排除在外, 可提高执行效率-->
    <mvc:default-servlet-handler/>
    <!--    通知SpringMVC有哪些转换类-->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <!--    这是转换类定义的地方-->
        <property name="converters">
            <set>
                <bean class="com.imooc.springmvc.converter.MyDateConverter"/>
            </set>
        </property>
    </bean>
</beans>

解决中文乱码问题

Web应用的中文乱码由来
  • Tomcat默认使用字符集ISO-8859-1,属于西欧字符集
  • 解决乱码的核心思路是将ISO-8859-1转换为UTF-8
  • Controller中请求与响应都需要设置UTF-8字符集
中文乱码的配置
  • Get请求乱码 - server.xml增加URIEncoding属性

去Tomcat-conf文件中寻找server.xml进行增加

 <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               URIEncoding="UTF-8"
               maxParameterCount="1000"
               />

===============================================
http://localhost/um/g?manager_name=丽丽
控制台:managerName:丽丽
  • Post请求乱码 - web.xml配置CharacterEncodingFilter
http://localhost/
输入:
张三  123456 2001-03-02
随后点击提交
控制台:??????:123456
src/main/webapp/WEB-INF/web.xml
<filter>
        <filter-name>characterFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
http://localhost/
输入:
张三  123456 2001-03-02
随后点击提交
控制台:张三:123456
  • Response相应乱码 - Spring配置StringHttpMessageConverter

解决响应中的中文乱码

com/imooc/springmvc/controller/URLMappingController.java
package com.imooc.springmvc.controller;

import com.imooc.springmvc.entity.User;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

@Controller
@RequestMapping("/um") //类上直接使用默认访问前缀 全局通用请求映射
public class URLMappingController {
    @GetMapping("/g")
    @ResponseBody
    public String getMapping(@RequestParam("manager_name") String managerName) {
        System.out.println("managerName:"+managerName);
        return "This is get method";
    }

    //    直接访问会出错 post请求如何访问 用进行html表单提交
    @PostMapping("/p")
    @ResponseBody
    public String postMapping(String username, Long password) {
        System.out.println(username + ":" + password);
        return "This is post method";
    }

//    @PostMapping("/p1")
    @ResponseBody
    public String postMapping1(User user){
        System.out.println(user.getUsername() + ":" + user.getPassword());
        return "This is post method";
    }

    @PostMapping("/p1")
    @ResponseBody
    public String postMapping1(User user, String username, @DateTimeFormat(pattern = "yyyy-MM-dd") Date createTime){
        System.out.println(user.getUsername() + ":" + user.getPassword());
        return "这是Post响应";
    }
}
User.java
//标准的java bean
public class User {
    private String username;
    private Long password;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date createTime;
}Getter + Setter
com/imooc/springmvc/converter/MyDateConverter.java
package com.imooc.springmvc.converter;

import org.springframework.core.convert.converter.Converter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MyDateConverter implements Converter<String, Date> {
    public Date convert(String s) { //转换工作
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        try {
            Date d = sdf.parse(s);
            return d;
        } catch (ParseException e) {
            return null;
        }
    }
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/um/p1" method="post">
        <input name="username"><br/>
        <input name="password"><br/>
        <input name="createTime"><br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mv="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- context:component-scan标签作用 在Spring IoC初始化过程中, 自动创建并管理com.imooc.springmvc
       及子包中拥有以下注解的对象:
           @Repository 通常存放在Dao类上 通常都是与数据发生直接交互的类
           @Service    ...Service类上 业务逻辑类
           @Controller 描述SpringMVC的控制器类
           @Component  无法确定的类型种类
       -->
    <context:component-scan base-package="com.imooc.springmvc"></context:component-scan>
    <!--    启用Spring MVC的注解开发模式 让底下的转换类生效-->
    <mvc:annotation-driven conversion-service="conversionService">
<!--        设置消息转换器-->

        <mvc:message-converters>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
<!--                转换什么呢?-->
                <property name="supportedMediaTypes">
                    <list>
<!--  在servlet中是直接  response.setContentType("text/html;charset=utf-8")  -->
                        <value>text/html;charset=utf-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>

    </mvc:annotation-driven>
    <!--    将图片/JS/CSS等静态资源排除在外, 可提高执行效率-->
    <mvc:default-servlet-handler/>
    <!--    通知SpringMVC有哪些转换类-->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <!--    这是转换类定义的地方-->
        <property name="converters">
            <set>
                <bean class="com.imooc.springmvc.converter.MyDateConverter"/>
            </set>
        </property>
    </bean>
</beans>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <!--    DispatchServlet 对所有请求进行拦截  -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <!--        DispatcherServlet是Spring MVC是最核心的对象
                    DispatcherServlet用于拦截http请求
                    并根据请求的URL调用与之对应的Controller方法,来完成Http请求的处理
        -->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--        在Web应用启动时自动创建Spring IoC容器,并初始化DispatcherServlet -->
        <!--        applicationContext.xml 初始化参数 启动时候加载-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <!--        "/" 所有请求都要拦截-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <filter>
        <filter-name>characterFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

响应中产生结果 [ModelAndView => MVC高效解耦理念]

  • @ResponseBody - 产生响应文本

    • @ResponseBody直接产生响应体数据, 过程不涉及任何视图
    • @ResponseBody可产生标准字符串/JSON/XML等格式数据
    • @ResponseBody被StringHttpMessageConverter所影响
  • ModelAndView - 利用模板引擎渲染输出

    • ModelAndView对象是指”模型(数据)与视图(界面)”对象
    • 通过ModelAndView可将包含数据对象与模板引擎进行绑定
    • SpringMVC中默认的View是JSP, 也可以配置其他模板引擎

提问:为什么不直接访问view.jsp 而是绕了一个圈子还是同样的效果
回答:因为jsp页面是写死的如何将页面和数据绑定在一起呢?先通过访问/um/view这个controller让其方法中产生数据, 之后再将这个数据通过modelandview对象绑定到页面中才可以做到

从请求传来一个用户编号把数据查询出来得到一个user用户对象 然后再view.jsp中把刚刚查询的对象进行显示[动态的]

高效解耦
后端:com/imooc/springmvc/controller/URLMappingController.java
// http://localhost/um/view?userId=1 数据动态查询产生
    @GetMapping("/view")
    public ModelAndView showView(Integer userId){
        ModelAndView mav = new ModelAndView("/view.jsp");
        User user = new User();
        if (userId == 1){
            user.setUsername("lily");
        }else if (userId == 2){
            user.setUsername("smith");
        }
    //  在当前请求中增加一个对象 数据绑定view.jsp
        mav.addObject("u", user);
        return mav;
    }
前端:src/main/webapp/view.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>I'm view page</h1>
<hr>
<h2>Username:${u.username}</h2>
</body>
</html>

ModelAndView [重定向]

[SpringMVC若跳转页面需要使用此对象进行数据绑定]
  • mav.addObject()方法设置的属性默认存放在当前请求中
  • 默认ModelAndView使用请求转发(forward)至页面
  • 重定向使用 new ModelAndView(“redirect:/index.jsp”) 新请求

页面重定向使用时机:内部的controller处理逻辑和跳转页面之间没有直接关系时可以用”redirect:/view.jsp”

ModelAndView mav = new ModelAndView("redirect:/view.jsp");
直接访问到了jsp  http://localhost/view.jsp

访问:http://localhost/um/view?userId=1
建立一个 没有斜杠的相对路径 src/main/webapp/um/view.jsp
com/imooc/springmvc/controller/URLMappingController.java
 // http://localhost/um/view?userId=1 数据动态查询产生
    @GetMapping("/view")
    public ModelAndView showView(Integer userId){
//        ModelAndView mav = new ModelAndView("redirect:/view.jsp");
        ModelAndView mav = new ModelAndView();
        mav.setViewName("view.jsp");
//        没有斜杠是代表相对路径 相对于@RequestMapping("/um")地址
//        mav.setViewName("view.jsp");

        User user = new User();
        if (userId == 1){
            user.setUsername("lily");
        }else if (userId == 2){
            user.setUsername("smith");
        }
//      在当前请求中增加一个对象 数据绑定view.jsp
        mav.addObject("u", user);
        return mav;
    }
String与ModelMap实现ModelAndView类似功能 [工作中的小技巧]
com/imooc/springmvc/controller/URLMappingController.java
//   String 与 ModelMap[为模型数据] 返回字符串
//    Controller方法返回String的情况
//    1.方法被@ResponseBody描述, 则SpringMVC直接响应Spring字符串本身
//    2.方法不存在@ResponseBody, 则SpringMVC处理String指代的视图(页面)
// 这里的String是直接代替了@ResponseBody - 产生响应文本
    public String showView1(Integer userId, ModelMap modelMap){
        String view = "/um/view.jsp";
        User user = new User();
        if (userId == 1){
            user.setUsername("lily");
        }else if (userId == 2){
            user.setUsername("smith");
        }
        modelMap.addAttribute("u", user);
        return view;
    }

SpringMVC整合Freemarker [项目:first-springmvc]

① pom.xml引入依赖
要随时导入包 tomcat → Edit configuration → Deployment → Artifacts 将右侧包put in
<dependency>
  <groupId>org.freemarker</groupId>
  <artifactId>freemarker</artifactId>
  <version>2.3.28</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-support</artifactId>
  <version>5.1.9.RELEASE</version>
</dependency>
② 启用Freemarker模板引擎
applicationContext.xml
<bean id="ViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
   <!-- 设置响应输出,并解决中文乱码 --> 
<!-- 渲染完成后向客户端浏览器响应式 响应体中使用的字符集编码 -->
   <property name="contentType" value="text/html;charset=utf-8"></property>
   <!-- 指定Freemarker模板文件扩展名 -->
   <property name="suffix" value=".ftl"/>
</bean>
③ 本身配置Freemarker参数
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<!-- 设置模板保存的目录 tomcat无法直接解析freemarker模板引擎 所以新增/WEB-INF/ftl -->
   <property name="templateLoaderPath" value="/WEB-INF/ftl"/>
   <!-- 其他模板引擎设置 -->
   <property name="freemarkerSettings">
     <props>
         <!-- 设置Freemarker脚本与数据渲染时使用的字符集 -->
         <!-- 模板与数据绑定渲染的过程中使用的字符集编码 -->
         <prop key="defaultEncoding">UTF-8</prop>
     </props> 
   </property>
</bean>
src/main/webapp/WEB-INF/ftl/test.ftl
<h1>${u.username}</h1>
com/imooc/springmvc/controller/FreemarkerController.java
package com.imooc.springmvc.controller;

import com.imooc.springmvc.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
// http://localhost/fm/test  显示 andy
@Controller
@RequestMapping("/fm")
public class FreemarkerController {
    @GetMapping("/test")
    public ModelAndView showTest(){
//        因为之前配置所有配置扩展名是.ftl 所以下面不用写
        ModelAndView mav = new ModelAndView("/test");
        User user = new User();
        user.setUsername("andy");
        mav.addObject("u",user);
        return mav;
    }
}
=====================================================
// http://localhost/fm/test  显示 andy

RESTful开发风格

RESTful 风格(详细介绍 + 案例实现)_c# restful风格接口-CSDN博客

REST与RESTful

  • REST- 表现层状态转换, 资源在网络中以某种表现形式进行状态转移
  • RESTful是基于REST理念的一套开发风格, 是具体的开发规则

RESTful开发规范

URL中所有的都是名词 请求都有不同的含义 返回的数据是JSON或者XML格式

  • 使用URL作为用户交互入口
  • 明确的语义规范(GET, POST, PUT, DELETE)
  • 只返回数据(JSON, XML)不包含任何展现
注解 作用
@RestController 由 @Controller + @ResponseBody组成(返回 JSON 数据格式)
@PathVariable URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到控制器处理方法的形参中
@RequestMapping 注解用于请求地址的解析,是最常用的一种注解
@GetMapping 查询请求
@PostMapping 添加请求
@PutMapping 更新请求
@DeleteMapping 删除请求
@RequestParam 将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解)

RESTful命名要求

URI 说明 修改建议
GET /articles?au=lily 正确用法
GET /a/1 URI必须具有语义 GET /student/1
POST /createArticle/1 URI必须使用名词 POST /article/1
GET /articles/author/1 URI扁平化, 不超两级 GET /articles/author?id=1
DELETE/articles/1 URI名词区分单复数 GET /articles?au=lily
DELETE /article/1

开发第一个RESTful应用

404究极报错
项目生成文件out/artifacts/XXX/WEB-INF中没有导入lib包
没有将下面的web文件里的web.xml手动复制进去

pom.xml 
写完pom.xml后记得在tomcat配置中的Deployment中加入依赖包
<?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.imooc</groupId>
    <artifactId>restful</artifactId>
    <version>1.0-SNAPSHOT</version>

    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
</project>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mv="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <context:component-scan base-package="com.imooc.restful"/>
    <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <!-- response.setContentType("text/html;charset=utf-8") -->
                        <value>text/html;charset=utf-8</value>
                        <value>application/json;charset=utf-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
    <mvc:default-servlet-handler/>
</beans>
web/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>characterFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
com/imooc/restful/controller/RestfulController.java
package com.imooc.restful.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/restful")
public class RestfulController {
    @GetMapping("/request")
    @ResponseBody
    public String doGetRequest(){
//  双引号中如果包括双引号 要用转义字符 \"
        return "{\"message\":\"返回查询结果\"}";
    }
}

=======================================================
http://localhost/restful/request
{"message":"返回查询结果"}

实现RESTful实验室

com/imooc/restful/controller/RestfulController.java
package com.imooc.restful.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/restful")
public class RestfulController {
    @GetMapping("/request")
    @ResponseBody
    public String doGetRequest(){
//  双引号中如果包括双引号 要用转义字符 \"
        return "{\"message\":\"返回查询结果\"}";
    }
    @PostMapping("/request")
    @ResponseBody
    public String doPostRequest(){
        return "{\"message\":\"数据新建成功\"}";
    }
    @PutMapping ("/request")
    @ResponseBody
    public String doPutRequest(){
        return "{\"message\":\"数据更新成功\"}";
    }
    @DeleteMapping("/request")
    @ResponseBody
    public String doDeleteRequest(){
        return "{\"message\":\"数据删除成功\"}";
    }
}
===========================
http://localhost/client.html
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mv="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <context:component-scan base-package="com.imooc.restful"/>
    <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <!-- response.setContentType("text/html;charset=utf-8") -->
                        <value>text/html;charset=utf-8</value>
                        <!--只要响应产生就会使用utf-8字符集-->
                        <value>application/json;charset=utf-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
    <mvc:default-servlet-handler/>
</beans>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>characterFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
pom.xml
<?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.imooc</groupId>
    <artifactId>restful</artifactId>
    <version>1.0-SNAPSHOT</version>

    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.9</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.9</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.9</version>
        </dependency>
    </dependencies>
</project>
web/client.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="jquery-3.3.1.min.js"></script>
    <script>
        $(function () {
            $("#btnGet").click(function () {
                $.ajax({
                    url: "/restful/request",
                    type: "get",
                    dataType: "json",
                    success: function (json) {
                        $("#message").text(json.message);
                    }
                })
            });
        })
        $(function () {
            $("#btnPost").click(function () {
                $.ajax({
                    url: "/restful/request",
                    type: "post",
                    dataType: "json",
                    success: function (json) {
                        $("#message").text(json.message);
                    }
                })
            });
        })
        $(function () {
            $("#btnPut").click(function () {
                $.ajax({
                    url: "/restful/request",
                    type: "put",
                    dataType: "json",
                    success: function (json) {
                        $("#message").text(json.message);
                    }
                })
            });
        })
        $(function () {
            $("#btnDelete").click(function () {
                $.ajax({
                    url: "/restful/request",
                    type: "delete",
                    dataType: "json",
                    success: function (json) {
                        $("#message").text(json.message);
                    }
                })
            });
        })
    </script>
</head>
<body>
    <input type="button" id="btnGet" value="发送Get请求">
    <input type="button" id="btnPost" value="发送Post请求">
    <input type="button" id="btnPut" value="发送Put请求">
    <input type="button" id="btnDelete" value="发送Delete请求">
    <h1 id="message"></h1>
</body>
</html>

RestController注解与路径变量

在post请求中通过client.html利用Ajax动态注入了一个rid到RestfulController.java
代替了所有的@ResponseBody 默认向请求台进行输出

@Controller: 用于标识一个类是Spring MVC中的控制器,类似于标准的@Controller 注解。它告诉Spring框架该类是一个控制器,可以处理HTTP请求。

@ResponseBody: 用于将方法的返回值直接作为HTTP响应的主体(Body)内容。这意味着不会进行视图解析,而是直接将返回的对象(通常是JSON或XML)写入HTTP响应

因此,@RestController 的作用是将一个控制器类标记为RESTful风格的控制器,其中的每个方法都被视为返回数据,而不是视图。这样就不需要在每个方法上都添加 @ResponseBody 注解,因为该注解已经包含在 @RestController 中。
client.html
 $(function(){
            $("#btnPost").click(function () {
                $.ajax({
                    url : "/restful/request/100",
                    type : "post" ,
                    dataType : "json" ,
                    success : function(json){
                   $("#message").text(json.message+":"+json.id);
                    }
                })
            });
        })
com/imooc/restful/controller/RestfulController.java
    
package com.imooc.restful.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/restful")
//@CrossOrigin(origins = {"http://localhost:8080","http://www.imooc.com"})
//@CrossOrigin(origins = "*",maxAge = 3600)
public class RestfulController {
    @GetMapping("/request")
    //@ResponseBody
    public String doGetRequest() {
        return "{\"message\":\"返回查询结果\"}";
    }

    // POST /article/1
    // POST /restful/request/100
    @PostMapping("/request/{rid}")
    //@ResponseBody
    public String doPostRequest(@PathVariable("rid") Integer requestId) {
        return "{\"message\":\"数据新建成功\",\"id\":" + requestId + "}";
    }

    @PutMapping("/request")
    //@ResponseBody
    public String doPutRequest() {
        return "{\"message\":\"数据更新成功\"}";
    }

    @DeleteMapping("/request")
    //@ResponseBody
    public String doDeleteRequest() {
        return "{\"message\":\"数据删除成功\"}";
    }
}

简单请求与非简单请求

  • 简单请求是指标准结构的HTTP请求, 对应GET/POST请求
  • 非简单请求是复杂要求的HTTP请求, 指PUT/DELETE、扩展标准请求
  • 两者最大区别是非简单请求发送前需要发送预检请求 [看看能不能进行处理 可以才实际处理 预先处理不符合的数据挡在外面]
web.xml
<!--    对put和delete请求进行支持-->
    <filter>
        <filter-name>formContentFilter</filter-name>
        <filter-class>org.springframework.web.filter.FormContentFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>formContentFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
web/client.html
$(function(){
            $("#btnPost").click(function () {
                $.ajax({
                    url : "/restful/request/100",
                    type : "post" ,
                    data : "name=lily&age=23",
                    dataType : "json" ,
                    success : function(json){
                        $("#message").text(json.message+":"+json.id);
                    }
                })
            });
        })

        $(function(){
            $("#btnPut").click(function () {
                $.ajax({
                    url : "/restful/request",
                    type : "put" ,
                    data : "name=lily&age=23",
                    dataType : "json" ,
                    success : function(json){
                        $("#message").text(json.message);
                    }
                })
            });
        })
com/imooc/restful/controller/RestfulController.java
 // POST /article/1
    // POST /restful/request/100
    @PostMapping("/request/{rid}")
    //@ResponseBody
    public String doPostRequest(@PathVariable("rid") Integer requestId, Person person){
        System.out.println(person.getName() + ":" + person.getAge());
        return "{\"message\":\"数据新建成功\",\"id\":" + requestId + "}";
    }

    @PutMapping("/request")
    //@ResponseBody
    public String doPutRequest(Person person){
        System.out.println(person.getName() + ":" + person.getAge());
        return "{\"message\":\"数据更新成功\"}";
    }
====================
lily:23

创建一个Person用来插入数据 并打印在控制台

com/imooc/restful/entity/Person.java
public class Person {
    private String name;
    private Integer age;
}Getter + Setter

JSON序列化

pom.xml
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.9</version>
        </dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.9</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.9</version>
        </dependency>
一定要使用2.9以后的版本 之前的版本有严重的安全风险
http://localhost/restful/person?id=1
@GetMapping("/person")
//不返回String 返回JSON序列化的对象
    public Person findByPersonId(Integer id){
        Person p = new Person();
        if (id==1){
            p.setName("lily");
            p.setAge(23);
        } else if (id==2) {
            p.setName("smith");
            p.setAge(22);
        }
        return p;
    }
============================================
{
    "name": "lily",
    "age": 23
}



http://localhost/restful/persons
@GetMapping("/persons")
    public List<Person> findPersons(){
        List list = new ArrayList();
        Person p1 = new Person();
        p1.setName("lily");
        p1.setAge(23);
        list.add(p1);

        Person p2 = new Person();
        p2.setName("smith");
        p2.setAge(22);
        list.add(p2);
        
        return list;
    }
=============================================
[
    {
        "name": "lily",
        "age": 23
    },
    {
        "name": "smith",
        "age": 22
    }
]

服务器返回json数组

client.html
$(function(){
            $("#btnPersons").click(function () {
                $.ajax({
                    url : "/restful/persons",
                    type : "get" ,
                    dataType : "json" ,
                    success : function(json){
                        console.info(json);
                        for (var i=0; i<json.length; i++){
                            var p = json[i];
                            $("#divPersons").append("<h2>" + p.name + "-" + p.age + "</h2>")
                        }
                    }
                })
            });
        })

    </script>
</head>
<body>
    <input type="button" id="btnGet" value="发送Get请求">
    <input type="button" id="btnPost" value="发送Post请求">
    <input type="button" id="btnPut" value="发送Put请求">
    <input type="button" id="btnDelete" value="发送Delete请求">
    <h1 id="message"></h1>
    <hr/>
    <!-- 点击btnPersons按钮时发送Ajax请求[上面有操作代码] 将所有人员信息追加到div中-->
    <input type="button" id="btnPersons" value="查询所有人员">
    <div id="divPersons"></div>
</body>
com/imooc/restful/controller/RestfulController.java
@GetMapping("/persons")
    public List<Person> findPersons(){
        List list = new ArrayList();
        Person p1 = new Person();
        p1.setName("lily");
        p1.setAge(23);
        list.add(p1);

        Person p2 = new Person();
        p2.setName("smith");
        p2.setAge(22);
        list.add(p2);

        return list;
    }

===============================================
http://localhost/client.html
点击查询所有人员
=> 
lily-23
smith-22

增加时间!

Person.java
public class Person {
    private String name;
    private Integer age;
// 记得用日期的格式化输出
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date birthday;
}Getter + Setter
client.html
$(function(){
            $("#btnPersons").click(function () {
                $.ajax({
                    url : "/restful/persons",
                    type : "get" ,
                    dataType : "json" ,
                    success : function(json){
                        console.info(json);
                        for (var i=0; i<json.length; i++){
                            var p = json[i];
                            $("#divPersons").append("<h2>" + p.name + "-" + p.age + "-" + p.birthday+ "</h2>")
                        }
                    }
                })
            });
        })

===================================
lily-23-2024-01-17 09:09:28
smith-22-2024-01-17 09:09:28

浏览器的同源策略

  • 同源策略阻止从一个域加载的脚本去获取另一个域上的资源
  • 只要协议域名端口有任何一个不同,都被当做是不同的域
  • 浏览器Console看到Access-Control-Allow-Origin就代表了跨域了

HTML中允许跨域的标签

  • < img > - 显示远程图片
  • < script > - 加载远程JS
  • < link > - 加载远程CSS

CORS跨域资源访问

  • CORS是一种机制, 使用额外的HTTP头通知浏览器可以访问其他域
  • URL响应头包含 Access-Control-* 指明请求允许跨域

Spring MVC解决跨域访问

  • @CrossOrigin - Controller跨域注解
  • < mvc:cors > - Spring MVC全局跨域配置
RestfulController.java [代码第四行]
@RestController
@RequestMapping("/restful")
//@CrossOrigin(origins = {"http://localhost:8080","http://www.imooc.com"})
//@CrossOrigin(origins = "*",maxAge = 3600) 所有端口都会访问发送请求
//maxAge = 3600 一小时时间后发送预检请求  之内的就发送实际请求 【非简单请求】
public class RestfulController {
    @GetMapping("/request")
    //@ResponseBody
    public String doGetRequest(){
        return "{\"message\":\"返回查询结果\"}";
    }

    // POST /article/1
    // POST /restful/request/100
    @PostMapping("/request/{rid}")
    //@ResponseBody
    public String doPostRequest(@PathVariable("rid") Integer requestId, Person person){
        System.out.println(person.getName() + ":" + person.getAge());
        return "{\"message\":\"数据新建成功\",\"id\":" + requestId + "}";
    }

    @PutMapping("/request")
    //@ResponseBody
    public String doPutRequest(Person person){
        System.out.println(person.getName() + ":" + person.getAge());
        return "{\"message\":\"数据更新成功\"}";
    }

    @DeleteMapping("/request")
    //@ResponseBody
    public String doDeleteRequest(){
        return "{\"message\":\"数据删除成功\"}";
    }

    @GetMapping("/person")
    public Person findByPersonId(Integer id){
        Person p = new Person();
        if (id==1){
            p.setName("lily");
            p.setAge(23);
        } else if (id==2) {
            p.setName("smith");
            p.setAge(22);
        }
        return p;
    }

    @GetMapping("/persons")
    public List<Person> findPersons(){
        List list = new ArrayList();
        Person p1 = new Person();
        p1.setName("lily");
        p1.setAge(23);
        p1.setBirthday(new Date());
        list.add(p1);

        Person p2 = new Person();
        p2.setName("smith");
        p2.setAge(22);
        p2.setBirthday(new Date());
        list.add(p2);
        return list;
    }
}

CORS全局配置

<mvc:cors>
    <mvc:mapping path="/restful/**"
        allowed-origins="http://localhost:8080,http://www.imooc.com"
        max-age="3600"/>
</mvc:cors>

SpringMVC拦截器 [高级组件]

拦截器-Interceptor
  • 拦截器(Interceptor)用于对URL请求进行前置/后置过滤
  • Interceptor与Filter用途相似, 但实现方式不同
  • Interceptor底层基于Spring AOP面向切面编程实现[类似于环绕通知]
拦截器开发流程
  • Maven依赖servlet-api
  • 实现HandlerInterceptor接口
  • applicationContext配置过滤地址
HandlerInterceptor接口
  • preHandle - 前置执行处理
  • postHandle - 目标资源已被Spring MVC框架处理 没产生响应文本
  • afterCompletion - 相应文本已经产生
pom.xml
     <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
<!-- 只有在开发编译才会引用 打包最终使用的时候会排除在外 因为 servlet-api与tomcat-api冲突  -->
            <scope>provided</scope>
        </dependency>
    </dependencies>
com/imooc/restful/interceptor/MyInterceptor.java
package com.imooc.restful.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

public class MyInterceptor implements HandlerInterceptor {
//    Code -> Implement Methods

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(request.getRequestURI()+"准备执行");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(request.getRequestURI()+"目标处理成功");
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(request.getRequestURI()+"响应内容已产生");
    }
//  随后去applicationContext.xml配置
}

=============================================================
/准备执行
/目标处理成功
/响应内容已产生
/准备执行
/目标处理成功
/响应内容已产生
/准备执行
/目标处理成功
/响应内容已产生
    
http://localhost/restful/persons
/restful/persons准备执行
/restful/persons目标处理成功
/restful/persons响应内容已产生
    
http://localhost/client.html
/client.html准备执行
/client.html目标处理成功
/client.html响应内容已产生
applicationContext.xml
<mvc:interceptors>
        <mvc:interceptor>
<!--  哪些[所有]地址进行拦截 下面加个bean是哪个类进行处理[送到这个类中处理]-->
            <mvc:mapping path="/**"/>
            <bean class="com.imooc.restful.interceptor.MyInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

拦截器使用细则

applicationContext.xml
<mvc:interceptors>
        <mvc:interceptor>
<!--            哪些[所有]地址进行拦截 下面加个bean是哪个类进行处理[送到这个java中处理]-->
       <!-- <mvc:mapping path="/**"/> -->
            <mvc:mapping path="/restful/**"/>
            <mvc:mapping path="/webapi/**"/>
<!--            以下是不需要拦截 排除在外的-->
            <mvc:exclude-mapping path="/**.ico"/>
            <mvc:exclude-mapping path="/**.jpg"/>
            <mvc:exclude-mapping path="/**.gif"/>
            <mvc:exclude-mapping path="/**.js"/>
            <mvc:exclude-mapping path="/**.css"/>
<!--   创建一个resources  规范静态文件目录 一次性都排除在外-->
            <mvc:exclude-mapping path="/rescources/**"/>

            <bean class="com.imooc.restful.interceptor.MyInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(request.getRequestURI()+"准备执行");
        return true;
    }
通过preHandle的返回值可以做很多事情
对某一个url进行前置检查 对请求放行 如果不符合要求 直接在拦截器中返回相应
return true; 请求依次向后发送
return false; 请求被阻挡

开发”用户流量”拦截器

pom.xml [引入新的依赖后一定要去Tomcat导入新的依赖包]
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%thread] %d %level %logger{10} - %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="accessHistoryLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>d:/logs/history.%d.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>[%thread] %d %level %logger{10} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="debug">
        <appender-ref ref="console"/>
    </root>
    <logger name="com.imooc.restful.interceptor.AccessHistoryInterceptor"
            level="INFO" additivity="false">
        <appender-ref ref="accessHistoryLog"/>
    </logger>
</configuration>
com/imooc/restful/interceptor/AccessHistoryInterceptor.java
package com.imooc.restful.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;

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

public class AccessHistoryInterceptor implements HandlerInterceptor {
    private Logger logger = LoggerFactory.getLogger(AccessHistoryInterceptor.class);
//    前置处理

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        StringBuilder log = new StringBuilder();
        log.append(request.getRemoteAddr());
        log.append("|");
        log.append(request.getRequestURL());
        log.append("|");
        log.append(request.getHeader("user-agent"));
        logger.info(log.toString());
        return true;
    }
}
com/imooc/restful/interceptor/MyInterceptor.java
package com.imooc.restful.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

public class MyInterceptor implements HandlerInterceptor {
//    Code -> Implement Methods

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(request.getRequestURL() + "-准备执行");
//        response.getWriter().print("[]");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(request.getRequestURL() + "-目标处理成功");
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(request.getRequestURL() + "-响应内容已产生");
    }
}

==============================================================
http://localhost/client.html
[http-nio-80-exec-1] 2024-01-18 10:43:54,739 DEBUG o.s.w.s.DispatcherServlet - GET "/login.html", parameters={}
[http-nio-80-exec-1] 2024-01-18 10:43:54,750 DEBUG o.s.w.s.h.SimpleUrlHandlerMapping - Mapped to org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler@5c0e8bb9
[http-nio-80-exec-1] 2024-01-18 10:43:54,758 DEBUG o.s.w.s.DispatcherServlet - Completed 404 NOT_FOUND
[http-nio-80-exec-2] 2024-01-18 10:44:00,495 DEBUG o.s.w.s.DispatcherServlet - GET "/restful/request", parameters={}
[http-nio-80-exec-2] 2024-01-18 10:44:00,502 DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.lang.String com.imooc.restful.controller.RestfulController.doGetRequest()
http://localhost/restful/request-准备执行
[http-nio-80-exec-2] 2024-01-18 10:44:00,539 DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json;charset=utf-8', given [application/json, text/javascript, */*;q=0.01] and supported [text/html;charset=utf-8, application/json;charset=utf-8, text/plain, */*, application/json, application/*+json]
[http-nio-80-exec-2] 2024-01-18 10:44:00,539 DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing ["{"message":"返回查询结果"}"]
http://localhost/restful/request-目标处理成功
http://localhost/restful/request-响应内容已产生
[http-nio-80-exec-2] 2024-01-18 10:44:00,580 DEBUG o.s.w.s.DispatcherServlet - Completed 200 OK
[http-nio-80-exec-5] 2024-01-18 10:44:08,194 DEBUG o.s.w.s.DispatcherServlet - GET "/restful/persons", parameters={}
[http-nio-80-exec-5] 2024-01-18 10:44:08,195 DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.util.List<com.imooc.restful.entity.Person> com.imooc.restful.controller.RestfulController.findPersons()
http://localhost/restful/persons-准备执行
RestfulController.findPersons() - return list
[http-nio-80-exec-5] 2024-01-18 10:44:08,211 DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [application/json, text/javascript, */*;q=0.01] and supported [application/json, application/*+json]
[http-nio-80-exec-5] 2024-01-18 10:44:08,211 DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [[com.imooc.restful.entity.Person@619c4d80, com.imooc.restful.entity.Person@71459fb]]
http://localhost/restful/persons-目标处理成功
http://localhost/restful/persons-响应内容已产生
===================================================================
D:\logs\history.2024-01-18
[http-nio-80-exec-1] 2024-01-18 10:43:54,755 INFO c.i.r.i.AccessHistoryInterceptor - 0:0:0:0:0:0:0:1|http://localhost/login.html|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0
[http-nio-80-exec-2] 2024-01-18 10:44:00,502 INFO c.i.r.i.AccessHistoryInterceptor - 0:0:0:0:0:0:0:1|http://localhost/restful/request|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0
[http-nio-80-exec-5] 2024-01-18 10:44:08,195 INFO c.i.r.i.AccessHistoryInterceptor - 0:0:0:0:0:0:0:1|http://localhost/restful/persons|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0
[http-nio-80-exec-3] 2024-01-18 10:44:10,137 INFO c.i.r.i.AccessHistoryInterceptor - 0:0:0:0:0:0:0:1|http://localhost/restful/request/100|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0
[http-nio-80-exec-4] 2024-01-18 10:44:10,935 INFO c.i.r.i.AccessHistoryInterceptor - 0:0:0:0:0:0:0:1|http://localhost/restful/request|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0
[http-nio-80-exec-6] 2024-01-18 10:44:11,481 INFO c.i.r.i.AccessHistoryInterceptor - 0:0:0:0:0:0:0:1|http://localhost/restful/request|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0
applicationContext.xml
<mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <mvc:exclude-mapping path="/resources/**"/>
            <bean class="com.imooc.restful.interceptor.AccessHistoryInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

Spring MVC处理流程

阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.