SpringBoot知识点总结-DX的笔记

什么是SpringBoot?

  • Spring Boot 是由 Pivotal 团队提供的全新框架
  • 目的是用来简化新 Spring 应用的初始搭建以及开发过程
  • 该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置
  • 通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
  • Spring Boot 并不是对 Spring 功能上的增强,而是提供了一种快速使用 Spring 的方式。

特性

  • 约定大于配置
  • 创建独立的spring应用程序。
  • 嵌入的tomcat jetty 或者undertow 不用部署WAR文件。
  • 允许通过Maven来根据需要获取starter
  • 尽可能的使用自动配置spring
  • 提供生产就绪功能,如指标,健康检查和外部配置properties yaml yml
  • 开箱即用,没有代码生成,也无需 XML 配置,同时也可以修改默认值来满足特定的需求。

传统开发模式

  • 优点

    • 开发简单,集中式管理
    • 基本不会重复开发
    • 功能都在本地,没有分布式的管理和调用消耗
  • 缺点

    • 效率低:开发都在同一个项目改代码,相互等待,冲突不断
    • 维护难:代码功功能耦合在一起,新人不知道何从下手
    • 不灵活:构建时间长,任何小修改都要重构整个项目,耗时
    • 稳定性差:一个微小的问题,都可能导致整个应用挂掉
    • 扩展性不够:无法满足高并发下的业务需求
    • 对服务器的性能要求要统一,要高

微服务开发模式

  • 优点
    • 每个微服务都很小,这样能聚焦一个指定的业务功能或业务需求
    • 微服务能够被小团队开发,这个小团队2-5人就可以完成了
    • 微服务是松耦合的,是有功能,有意义的服务,开发阶段或部署阶段都是独立的
    • 微服务可以使用不同的语言开发
    • 微服务能部署在中低端配置的服务器上
    • 很容易和第三方集成
    • 每个服务都有自己的存储能力,单独的库,也可以有统一的库
  • 缺点:
    • 微服务会带来过多的操作
    • 可能有双倍的努力
    • 分布式系统可能复杂难管理
    • 分布跟踪部署难
    • 当服务数量增加时,管理复杂度增加

SpringBoot的配置

配置文件

  • properties文件

    #第一种配置文件
    #mysql
    jdbc.mysql.driverClassName=com.mysql.cj.jdbc.Driver
    jdbc.mysql.url=jdbc:mysql://localhost:3306/crm?serverTimezone=UTC
    jdbc.mysql.username=root
    jdbc.mysql.password=root
    
    #druid
    druid.initialSize=10
    druid.minIdle=10
    druid.maxActive=50
    druid.maxWait=60000
    

    yml文件

    #第二种配置文件
    #myKey: myValue
    
    #yaml 层级式键值对配置
    #层级:通过换行符+空格,同一层级必须对其,父层与子层之间通常使用2个空格
    #level1:
    #  level21: aaa
    #  level2:
    #    level31: bb
    #    level32: cc
    
    #数据类型
    #简单值
    #字符串:可以不使用引号包裹,但是有特殊符号时需要使用引号包裹(例如:空格)
    name: 张三
    age: 10
    birthday: 1998/07/25
    gender: true
    obj: ~
    
    #数组(也认为是LIst集合)
    #行内
    array1: [spring, springmvc, mybatis]
    array2:
      - html
      - css
      - javascript
    
    #对象(也可认为是Map集合)
    student:
      name: 李四
      age: 22
      birth: 1999/10/17
      gender: false
      hobby: [football, games, swimming]
      address:
        city: 郑州市
        area: 金水区
        street: 民航路
    
    #主配置
    #激活环境配置profiles
    #spring:
    #  profiles:
    #    active: dev
    #server:
    #  port: 8080
    

读取配置文件

  • 本质是将配置类对象放到ioc容器中

  • 单个读取

    @Value("${name}")
    private String username;
    
  • 使用配置类读取

    • 第一种方式

      @Component + @ConfigurationProperties
      
    • 第二种方式

      @EnableConfigurationProperties + @ConfigurationProperties
      
  • 读取第三方的properties文件

    @Component
    @ConfigurationProperties(prefix = "jdbc.mysql")
    //加载第三方资源文件
    @PropertySource(value = "classpath:db.properties")
    
  • 读取原生的xml文件

  • demo

    /**
     * 读取配置文件数据
     */
    @RestController
    public class ReadConfigController {
    
        /**
         * 单个读取,不方便
         * 使用@Value注解+SpringEL来实现配置读取 
         */
        @Value("${name}")
        private String username;
        @Value("${age}")
        private Integer age;
        @Value("${birthday}")
        private Date birthday;
        @Value("${gender}")
        private Boolean gender;
        @Value("${student.name}")
        private String studentName;
        @GetMapping("/read")
        public String read(){
            return "姓名:" + username + " 年龄:" + age + " 生日:" + birthday + " 性别:" + gender + " 学生姓名:" + studentName;
        }
    
        /**
         * 使用配置类
         * 将配置类放到IOC容器,在这里自动注入
         */
        @Autowired
        private StudentProperties studentProperties;
        @GetMapping("/readProperties")
        public StudentProperties readProperties(){
            return studentProperties;
        }
    
        /**
         * 读取第三方的properties文件
         */
        @Autowired
        private MysqlProperties mysqlProperties;
        @GetMapping("/readProp")
        public MysqlProperties readProp(){
            return mysqlProperties;
        }
    
        /**
         * 读取原生的xml配置文件
         */
        @Autowired
        private Emp emp;
        @GetMapping("/readXml")
        public Emp readXml(){
            return emp;
        }
    }
    

    在配置类上加注解

    //加载原生的xml spring配置文件
    @ImportResource(locations = "classpath:applicationContext.xml")
    //开启配置文件读取,属性为要读取的配置类,可以批量读取配置类
    @EnableConfigurationProperties({StudentProperties.class, AddressProperties.class})
    @SpringBootApplication
    public class Springboot02ConfigApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(Springboot02ConfigApplication.class, args);
        }
    }
    

    读取yaml文件的配置类

    /**
     * 学生配置读取类,自动读取yml配置文件
     *
     * 成员变量名称必须与配置名称保持一致(驼峰与中划线)
     * 1.@Component + @ConfigurationProperties
     * 2.@EnableConfigurationProperties + @ConfigurationProperties
     *
     * 第二种方式的@EnableConfigurationProperties注解
     * 需要放到配置类上
     * 作用是:开启配置文件读取
     */
    @Data
    //@Component //配置bean
    @ConfigurationProperties(prefix = "student")  //读取配置文件,属性用来指定层级
    public class StudentProperties {
    
        private String name;
        private Integer age;
        private Date birth;
        private Boolean gender;
        private String[] hobby;
        private AddressProperties address;
    
        //get...  set...
    }
    
    // @Component
    @Data
    @ConfigurationProperties(prefix = "student.address")
    public class AddressProperties {
        private String city;
        private String area;
        private String street;
    }
    

    读取properties文件的配置类

    /**
     * 读取#第三方的priperties文件
     */
    @Data
    @Component
    @ConfigurationProperties(prefix = "jdbc.mysql")
    //加载第三方资源文件
    @PropertySource(value = "classpath:db.properties")
    public class MysqlProperties {
    
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    }
    

    读取原生的applicationContext.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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--使用原生的xml配置文件,此文件默认不读,需要在配置-->
        <bean id="dept" class="com.dx.bean.Dept">
            <property name="id" value="10"/>
            <property name="dname" value="研发部"/>
        </bean>
        <bean id="emp" class="com.dx.bean.Emp">
            <property name="id" value="1001"/>
            <property name="ename" value="汤姆"/>
            <property name="dept" ref="dept"/>
        </bean>
    </beans>
    
    @Data
    public class Emp {
        private Integer id;
        private String ename;
        private Dept dept;
    }
    
    @Data
    //开启链式调用
    @Accessors(chain = true)
    public class Dept {
        private Integer id;
        private String dname;
    }
    

profile配置文件

  • 三种工作环境配置文件:开发环境(dev)、生产环境(prod)、测试环境(test)

    #环境配置
    #需要激活:在application.yml中激活
    #命名规则:application-xxx.xml
    
    #开发环境配置
    server:
      #端口号
      port: 8001
      servlet:
        #根路径
        context-path: /web-dev
    
    #生产环境配置
    server:
      port: 8003
      servlet:
        context-path: /web-prod
    
    #测试环境
    server:
      port: 8002
      servlet:
        context-path: /web-test
    

    在测试的时候,需要修改pom的编译路径,确保把所有的配置文件都编译以后再测试

    <build>
        <!-- 将所有的配置文件都编译-->
        <resources>
            <resource>
                <directory>D:\workspace\SpringBoot-Code\02-spring-boot-config</directory>
                <includes>
                    <include>**/*.yml</include>
                    <include>application.yml</include>
                </includes>
            </resource>
        </resources>
    </build>
    

部署时配置文件

  • 外部配置文件

    在D盘放一个application.yml文件 端口指定为8009
    打包后使用命令行运行并且指定
    
    java -jar aaa.jar --spring.config.location=D:/application.yml
    
  • 命令修改配置文件

    可以使用命令行参数指定(文档一行写不下的时候,不要回车)
    
    java -jar aaa.jar --server.port=8888 --server.servlet.context-path=/bjpowernode
    

配置优先级

  • 后加载的会覆盖先加载的

  • 最后加载的优先级最高

  • spring boot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件

    其中同一目标下的properties文件的优先级大于yml文件

  • 配置文件可以放的位置和优先级
    classpath:/ --优先级4

    classpath:/config/ --优先级3

    file:./ --优先级2

    file:./config/ --优先级1

日志logback

配置日志级别
  • @RestController
    //注解式日志:动态为该类声明一个静态Logger成员变量
    //@Slf4j
    public class LogController {
    
        /**
         * 使用日志:
         * 在当前类中声明一个成员变量,类型为Logger类型
         */
        private Logger log = LoggerFactory.getLogger(LogController.class);
    
        @GetMapping("/getLog")
        public String getLog(){
    
            /**
             * 在各个日志框架都有级别
             * 错误error,警告warn,信息info,调试debug,底层trace
             *
             * springboot默认级别为info
             * 可以在application.yml配置文件中配置
             *
             * 例如:当级别为info,日志输入仅会输入info以及以上级别日志
             */
            //程序出现异常
            log.error("error级别的日志");
            //程序有可能出现问题,提醒或警告
            log.warn("warn级别的日志");
            //信息:记录用户操作
            log.info("info级别的日志");
            //调试信息:开发时使用
            log.debug("debug级别的日志");
            //程序底层信息
            log.trace("trace级别的日志");
            //热部署添加内容
            log.info("无需重启自动生效...");
            return "正在记录日志";
        }
    }
    
  • 全局配置 局部配置

    #配置日志
    logging:
      level:
        #使用root配置全局的日志级别
        root: info
        #局部日志级别,两种写法
    #    com.dx.controller: trace
        com:
          dx:
            controller: trace
    
自定义日志输出格式
  #自定义日志格式
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss SSS} %-5level [%thread] %logger -- %msg%n"
生成日志文件
  #日志在文件中的输出
  file:
    #日志文件名,在根目录下可以找到文件
    name: demo.log
    #指定日志文件路径,但是不能与那么同时配置,两个只能用一个
#    path: c:/log/

  #如果想既自定义文件名 又指定文件位置,可以导入logback.xml文件,如果使用logback.xml记得把这里的配置去掉,不然会冲突

logback.xml:可以直接使用,直接放到项目中就可以

<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
    <!-- 定义日志的根目录 -->
    <property name="LOG_HOME" value="d:/logback/" />
    <!-- 定义日志文件名称 -->
    <property name="appName" value="springboot"></property>
    <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
    <appender name="cc" class="ch.qos.logback.core.ConsoleAppender">
        <!--
        日志输出格式:
			%d表示日期时间,
			%thread表示线程名,
			%-5level:级别从左显示5个字符宽度
			%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
			%msg:日志消息,
			%n是换行符
        -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </layout>
    </appender>

    <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
    <appender name="ff" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 指定日志文件的名称
        d:/logback/springboot.log
        -->
        <file>${LOG_HOME}/${appName}.log</file>
        <!--
        当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
        TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
        -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--
            滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
            %i:当文件大小超过maxFileSize时,按照i进行文件滚动
            -->
            <fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!--
            可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
            且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
            那些为了归档而创建的目录也会被删除。
            -->
            <MaxHistory>365</MaxHistory>
            <!--
            当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
            -->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!-- 日志输出格式: -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
        </layout>
    </appender>

    <!--
		logger主要用于存放日志对象,也可以定义日志类型、级别
		name:表示匹配的logger类型前缀,也就是包的前半部分
		level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
		additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
		false:表示只用当前logger的appender-ref,true:
		表示当前logger的appender-ref和rootLogger的appender-ref都有效
    -->
    <!-- 自定义的 logger -->
    <logger name="com.bjpowernode.controller" level="debug" />
    <!-- Spring framework logger -->
    <logger name="org.springframework" level="debug" additivity="false"></logger>

    <!--
    root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
    要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
    -->
    <root level="info">
        <!--        控制台-->
        <appender-ref ref="cc" />
        <!--        滚动文件-->
        <appender-ref ref="ff" />
    </root>
</configuration>

工具

自定义配置提示
<!--实现自定义配置的提示功能-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
热部署
<!--热部署:更新代码或页面不需要重启服务,直接Build一下就可以-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
lombok
<!--简化工具
    作用:实现注解式get/set,构造方法,toString方法...注解式日志
    条件:1.依赖,2.IDEA插件
-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
//开启对象中set方法的链式调用
@Accessors(chain = true)
//链式调用
Student s3 = new Student()
    .setId(30)
    .setName("王五")
    .setAddress("深圳")
    .setScore(82.6);
System.out.println(s3);
注解式日志
//注解式日志:动态为该类声明一个静态Logger成员变量
@Slf4j
public class LogController {
    
	  //可以代替这条语句
//     private Logger log = LoggerFactory.getLogger(LogController.class);

    @GetMapping("/getLog")
    public String getLog(){
        log.error("error级别的日志");
        log.warn("warn级别的日志");
        log.info("info级别的日志");
        log.debug("debug级别的日志");
        log.trace("trace级别的日志");
        log.info("无需重启自动生效...");
        return "正在记录日志";
    }
}

处理静态资源

配置静态资源路径

  • 默认位置

    # 默认静态资源路径:
    #   "classpath:/META-INF/resources/"
    #   "classpath:/resources/"
    #   "classpath:/static/"
    #   "classpath:/public/"
    
  • 配置自定义位置

  • 配置静态资源访问路径

    spring:
      web:
        resources:
          #设置当前项目静态资源目录
          #注意:配置自定义静态资源目录时,需要添加原有配置项,否则会覆盖原有配置
          #classpath表示在类路径下查找资源,file表示在操作系统的文件系统下查找资源 d:/
          static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/my/,file:${custom.upload}
      mvc:
        #设置当前项目静态资源的访问前缀
        static-path-pattern: /static/**
        
    #配置自定义本地的资源路径
    custom:
      upload: E:\文件\图片\壁纸
    

使用静态资源jar包

  • 导入bootstrap和jqury依赖

    <!--jquery-->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>jquery</artifactId>
        <version>3.6.0</version>
    </dependency>
    <!--bootstrap-->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>bootstrap</artifactId>
        <version>3.4.1</version>
    </dependency>
    
  • 使用demo

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="/webjars/bootstrap/3.4.1/css/bootstrap.min.css">
    </head>
    <body>
      <div style="text-align: center">
          <h1>首页</h1>
          <div>
              <span id="hello">你好</span>
          </div>
          <div>
              <button type="button" class="btn btn-primary">按钮</button>
          </div>
          <div>
              <img src="/static/bbb.jpg" alt="" style="width: 300px; height: 200px">
          </div>
          <!--webjars:以jar包的方式引入静态资源,可以从maven配置依赖-->
          <script type="text/javascript" src="/webjars/jquery/3.6.0/jquery.min.js"></script>
          <script type="text/javascript">
              $(function (){
                  var str = $('#hello').html();
                  alert(str);
                  $('#hello').append('springboot');
              })
          </script>
      </div>
    </body>
    </html>
    

使用模板引擎 Thymeleaf

配置Thymeleaf

  • 开发时关闭缓存、发布时打开缓存

    spring:   
      #Thymeleaf的配置
      thymeleaf:
        #页面缓存,开发时关闭缓存、发布时打开缓存
        cache: false
        #前后缀,使用默认值就可以
    #    prefix: classpath:/templates/
    #    suffix: .html
    

动态属性获取数据

  • 取值

    <!--
    要在头上添加这个,不然${msg}会报错
    xmlns:th="http://www.w3.org/1999/xhtml"-->
    <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
    ......
    <!--
    通过动态属性来获取数据
    th:text 从域对象中获取数据,渲染到指定标签中,对特殊符号进行转码  会将传过来的标签直接当字符串输出
    th:utext 从域对象中获取数据,渲染到指定标签中,不处理特殊符号		传过来的标签会生效
    [[${msg}]]  加两个中括号,表示里面的内容是动态的
    -->
    <p th:text="${msg}"></p>
    <p th:text="${message}"></p>
    <p th:utext="${message}"></p>
    <p>[[${msg}]]</p>
        
    <!--表单取各种值-->
    <form action="/emp/edit" th:action="@{/emp/edit}" method="post">
        <div class="form-group">
            <label>员工编号</label>
            <!--取值方式一:${emp.id}-->
            <input type="text" name="id" class="form-control" th:value="${emp.id}">
        </div>
        <div class="form-group">
            <label>员工姓名</label>
            <!--取值方式二:*{ename}-->
            <input type="text" name="ename" class="form-control" th:value="*{ename}">
        </div>
        
        <div class="form-group">
            <label>入职日期</label>
            <!--取日期-->
            <input type="date" name="hiredate" class="form-control" th:value="${#dates.format(emp.hiredate, 'yyyy-MM-dd')}">
        </div>
        <div class="form-group">
            <label>员工性别</label>
            <!--select取值-->
            <select name="gender" class="form-control">
                <option value=""></option>
                <option value="1" th:selected="${emp.gender=='1'}">男性</option>
                <option value="2" th:selected="${emp.gender=='2'}">女性</option>
            </select>
        </div>
        <div class="form-group">
            <button type="button" class="btn btn-primary">提交</button>
        </div>
    </form>
    
  • 循环

    <tr th:each="emp : ${empList}">
        <td>[[${emp.id}]]</td>
        <td th:text="${emp.ename}"></td>
    </tr>
    
  • 判断

    <td>
        <span th:if="${emp.gender=='1'}">男性</span>
        <span th:if="${emp.gender=='2'}">女性</span>
    </td>
    <td th:switch="${emp.gender}">
        <span th:case="'1'">帅哥</span>
        <span th:case="'2'">靓女</span>
    </td>
    
  • 日期格式化

    //日期格式化
    //在成员变量上添加注解  @DateTimeFormat:接收参数时格式化		@JsonFormat:发送参数时格式化
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date hiredate;
    
    <!--动态页面中的日期格式化-->
    <td th:text="${#dates.format(emp.hiredate, 'yyyy-MM-dd')}"></td>
    
  • 自动根路径 @{...}

    #在application.yml文件中
    #修改项目的根路径
    server:
      servlet:
        context-path: /boot
    
    <!--
    静态路径:href="/emp/list"
    动态路径:th:href="@{/emp/list}"
    可以设置一个静态的和一个动态的,静态的留给前端用
    -->
    <p><a href="/emp/list" th:href="@{/emp/list}">员工列表</a></p>
    
  • 路径传参

    <td>
        <!--路径传参-->
        <a href="/emp/get/1" th:href="@{'/emp/get/' + ${emp.id}}" class="btn btn-warning btn-xs">编辑</a>&emsp;
        <!--路径拼接传参,传的是名值对-->
        <a href="#" th:href="@{/emp/remove(id=${emp.id},ename=${emp.ename},job=${emp.job})}" class="btn btn-danger btn-xs">删除</a>
    </td>
    
    <!--表单传参-->
    <form th:action="@{/auth/login}" method="post" style="width: 350px;margin: 10px auto;">
        <div style="color: red;">[[${message}]]</div>
        <div class="form-group">
            <label>用户名</label>
            <input type="text" name="username" class="form-control">
        </div>
        <div class="form-group">
            <label>密码</label>
            <input type="password" name="password" class="form-control">
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-primary">登录</button>
        </div>
    </form>
    

SpringMVC配置

/**
 * 自定义WebMvc配置类
 * 实现一个接口WebMvcConfigurer的配置类,相当于原生springmvc.xml配置文件
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 动态页面访问映射
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/page/login").setViewName("login");
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/page/index").setViewName("index");
    }

    /**
     * 注册拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry
                //配置拦截器对象
                .addInterceptor(new LoginInterceptor())
                //配置拦截器拦截路径
                .addPathPatterns("/**")
                //配置拦截器放行路径(白名单)
                .excludePathPatterns("/static/**", "/webjars/**", "/auth/login", "/page/login", "/");
    }

    /**
     * 对静态资源处理
     */
    // @Override
    // public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //     registry
    //             .addResourceHandler("/static2/**")
    //             .addResourceLocations("classpath:/my2/");
    // }

    /**
     * 添加自定义的数据格式转换器
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // registry.addFormatter();
        registry.addConverter(new PointConverter());
    }

    /**
     * 前后端分离的项目:设置全局跨域处理
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    }
}

拦截器

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object loginUser = request.getSession().getAttribute("loginUser");
        if(loginUser == null){
            request.setAttribute("message", "没有登录");
            request.getRequestDispatcher("/page/login").forward(request, response);
            return false;
        }
        return true;
    }
}

类型转换器

/**
 * 自定义的类型转换器:
 * 作用:将String转换为自定义Point
 */
public class PointConverter implements Converter<String, Point> {
    @Override
    public Point convert(String source) {
        String[] ss = source.split(",");
        Point point = new Point(ss[0], ss[1]);
        return point;
    }
}
<p><a th:href="@{/getPoint(point='15,58')}">类型转换器案例</a></p>

web中的三大组件

  • servlet、filter、listener

  • 默认不支持,需要在配置类中配置bean

  • 配置三大组件

    @Configuration
    public class ServletConfig {
    
        /**
         * 注册servlet
         */
        @Bean
        public ServletRegistrationBean servletRegistrationBean(){
            ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
            //设置Servlet对象
            servletRegistrationBean.setServlet(new DemoServlet());
            //设置路径映射
            servletRegistrationBean.addUrlMappings("/demo");
            //设置初始化参数
            //单个
            servletRegistrationBean.addInitParameter("msg", "hello");
            //批量
            // servletRegistrationBean.setInitParameters();
            return servletRegistrationBean;
        }
    
        /**
         * 注册Filter
         */
        @Bean
        public FilterRegistrationBean filterRegistrationBean(){
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            filterRegistrationBean.setFilter(new DemoFilter());
            filterRegistrationBean.addUrlPatterns("/*");
            // filterRegistrationBean.addInitParameter();
            return filterRegistrationBean;
        }
    
        /**
         * 注册Listener
         */
        @Bean
        public ServletListenerRegistrationBean servletListenerRegistrationBean(){
            ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
            servletListenerRegistrationBean.setListener(new DemoListener());
            return servletListenerRegistrationBean;
        }
    }
    

    DemoFilter.java

    @WebFilter("/*")
    public class DemoFilter implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("DemoFilter执行了.........");
    
            //过滤器放行
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    

    DemoListener.java

    @WebListener
    public class DemoListener implements ServletContextListener {
    
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            System.out.println("DemoListener执行了.....");
        }
    }
    

    DemoServlet.java

    @WebServlet("/demo")
    public class DemoServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("DemoServlet.........." + msg);
            resp.getWriter().write("ok");
        }
    
        private String msg;
    
        @Override
        public void init(ServletConfig config) throws ServletException {
            msg = config.getInitParameter("msg");
        }
    }
    

全局异常处理器

  • public class CustomException extends RuntimeException{
    }
    
  • /**
     * 全局异常处理器
     * @RestControllerAdvice:全局异常处理器的注解
     */
    // @ControllerAdvice
    @RestControllerAdvice
    public class GlobalExceptionHander {
    
        @ExceptionHandler(CustomException.class)
        public Map<String, Object> customExceptionHandle(CustomException e){
            Map<String, Object> map = new HashMap<>();
            map.put("code", -2);
            map.put("message", "自定义异常发生");
            return map;
        }
    
        @ExceptionHandler(Exception.class)
        public Map<String, Object> exceptionHandle(Exception e){
            Map<String, Object> map = new HashMap<>();
            map.put("code", -1);
            map.put("message", "其他异常发生");
            return map;
        }
    }
    

全局日期格式化

  • 在SpringMVC使用@DateTimeFormat和@JsonFormat

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date hiredate;
    
  • application.yml

    #全局输入日期格式化,局部@DateTimeFormat
    spring: 
      mvc: 
        format:
          date: yyyy-MM-dd HH:mm:ss
          
      #全局输出日期格式化,局部@JsonFormat
      jackson:
        date-format: yyyy-MM-dd HH:mm:ss
        time-zone: GMT+8
    

配置文件上传

#配置文件上传
spring: 
  servlet:
    multipart:
      #单个文件大小限制
      max-file-size: 1MB
      #单次请求总文件大小限制
      max-request-size: 10MB

前后端分离

设置全局跨域处理

  • WebConfig.java配置文件

    /**
         * 前后端分离的项目:设置全局跨域处理
         * 浏览器的同源策略:
         * 在发送异步请求时,请求地址中 协议、服务器地址、服务器端口号必须保持一致
         * 如果不一致就违反浏览器同源策略,浏览器禁止访问,会接收到一个CORS错误
         *
         * 注意:协议http与https不同源
         *      地址localhost与127.0.0.1不同源
         *
         * 前后端分离的项目:设置全局跨域处理
         */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
            //跨域访问的路径(资源)
            .addMapping("/**")
            //设置允许跨域的服务器地址
            // .allowedOriginPatterns("http://localhost:63342")
            .allowedOriginPatterns("*")
            //设置允许跨域访问的请求方式
            // .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
            .allowedMethods("*")
            //设置允许跨域访问时的头部信息
            // .allowedHeaders()
            //设置暴露给请求服务器头部信息
            // .exposedHeaders()
            //设置跨域访问时是否允许携带凭证(cookie中JSESSIONID)
            .allowCredentials(true)
            //跨域访问时会先发送一个询问请求(询问当前请求是否允许跨域)
            //设置询问请求的发送周期
            .maxAge(3600);
    }
    
  • 后端资源

    @RestController
    public class UserController {
    
        @GetMapping("/user/list")
        public List<User> list(){
            User user1 = new User(1, "zhangsan", "123");
            User user2 = new User(2, "lisi", "123");
            User user3 = new User(3, "wangwu", "123");
            User user4 = new User(4, "zhaoliu", "123");
            return Arrays.asList(user1,user2,user3,user4);
        }
    }
    
  • 前端请求(另一个项目)

    $.ajax('http://localhost:8080/boot/user/list',{
        type:'get',
        success:function (res) {
            var str='';
            $.each(res,function () {
                str+='<tr>';
                    str+='<td>'+this.id+'</td>';
                    str+='<td>'+this.username+'</td>';
                    str+='<td>'+this.password+'</td>';
                    str+='</tr>';
            });
            $('#tab').html(str);
        }
    });
    

外部Tomcat

  • 外部Tomcat支持tomcat jsp,内部Tomcat不支持tomcat jsp

  • 新建项目的时候与SpringBoot相似,唯一不同是打包方式改为war

  • 自己配置TomCat

  • 将静态资源文件放到 webapp/WEB-IF/ 下面

  • 自动生成ServletInitializer.java文件

    /**
     * 在外部tomcat启动服务器时加载springboot程序
     * 如果把这个类去掉,不会启动springboot,也就无法使用@Controller等组件
     */
    public class ServletInitializer extends SpringBootServletInitializer {
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(SpringBoot05JspApplication.class);
        }
    }
    
  • 可以使用 jsp 文件

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <base href="${pageContext.request.contextPath}/">
        <title>Title</title>
    </head>
    <body>
        <div style="text-align: center">
            <h1>首页</h1>
            <p><a href="/hello">测试</a></p>
        </div>
    </body>
    </html>
    
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <base href="${pageContext.request.contextPath}/">
        <title>成功</title>
    </head>
    <body>
        <div style="text-align: center">
            <h1>成功</h1>
            <p>${message}</p>
        </div>
    </body>
    </html>
    
  • Controller

    @Controller
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello(HttpServletRequest request){
            System.out.println("HelloController中的hello方法执行了....");
            request.setAttribute("message", "hello jsp");
            return "success";
        }
    }
    
  • application.yml

    #配置jsp页面的前缀与后缀
    spring:
      mvc:
        view:
          prefix: /WEB-INF/pages/
          suffix: .jsp
    

后端数据校验

  • 新建项目时除了选中工具和核心包外,还要选中IO中的Validation依赖

  • 自动引入依赖

    <!--后台数据校验-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    

数据校验

  • /**
     * hibernate-validator框架,后台数据校验,通过注解的方式来实现
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    public class Student {
    
        /**
         * @NotNull 数据不能为null
         * @NotEmpty 数据不能为null,也不能为空字符串
         * @NotBlank 数据不能为null,也不能为空字符串,也不能由空白字符组成
         * @Size 校验字符串、数组、集合的长度
         * @Range 校验数字的范围
         * @Email 校验有效是否合法
         * @Pattern 通过正则表达式校验
         *
         * 注解通用配置项:message,校验失败时提示信息
         */
        @NotBlank(message = "姓名不能为空")
        @Size(message = "姓名长度为4~10个字符", min = 4, max = 10)
        private String name;
        @NotNull(message = "年龄不能为空")
        @Range(message = "年龄范围为0~100岁", min = 0, max = 100)
        private Integer age;
        @NotBlank(message = "邮箱不能为空")
        //xxx@xx.xx
        @Email(message = "邮箱格式不合法")
        //正则校验
        // @Pattern(message = "", regexp = "")
        private String email;
    }
    
  • 开启数据校验

    @RestController
    public class ValidationController {
    
        /**
         * 开启校验 @Validated
         *
         * 在方法的参数上标注@Validated表示开启后台数据校验,每个开启的数据校验数据之后,需要跟随一个BindingResult类型的参数
         * BindingResult对象用于接收校验失败的信息
         */
        @PostMapping("/save")
        public String save(@Validated Student student, BindingResult bindingResult){
            //判断校验是否通过
            if(bindingResult.hasErrors()){
                List<ObjectError> allErrors = bindingResult.getAllErrors();
                for (ObjectError error : allErrors) {
                    System.out.println(error.getDefaultMessage());
                }
            }else{
                System.out.println(student);
            }
            return "ok";
        }
    }
    

配置文件校验

  • application.yml

    #用于校验配置文件
    user:
      username: zhangsan
      age: 20
      email: aa@aa.aa
    
  • 配置类

    @SpringBootApplication
    //使配置类的注解生效
    @EnableConfigurationProperties(UserProperties.class)
    public class Springboot06ValidationApplication {
        public static void main(String[] args) {
            SpringApplication.run(Springboot06ValidationApplication.class, args);
        }
    }
    
    @Data
    @ConfigurationProperties(prefix = "user")
    //添加批量读取配置文件的类上
    @Validated
    public class UserProperties {
    
        @NotBlank(message = "用户名不能为空")
        @Size(message = "用户名长度为6~16个字符", min = 6, max = 16)
        private String username;
        @NotNull(message = "年龄不能为空")
        @Range(message = "年龄范围为0~100岁", min = 0, max = 100)
        private Integer age;
        @NotBlank(message = "邮箱不能为空")
        @Email(message = "邮箱格式不合法")
        private String email;
    }
    

正则表达式

  • 可以参考JDK手册:java.util.regex / Pattern

  • 后端使用正则表达式

    public class ParttenTests {
    
        @Test
        public void run(){
            /**
             * 正则表达式,验证字符串是否符合指定的规则
             */
            //1.String类
            String str = "zhang#123";
            System.out.println(str.matches("[abc]"));
            System.out.println(str.matches("[^abc]"));
            System.out.println(str.matches("[a-zA-Z]"));
            System.out.println(str.matches("[0-9]"));
            System.out.println(str.matches("[a-zA-Z0-9]"));
            System.out.println(str.matches("\\S"));
            System.out.println(str.matches("\\w{5,10}"));
            System.out.println(str.matches(".{5,10}"));
            System.out.println(str.matches(".*"));
            System.out.println("14345678901".matches("^1[3|5|6|7|8|9]\\d{9}$"));
    
            //2.Pattern类
            boolean result = Pattern.matches("^[0-9]+$", "123");
            System.out.println(result);
    
            //3.Matcher类
            Pattern pattern = Pattern.compile("^[0-9]+$");
            Matcher matcher = pattern.matcher("123a");
            System.out.println(matcher.matches());
        }
    }
    
  • 前端使用正则表达式

    <script type="text/javascript">
      var regex = /^[0-9]+$/;
      var result = regex.test('123');
      alert(result);
    </script>
    
  • 使用注解

    //正则校验
    // @Pattern(message = "", regexp = "")
    private String email;
    

aop

  • 引入依赖

    <!--aop-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • Controller

    @RestController
    public class AopController {
    
        @GetMapping("/list")
        public String list(){
            System.out.println("AopController中的list方法....");
            return "ok";
        }
    }
    
  • AopAspect.java

    /**
     * 切面类
     */
    @Component
    @Aspect
    // @Order(1)	//设置切面的启动顺序
    public class AopAspect {
    
        //公共切点表达式,方式一
        public static final String POINT_CUT = "execution(* com.bjpowernode.controller.*.*(..))";
        //公共切点表达式,方式二
        @Pointcut("execution(* com.bjpowernode.controller.*.*(..))")
        public void exp(){}
        
        /**
         * 前置通知
         */
        @Before("execution(* com.bjpowernode.controller.*.*(..))")
        public void before(){
            System.out.println("切面的前置通知...");
        }
        /**
         * 后置通知
         */
        @After(POINT_CUT)
        public void after(){
            System.out.println("切面的后置通知...");
        }
        /**
         * 返回通知
         */
        @AfterReturning(value = "exp()", returning = "result")
        public void returning(Object result){
            System.out.println("切面的返回通知..." + result);
        }
    }
    

连接数据库

使用jdbc

  • 创建项目选择 jdbc api和Mysql Dirver 依赖

  • application.yml

    #使用jdbc
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/student
        username: root
        password: 111111
    
        #?切换数据源,需要配置才能生效
        type: com.alibaba.druid.pool.DruidDataSource
    
        #数据源配置
        initialSize: 5
        minIdle: 10
        maxActive: 50
        maxWait: 30000
    
  • entity层 略

  • dao

    public class DeptDaoImpl implements DeptDao {
    
        /**
         * 注入JdbcTemplate对象
         * JdbcTemplate是spring-jdbc中提供的工具类,用于操作数据库
         */
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public int insert(Dept dept) {
            String sql="insert into dept(dname,loc) values(?,?)";
            return jdbcTemplate.update(sql,dept.getDname(),dept.getLoc());
        }
    }
    
  • 测试

    @SpringBootTest
    class Springboot08JdbcApplicationTests {
    
        @Autowired
        private DataSource dataSource;
    
        //查看数据源配置信息
        @Test
        void contextLoads() {
            //自带数据源是: HikariDataSource
            System.out.println("数据源:"+dataSource);
        }
    
        /**
         * 使用jdbc完成查询
         */
        @Test
        void testJdbc() throws SQLException {
            Connection connection = dataSource.getConnection();
            String sql="select deptno,dname,loc from dept where deptno=?";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1,60);
            ResultSet resultSet = preparedStatement.executeQuery();
            while (resultSet.next()){
                int deptno = resultSet.getInt("deptno");
                String dname = resultSet.getString("dname");
                String loc = resultSet.getString("loc");
                System.out.println("编号="+deptno+" 名字="+dname+" 地址="+loc);
            }
            resultSet.close();
            preparedStatement.close();
            connection.close();
        }
    }
    

使用数据库连接池Druid

自定义配置类
  • 引入依赖,版本号不能省略

    <!--德鲁伊数据源,自己写配置类-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.10</version>
    </dependency>
    
  • 配置文件按application.yml

    #配置数据源
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/student
        username: root
        password: 111111
    
        #切换数据源,需要配置才能生效
        type: com.alibaba.druid.pool.DruidDataSource
    
        #数据源配置
        initialSize: 5
        minIdle: 10
        maxActive: 50
        maxWait: 30000
    
        #开启监控: 监控stat,SQL防火墙wall,日志slf4j,需要配置
        filters: stat,wall,slf4j
    
  • 自定义的配置类

    /**
     * 自定义德鲁伊数据源配置类
     */
    @Configuration
    public class DruidConfig {
    
        @Bean
        //使用注解使配置文件的数据源生效
        @ConfigurationProperties(prefix = "spring.datasource")
        public DruidDataSource dataSource(){
            DruidDataSource druidDataSource = new DruidDataSource();
            //可以在这配置,也可以使用配置文件配置
    //        druidDataSource.setInitialSize(10);
            return druidDataSource;
        }
    
        /**
         * sql监控的servlet
         */
        @Bean
        public ServletRegistrationBean servletRegistrationBean(){
            ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
            servletRegistrationBean.setServlet(new StatViewServlet());
            servletRegistrationBean.addUrlMappings("/druid/*");
            servletRegistrationBean.addInitParameter("loginUsername","admin");
            servletRegistrationBean.addInitParameter("loginPassword", "123");
            return servletRegistrationBean;
        }
    
        /**
         * sql监控过滤器
         */
        @Bean
        public FilterRegistrationBean filterRegistrationBean(){
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            filterRegistrationBean.setFilter(new WebStatFilter());
            filterRegistrationBean.addUrlPatterns("/*");
            //排除路径
            filterRegistrationBean.addInitParameter("exclusions", "/druid/*,*.js,*.css,*.jpg,*.png");
            return filterRegistrationBean;
        }
    }
    
自带配置类
  • 引入依赖

    <!--自带启动器的德鲁伊,配置类不用写了-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    
  • 配置

    #使用自带启动类的德鲁伊
    spring:
      datasource:
        #切换数据源
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          #必要信息
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/student
          username: root
          password: 111111
          #连接池基本信息
          initial-size: 6
          min-idle: 7
          max-active: 15
          max-wait: 20000
          #sql监控
          filter: stat,wall,slf4j
          #serlvet
          stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
            login-password: 111
            login-username: root
          #filter
          web-stat-filter:
            enabled: true
            url-pattern: /*
            exclusions: "/druid/*,*.js,*.css,*.jpg,*.png"
    

JdbcTemplate工具

  • 封装了jdbc的工具

  • DeptDaoImpl.java

    @Repository
    public class DeptDaoImpl implements DeptDao {
    
        /**
         * 注入JdbcTemplate对象
         * JdbcTemplate是spring-jdbc中提供的工具类,用于操作数据库
         */
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public int insert(Dept dept) {
            String sql="insert into dept(dname,loc) values(?,?)";
            return jdbcTemplate.update(sql,dept.getDname(),dept.getLoc());
        }
    
        @Override
        public List<Dept> select() {
            /**
             * queryForList() 批量查询返回数据封装在map集合中
             * query(sql, rowMapper) 批量查询返回数据封装在指定的实体对象中
             *
             * RowMapper 行映射器,将字段与实体中的属性对照
             * BeanPropertyRowMapper 是RowMapper接口的实现类对象,根据字段名称与实体类中属性名称实现自动映射
             */
            String sql = "select deptno, dname, loc from dept order by deptno desc";
            //手动映射
            // jdbcTemplate.query(sql, new RowMapper<Dept>(){
            //     @Override
            //     public Dept mapRow(ResultSet rs, int rowNum) throws SQLException {
            //         Dept dept = new Dept();
            //         dept.setDeptno(rs.getInt("deptno"));
            //         dept.setDname(rs.getString("dname"));
            //         dept.setLoc(rs.getString("loc"));
            //         return dept;
            //     }
            // });
            RowMapper<Dept> rowMapper = new BeanPropertyRowMapper<>(Dept.class);
            List<Dept> list = jdbcTemplate.query(sql, rowMapper);
            return list;
        }
    
        @Override
        public Dept selectById(Integer deptno) {
            String sql = "select deptno, dname, loc from dept where deptno=?";
            /**
             * queryForObject(String sql, RowMapper rowMapper) 返回结果为单行多列
             * queryForObject(String sql, Class<T> requiredType) 返回结果为单行单列
             */
            // int i = jdbcTemplate.queryForObject(sql, Integer.class);
            RowMapper<Dept> rowMapper = new BeanPropertyRowMapper<>(Dept.class);
            Dept dept = jdbcTemplate.queryForObject(sql, rowMapper, deptno);
            return dept;
        }
    }
    
  • 测试类

    @SpringBootTest
    class Springboot08JdbcApplicationTests {
    
        @Autowired
        private DataSource dataSource;
    
        @Test
        void contextLoads() {
            //自带数据源是: HikariDataSource
            //查看数据源配置信息
            System.out.println("数据源:"+dataSource);
            if (dataSource instanceof DruidDataSource){
                DruidDataSource druidDataSource=(DruidDataSource) dataSource;
                System.out.println(druidDataSource.getInitialSize());
                System.out.println(druidDataSource.getMinIdle());
                System.out.println(druidDataSource.getMaxActive());
                System.out.println(druidDataSource.getMaxWait());
            }
        }
    
        @Autowired
        DeptDao deptDao;
        @Test
        void testInsert(){
            Dept dept = new Dept();
            dept.setDname("采购部");
            dept.setLoc("深圳");
            int result = deptDao.insert(dept);
            System.out.println(result);
        }
    
        @Test
        void testSelect(){
            List<Dept> list = deptDao.select();
            for (Dept dept : list) {
                System.out.println(dept);
            }
        }
    
        @Test
        void testSelectById(){
            Dept dept = deptDao.selectById(48);
            System.out.println(dept);
        }
    }
    

整合MyBatis

  • 创建项目时,加上MyBatisFramwork依赖

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    
  • 数据库连接池依赖和分页插件

    <!--德鲁伊数据源-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <!--分页插件-->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.4.2</version>
    </dependency>
    
  • 配置文件

    #配置数据源
    spring:
      jackson:
        date-format: yyyy-MM-dd
        time-zone: GMT+8
      mvc:
        format:
          date: yyyy-MM-dd
      datasource:
        #切换数据源
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          #必要信息
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/bjpowernode
          username: root
          password: root
          #连接池基本信息
          initial-size: 6
          min-idle: 7
          max-active: 15
          max-wait: 20000
          #sql监控
          filter: stat,wall,slf4j
          #serlvet
          stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
            login-password: 111
            login-username: root
          #filter
          web-stat-filter:
            enabled: true
            url-pattern: /*
            exclusions: "/druid/*,*.js,*.css,*.jpg,*.png"
    
    #mybatis配置
    #1.SqlSessionFactory配置bean -- 在自动化配置中已实现
    #2.mapper代理对象配置bean -- 通过注解的方式来实现
    mybatis:
      #加载核心配置文件
      #  config-location: classpath:mybatis.xml
      #加载mapper文件
      mapper-locations: classpath:mapper/*.xml
      #  mapper-locations: classpath:mapper/**/*.xml
      #类型别名
      type-aliases-package: com.bjpowernode.entity
      #全局参数: 与加载核心配置文件项冲突
      configuration:
        #开启下划线转驼峰
        map-underscore-to-camel-case: true
        #日志输出
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
      #插件:分页(单独配置)
    
    #分页组件pagehelper
    pagehelper:
      #自动识别数据库,应用数据库方言
      #mysql: select * from table_name limit offset,pageSize
      #oracle: select * from (select *,rownum myrow from (select * from table_name) where rownum <= 10) where myrow >= 5
      #  auto-dialect: true
      #合理化分页
      reasonable: true
    
  • @SpringBootApplication
    /**
     * 批量配置mapper代理
     * 需要指定dao接口所在包
     */
     @MapperScans({
             @MapperScan(basePackages = "com.bjpowernode.dao"),
             @MapperScan(basePackages = "com.bjpowernode.dao2")
     })
     @MapperScan(basePackages = "com.bjpowernode.dao")
    public class Springboot09MybatisApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(Springboot09MybatisApplication.class, args);
        }
    }
    
  • entity

    @Data
    public class Emp {
        private Integer empno;
        private String ename;
        private String job;
        private Integer mgr;
        private Date hiredate;
        private Double sal;
        private Double comm;
        private Integer deptno;
    }
    
  • dao

    /**
     * 使用注解来配置mapper代理对象
     *
     * @Mapper
     * 作用:为当前接口生成mapper代理对象,并将代理对象放入ioc容器
     *      类似于spring中单个接口代理对象的配置
     */
    @Mapper
    public interface EmpDao {
    
        List<Emp> select();
    
        Emp selectById(Integer id);
    
        int insert(Emp entity);
    
        int update(Emp entity);
    
        int delete(Integer id);
    }
    
  • mapper

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.bjpowernode.dao.EmpDao">
    
        <select id="select" resultType="emp">
            select empno,ename,job,mgr,hiredate,sal,comm,deptno from emp order by empno asc
        </select>
        <select id="selectById" parameterType="int" resultType="Emp">
            select empno,ename,job,mgr,hiredate,sal,comm,deptno from emp where empno=#{empno}
        </select>
        <insert id="insert" parameterType="com.bjpowernode.entity.Emp">
            insert into emp(ename,job,mgr,hiredate,sal,comm,deptno)
            values(#{ename},#{job},#{mgr},#{hiredate},#{sal},#{comm},#{deptno})
        </insert>
        <update id="update" parameterType="com.bjpowernode.entity.Emp">
            update emp set ename=#{ename},job=#{job},mgr=#{mgr},hiredate=#{hiredate},sal=#{sal},comm=#{comm},deptno=#{deptno}
            where empno=#{empno}
        </update>
        <delete id="delete" parameterType="int">
            delete from emp where empno=#{empno}
        </delete>
    </mapper>
    
  • service

    public interface EmpService {
    
        Map<String, Object> page(Integer pageNumber, Integer pageSize);
    
        Emp get(Integer id);
    
        boolean save(Emp entity);
    
        boolean update(Emp entity);
    
        boolean remove(Integer id);
    }
    
    
    @Service
    public class EmpServiceImpl implements EmpService {
    
        @Autowired
        private EmpDao empDao;
    
        @Override
        public Map<String, Object> page(Integer pageNumber, Integer pageSize) {
            PageHelper.startPage(pageNumber, pageSize);
            PageInfo<Emp> pageInfo = new PageInfo<>(empDao.select());
            Map<String, Object> pageMap = new HashMap<>();
            pageMap.put("list", pageInfo.getList());
            pageMap.put("total", pageInfo.getTotal());
            return pageMap;
        }
    
        @Override
        public Emp get(Integer id) {
            return empDao.selectById(id);
        }
    
        /**
         * springboot中事务管理
         * 使用注解的方式来实现
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public boolean save(Emp entity) {
            return empDao.insert(entity) > 0;
        }
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public boolean update(Emp entity) {
            return empDao.update(entity) > 0;
        }
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public boolean remove(Integer id) {
            return empDao.delete(id) > 0;
        }
    }
    
  • Controller

    @RestController
    @RequestMapping("/emp")
    public class EmpController {
    
        @Autowired
        private EmpService empService;
    
        @GetMapping("/page")
        public Map<String, Object> page(Integer pageNumber, Integer pageSize){
            Map<String, Object> pageMap = empService.page(pageNumber, pageSize);
            Map<String, Object> result = new HashMap<>();
            result.put("code", 200);
            result.put("message", "ok");
            result.put("data", pageMap);
            return result;
        }
    
        @GetMapping("/get/{id}")
        public Map<String, Object> get(@PathVariable("id") Integer id){
            Map<String, Object> result = new HashMap<>();
            result.put("code", 200);
            result.put("message", "ok");
            result.put("data", empService.get(id));
            return result;
        }
    
        @PostMapping("/save")
        public Map<String, Object> save(@RequestBody Emp emp){
            Map<String, Object> result = new HashMap<>();
            result.put("code", 200);
            result.put("message", "ok");
            result.put("data", empService.save(emp));
            return result;
        }
    
        @PutMapping("/edit")
        public Map<String, Object> edit(@RequestBody Emp emp){
            Map<String, Object> result = new HashMap<>();
            result.put("code", 200);
            result.put("message", "ok");
            result.put("data", empService.update(emp));
            return result;
        }
    
        @DeleteMapping("/remove/{id}")
        public Map<String, Object> remove(@PathVariable("id") Integer id){
            Map<String, Object> result = new HashMap<>();
            result.put("code", 200);
            result.put("message", "ok");
            result.put("data", empService.remove(id));
            return result;
        }
    }
    

Api接口文档 swagger

  • 引入依赖

    <!--swagger starter-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>
    
  • 配置文件

    #swagger自定义配置
    #测试访问文档页面:http://localhost:8080/swagger-ui/index.html
    swagger3:
      base-package: com.dx.controller
      name: xxx
      url: https://gitee.com/
      email: 1233453534@qq.com
      version: 1.0
      group-name: dx
      title: "标题 "
      description: "描述信息"
      terms-of-service-url: https://gitee.com/
      license: cxs
      license-url: https://gitee.com/
    
    #时间格式
    spring:
      jackson:
        date-format: yyyy-MM-dd HH:mm:ss
        time-zone: GMT+8
      mvc:
        format:
          date: yyyy-MM-dd HH:mm:ss
        #解决swagger3.0和springboot整合的时问题
        pathmatch:
          matching-strategy: ant_path_matcher
    
  • 读取配置的类

    @Data
    @ConfigurationProperties(prefix = "swagger3")
    public class SwaggerProperties {
    
        // 扫描的包
        // 给这个包下面的接口创建文档
        private String basePackage;
        // 作者姓名
        private String name;
        // 作者主页链接
        private String url;
        // 作者邮箱
        private String email;
        // 版本号
        private String version;
        // 分组名称
        private String groupName;
        // 文档标题
        private String title;
        //文档描述
        private String description;
        // 组织地址
        private String termsOfServiceUrl;
        // 许可证
        private String license;
        // 许可链接
        private String licenseUrl;
    }
    
  • 注入配置类,配置swagger

    @Configuration
    @EnableConfigurationProperties(SwaggerProperties.class)
    public class SwaggerConfig {
    
        @Autowired
        private SwaggerProperties swaggerProperties;
    
        /**
         * 配置swagger中的标题,描述,联系人,联系方法...
         */
        public ApiInfo getApiInfo(){
            //创建联系人对象
            Contact contact = new Contact(swaggerProperties.getName(), swaggerProperties.getUrl(), swaggerProperties.getEmail());
            return new ApiInfoBuilder()
                    .contact(contact)
                    .title(swaggerProperties.getTitle())
                    .description(swaggerProperties.getDescription())
                    .version(swaggerProperties.getVersion())
                    .license(swaggerProperties.getLicense())
                    .licenseUrl(swaggerProperties.getLicenseUrl())
                    .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
                    .build();
        }
    
        /**
         * swagger的配置,基础信息,设置接口路径等等
         * RequestHandlerSelectors.basePackage()通过指定基础包来生成文档
         * RequestHandlerSelectors.withMethodAnnotation()通过指定的方法上注解来实现生成文档
         */
        @Bean
        public Docket docket(){
            return new Docket(DocumentationType.OAS_30)
                    .apiInfo(getApiInfo())
                    .select()
                    //文档的生成的位置,两种方式
                    // .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
                    .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                    .paths(PathSelectors.any())
                    .build();
        }
    }
    
  • 实体

    @Data
    @ApiModel("学生实体")
    public class Student {
    
        @ApiModelProperty("学生编号")
        private Integer id;
        @ApiModelProperty("学生姓名")
        private String name;
        @ApiModelProperty("学生住址")
        private String address;
        @ApiModelProperty("出生日期")
        private Date birthday;
    }
    
  • Controller

    • 配置对类的描述注解

    • 配置对方法的描述的注解

    • 对参数的注解

      @RestController
      @RequestMapping("/student")
      public class StudentController {
      
          @ApiOperation("分页+条件查询 学生信息")
          @ApiImplicitParams({
                  @ApiImplicitParam(name = "pageNumber", value = "当前页码", required = false, dataType = "Integer",defaultValue = "1", paramType = "query"),
                  @ApiImplicitParam(name = "pageSize", value = "每页条数", required = false, dataType = "Integer",defaultValue = "10", paramType = "query")
          })
          @ApiResponses({
                  @ApiResponse(code = 0, message = "成功"),
                  @ApiResponse(code = -1, message = "失败")
          })
          @GetMapping("/page")
          public Result page(@RequestParam(value = "pageNumber",defaultValue = "1") Integer pagerNumber,
                             @RequestParam(value = "pageSize",defaultValue = "5") Integer pageSize,
                             Student student) {
              System.out.println(pagerNumber);
              System.out.println(pageSize);
              System.out.println(student);
              return Result.success();
          }
      
          @GetMapping("/get/{id}")
          @ApiOperation("根据ID获取学生信息")
          @ApiImplicitParam(name="id",value = "学生编号",required = true, dataType="Integer", paramType = "path")
          public Result get(@PathVariable("id") Integer id) {
              System.out.println(id);
              return Result.success();
          }
      
          @PutMapping("/edit")
          @ApiOperation("编辑学生信息")
          public Result edit(@RequestBody Student student){
              System.out.println(student);
              return Result.success();
          }
      
          @DeleteMapping("/remove/{id}")
          @ApiOperation("根据编号删除学生信息")
          @ApiImplicitParam(name = "id", value = "学生编号", required = true, dataType = "Integer", paramType = "path")
          public Result remove(@PathVariable("id") Integer id){
              System.out.println(id);
              return Result.success();
          }
      }
      
  • 查看swagger页面

    #测试访问文档页面:http://localhost:8080/swagger-ui/index.html
    

其他功能

spring boot 异步

  • 异步调用需要配置注解@Async

  • 启动类 开启异步调用

    //开启异步调用
    @EnableAsync
    
  • 案例:统计耗时

    @Service
    public class AsyncService {
    
        /**
         * 异步调用注解
         * 在处理请求的业务时,其中核心业务必须在同步处理中完成
         *                  辅助业务可以在异步处理中完成
         */
        @Async
        public void task1(){
            try {
                long start = System.currentTimeMillis();
                //模拟程序执行耗时
                Thread.sleep(1000);
                long end = System.currentTimeMillis();
                System.out.println("task1耗时:" + (end - start) + "毫秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Async
        public void task2(){
            try {
                long start = System.currentTimeMillis();
                //模拟程序执行耗时
                Thread.sleep(2000);
                long end = System.currentTimeMillis();
                System.out.println("task2耗时:" + (end - start) + "毫秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Async
        public void task3(){
            try {
                long start = System.currentTimeMillis();
                //模拟程序执行耗时
                Thread.sleep(3000);
                long end = System.currentTimeMillis();
                System.out.println("task3耗时:" + (end - start) + "毫秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    @RestController
    public class AsyncController {
    
        @Autowired
        private AsyncService asyncService;
    
        @GetMapping("/doAsync")
        public Map<String, Object> doAsync(){
            long start = System.currentTimeMillis();
            //调用service中的三个任务方法
            //同步:第一个方法完成之后才能调用第二方法...耗时:三个方法时间相加
            //异步:三个方法仅调用,不再登录方法执行完毕,即可向下执行
            asyncService.task1();
            asyncService.task2();
            asyncService.task3();
    
            Map<String, Object> map = new HashMap<>();
            long end = System.currentTimeMillis();
            map.put("code", 200);
            map.put("message", "调用方法成功,总耗时为" + (end-start) + "毫秒");
            return map;
        }
    }
    

定时任务

  • 在启动类上开启定时任务

    //开启定时任务
    @EnableScheduling
    
  • service 定时任务注解

  • 定时表达式

    @Service
    public class JobService {
        /**
         * @Scheduled
         * 定时任务注解
         * cron配置项,为定时表达式
         * 秒 分 时 日 月 周 年(可选)
         * *星号:表示每,每秒、每分、每时...
         * ?问好:只能在日和周两个位置出现,排除冲突
         * -中划线:表示一个范围
         * ,逗号:表示一个列表值,比如在星期中使用1,2,4
         */
        // @Scheduled(cron = "* * * ? * 1-5")
        @Scheduled(cron = "0/5 * * ? * 1-5")
        public void myJob(){
            System.out.println("定时任务...");
        }
    }
    

邮件

  • io中的 java mail … 依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    
  • 开启邮箱pop3的服务,写配置文件

    spring:
      mail:
        #邮箱服务器地址     qq:smtp.qq.com    网易163:smtp.163.com
        host: smtp.qq.com
        #授权码,邮箱-》设置-》账户-》POP3/AMTP服务,开启服务后会获得授权码
        password: xxxxxceoibdaaaaa
        username: 11118422@qq.com
        default-encoding: UTF-8
    
  • 发送简单的内容

    @Autowired
    private JavaMailSender javaMailSender;
    
    /**
     * 发送基本的内容(纯文本)
     */
    @Test
    void testSend() {
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
        //发件人
        simpleMailMessage.setFrom("11112342@qq.com");
        //收件人
        simpleMailMessage.setTo("22212342@qq.com");
        //主题
        simpleMailMessage.setSubject("这是一个测试邮件20220812");
        //邮件内容
        simpleMailMessage.setText("测试内容2022-08-12");
        javaMailSender.send(simpleMailMessage);
    }
    
  • 发送邮件工具类

    • 添加文件
    • 添加附件
    /**
     * 测试发送复杂内容,例如图片和附件等
     */
    @Test
    void testSend2() throws MessagingException {
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        // 创建一个邮件工具,可以发送附件
        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage,true,"utf-8");
        mimeMessageHelper.setFrom("28718422@qq.com");
        mimeMessageHelper.setTo("28718422@qq.com");
        mimeMessage.setSubject("这是一个携带了图片和附件的邮件20220812");
        //拼接内容参数
        StringBuilder sb = new StringBuilder();
        sb.append("<html> <body> <h1 style='color:red'>springboot 测试邮件发送复杂格式o</h1>");
        sb.append("<p style='color:blue;font-size:16px'>哈哈哈</p>");
        sb.append("<p style='text-align:center'>居中</p>");
        sb.append("<img src='cid:picture'/> </body></html>");  //如果要插入图片src='cid:picture'
        //设置内容,可以被html解析
        mimeMessageHelper.setText(sb.toString(), true);
        // 从本地磁盘中读取到图片 站位到内容中去
        mimeMessageHelper.addInline("picture",new File("C:\\Users\\NINGMEI\\Desktop\\aaa\\ddd.jpg"));
        // 添加附件
        mimeMessageHelper.addAttachment("SpringBoot.doc",new File("D:\\course\\05-SpringBoot\\springboot\\document\\SpringBoot.doc"));
        javaMailSender.send(mimeMessage);
    }
    
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习尚硅谷视频整理的文档 Spring Boot 1 1 Spring Boot入门 4 1.1 简介 4 1.2 微服务(martin fowler发表了一篇文章) 5 1.3 环境约束 7 1.4 第一个Spring Boot项目(jar):HelloWorld 8 1.5 入门案例详解 11 1.5.1 POM文件 11 1.5.2 主程序类,主入口类 12 1.6 使用Spring Initializer向导快速创建Spring Boot 16 2 Spring Boot配置 18 2.1 配置文件 18 2.2 YML语法 19 2.3 YML配置文件值获取 21 2.4 properties配置文件乱码问题 24 2.5 @ConfigurationProperties与@Value的区别 25 2.6 配置@PropertySource、@ImportResource、@Bean 27 2.7 配置文件占位符 30 2.8 Profile多环境支持 31 2.9 配置文件的加载位置 33 2.10 外部配置加载顺序 36 2.11 自动配置原理 37 2.12 @Conditional派生注解 41 3 Spring Boot与日志 42 3.1 日志框架分类和选择 42 3.2 SLF4j使用 43 3.3 其他日志框架统一转换成slf4j+logback 44 3.4 Spring Boot日志使用 45 3.5 Spring Boot默认配置 47 3.6 指定日志文件和日志Profile功能 52 3.7 切换日志框架(不使用SLF4j+LogBack) 54 4 Spring Boot与Web开发 55 4.1 Web开发简介 55 4.2 静态资源映射规则 56 4.3 引入Thymeleaf 60 4.4 Thymeleaf语法 61 4.5 SpringMVC自动配置原理 67 4.6 SpringBoot扩展与全面接管 70 4.7 如何修改SpringBoot的默认配置 72 4.8 【实验】CRUD操作 73 4.8.1 默认访问首页 73 4.8.2 登录页面国际化 74 4.8.3 登录 80 4.8.4 拦截器进行登录检查 81 4.8.5 实验要求(没按要求做,不想改了!) 82 4.8.6 CRUD-员工列表 83 4.8.7 CRUD-员工修改 86 4.8.8 CRUD-员工添加 87 4.8.9 CRUD-员工删除 88 4.9 错误处理原理&错误页面定制 90 4.10 配置嵌入式Servlet容器(springboot 1.50版本) 97 4.10.1 如何定制和修改Servelt容器的相关配置 97 4.10.2 注册servlet三大组件【servlet,filter,listener】 98 4.10.3 替换为其他嵌入式容器 102 4.10.4 嵌入式servlet容器自动配置原理 103 4.10.5 嵌入式servlet容器启动原理 103 4.11 使用外置的Servlet容器 104 4.11.1 步骤 104 4.11.2 原理 107 5 Spring Boot与Docker(虚拟化容器技术) 110 5.1 简介 110 5.2 核心概念 111 5.3 安装Docker 112 5.4 Docker常用命令&操作 113 5.5 安装MySQL示例 114 6 Spring Boot与数据访问 115 6.1 JDBC 115 6.1.1 实现 115 6.1.2 自动配置原理 116 6.2 整合Durid数据源 117 6.3 整合Mybatis 122 6.3.1 注解版 123 6.3.2 配置文件版 124 6.4 整合SpringData JPA 125 6.4.1 SpringData简介 125 6.4.2 整合 126 7 Spring Boot启动配置原理 128 7.1 启动流程(Springboot 1.50版本) 128 7.1.1 创建SpringApplication对象 129 7.1.2 运行run方法 130 7.1.3 编写事件监听机制 132 8 Spring Boot自定义starters 136 8.1 概述 136 8.2 步骤 137 9 更多Springboot整合示例 144 10 Spring Boot与缓存 145 10.1 JSR107缓存规范 145 10.2 Spring的缓存抽象 146 10.2.1 基本概念 146 10.2.2 整合项目 146 10.2.3 CacheEnable注解 148 10.2.4 Cache注解 150 10.3 整合redis 154 10.3.1 在Docker上安装redis 154 10.3.2 Redis的Template 154 10.3.3 整合(百度) 155
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值