springBoot学习
https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/html/index.html (官方文档)

1.搭建springBoot项目架构
2.spring boot的Maven基础配置
3.父项目springboot的版本仲裁管理依赖的版本号

<!-- 父项目springboot的版本仲裁管理依赖的版本号,后续导入就不需要导入版本号了--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.1.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent>
进入spring-boot-starter-parent里边又有一个parent项目<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.2.1.RELEASE</version><relativePath>../../spring-boot-dependencies</relativePath></parent>

进入spring-boot-dependencies里面是我们后续需要用到的所有依赖的版本号,所以后续导入依赖不需要再导入版本号

  <spring-amqp.version>2.2.1.RELEASE</spring-amqp.version><spring-batch.version>4.2.0.RELEASE</spring-batch.version><spring-cloud-connectors.version>2.0.6.RELEASE</spring-cloud-connectors.version><spring-data-releasetrain.version>Moore-SR1</spring-data-releasetrain.version><spring-framework.version>5.2.1.RELEASE</spring-framework.version>

如果需要自定义版本号在标签中修改版本信息

<properties><java.version>1.8</java.version></properties>2.springboot场景启动器
<!-- springboot场景启动器,导入web模块的依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>

spring-boot-starter-web里面是嵌入式Tomcat,和一些web开发需要的依赖,除了spring-boot-starter-web,springboot还提供了一些其他的spring-boot-starter-redis等等场景启动器用来开发对应应用

3.maven将应用打包成jar文件的插件

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

点击package即可将应用打包为jar文件,然后在cmd中输入java -jar 的命令就可以运行应用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T06iyBLT-1587288707505)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1585809808912.png)]

4.文件处理器依赖

<!--  配置文件处理器,配置文件进行绑定就会有提示,让有些注解生效--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency>

2.SpringBoot启动类配置

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

2.1.@SpringBootApplication
@SpringBootApplication 注解表明该类是springBoot的主配置类,应运行该类的主方法启动SpringBoot应用

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

2.2@SpringBootConfiguration**
SpringBoot的配置类;

标注在一个类上,说明该类是一个springBoot的配置类;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
2.3**@Configuration**
配置类注解是spring的底层注解;

配置类也是一个组件;@Component

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
2.4@EnableAutoConfiguration
@EnableAutoConfiguration:开启自动配置功能;

以前需要配置的东西,现在springBoot帮我们自动配置,通过这个注解开启自动配置;

@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {

@AutoConfigurationPackage:自动配置包,将主配置类(@SpringBootApplication标注的类)的所在包下的所有子包内的所有组件全部加入到spring容器中;

@Import({AutoConfigurationImportSelector.class})

spring底层注解@import,给容器导入一个组件;导入的组件由AutoConfigurationImportSelector.class提供;

AutoConfigurationImportSelector:决定导入哪些组件;

将所有需要导入的组件以全类名的方式返回(String【】);这些组件就会被添加到容器中;

3springBoot配置文件
3.1配置文件
springBoot使用一个全局配置文件,配置文件名字是固定的;

application.properties;
application.yml
二者都可以被spring boot识别;

配置文件的作用:对springBoot的自动配置哪里不满意可以修改springBoot的自动配置;

3.2.YAML语法
1.格式
k:(空格)v:属性和值之间必须有空格;

1、大小写敏感
2、使用缩进表示层级关系
3、禁止使用tab缩进,只能使用空格键
4、缩进长度没有限制,只要元素对齐就表示这些元素属于一个层级。
5、使用#表示注释
6、字符串可以不用引号标注

server:
  port: 8081

2.值的写法**
字面量:数字,字符串,布尔

name:lisi

对象:Map:

age : 12

行内写法:

{age:12,name:huang}

list,数组 :

[a,b,c]

或者

ch:
 - a
 - b
 - c

对象里面嵌套对象:

Userpo:
  age: 15
  name: zhangsan
  son:
     age: 1
     name: lisi

list嵌套list:

lang:
 -
  - Ruby
  - Perl
  - Python 
 - 
  - c
  - c++
  - java

list嵌套map

man:
 -
  id: 1
  name: huang
 -
  id: 2
  name: liao

3.配置文件值注入
//只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能
//@ConfigurationProperties告诉springboot将本类中的所有属性和配置文件中相关的配置进行绑定
//prefix表示与配置文件中哪个下面的所有属性进行映射
@Component
//prefix里面的配置只能小写
@ConfigurationProperties(prefix ="userpo")

public class UserPO {
    private  int age;
    private String name;
    private Son son;

<!--  配置文件处理器,配置文件进行绑定就会有提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
           <optional>true</optional>
        </dependency>

3.3 application.properties的配置方式
Userpo.age=12
userpo.name=lisi
userpo.son.name=wangwu
Userpo.son.age=1

3.4@PropertySource和@importResource
@PropertySource:避免application.properties的配置太多可以新建配置文件来进行值的注入,value是字符串数组可以同时支持多个配置文件用逗号隔开;

@PropertySource("classpath:user.properties")
@Component
public class UserPO {

@importResource:导入spring的配置文件.让配置文件生效;

我们编写的spring配置文件需要导入才能生效;

@importResource:标识在配置类上location也是一个字符串数组

@ImportResource(locations = {"classpath:beans.xml"})
@SpringBootApplication
public class DemoApplication {

spring配置文件beans.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">

<bean id="halloService" class="com.example.service.Service"></bean>
</beans>

springBoot推荐的给容器添加组件的方式**;推荐使用全注解的方式**(使用配置类)

//表明这是配置类,springboot会扫描这个类,将它的组件加入容器
@Configuration
public class MyAppConfig {
    //将方法的返回值加入到容器中,容器中的组件名默认为方法名
    @Bean
    public Service helloService() {
        System.out.println("加入成功");
        return new Service();
    }

3.5配置文件占位符
Userpo.age=12
userpo.name=lisi${random.uuid}
userpo.son.name=${userpo.name}123
Userpo.son.age=${Userpo.age:2}

结果:

Userpo.age已经存在时Userpo.son.age中定义的Userpo.age:2权限小于Userpo.age=12

userpo:UserPO{age=12, name='lisi09257e3d-2856-4491-b715-b40096a74fef', son=Son{age=12, name='lisi70573384-1584-4bd7-b11f-8ea828272546123'}}

3.6 profile
profile为spring对不同环境提供不同功能的支持;

1.使用application.{profile}.properties多文件配置

如:application.properties

​ application.dev.properties(开发环境)

​ application.prod.properties(生产环境)

默认会采用application.properties,如果需要采用其他环境则需要在application.properties中

spring.profiles.active=dev

激活开发环境

2.使用application.yml可以实现单文件多环境切换

server:
  port: 8080
spring:
  profiles:
    active: dev
---
server:
  port: 8082
spring:
    profiles: dev

---
server:
  port: 8081

spring:
  profiles: prod

YAML中—表示文档分块只需要使用active激活所需环境即可;

3.7配置文件加载顺序
1.文件根目录的config文件夹下的配置文件

2.文件根目录下的配置文件

3.classpath下config文件夹下的配置文件(IDE中classpath就是resource);

4.classpath(类路径)下的配置文件;

原则:四个位置的配置文件都会被加载(互补配置),且高优先级会覆盖低优先级的文件配置;

spring.config.location指定自定义的配置文件,也遵循上述互补配置原则多用于运维

spring.config.location=D:/application.properties

1
2
当项目发布了,而又想新加配置则使用命令行操作就可以新加配置

java -jar springbootxxxxx.jar --spring.config.location=D:/application.properties

3.8常见外部配置文件加载顺序
1.命令行参数

用空格隔开多个参数;

java -jar springbootxxxxx.jar --spring.config.location=D:/application.properties

#后台运行
nohup java -jar springbootxxxxx.jar --spring.config.location=D:/application.properties   &

高版本springboot中–spring.config.location 不遵循互补原则,不会加载jar包内的application配置文件,需要使用–spring.config.additional-location代替

nohup java -jar springbootxxxxx.jar --spring.config.additional-location=D:/application.properties   &
1
优先加载带profile,同等条件下jar包外部优先级高于内部配置文件;

2.jar包外部的application.{profile}.properties或application.yaml(带spring.profile)配置文件

springBoot进行web开发
4.模板引擎thymeleaf
4.1).依赖导入
<dependency> 
<groupId>org.springframework.boot</groupId> 
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

4.2). Thymeleaf相关配置**
Thymeleaf 中已经有默认的配置了,我们不需要再对其做过多的配置,有一个需要注意一下,

Thymeleaf 默认是开启页面缓存的,所以在开发的时候,需要关闭这个页面缓存,配置如下。

spring.thymeleaf.cache= false #关闭缓存

4.3).thymeleaf的命名空间(加入有语法提示)**
<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
    <html lang="en">

4.4)使用案例**
后端

Spring Boot 中会自动识别模板目录(templates/)下的success.html

@Controller
public class HelloController {

@RequestMapping("/success")
    public String success(Map<String,String>map) {
        map.put("hello","你好");
        return "success";
    }

前端

<div th:text="${hello}"></div>

1
2
浏览器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PfOeug0k-1587288707511)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1585971577323.png)]

注意使用thymeleaf不能在controller层使用@RestController注解,方法上也不能使用@RespondBody注解,该注解会以json格式将数据还回,这样模板引擎就不能进行解析,结果就会将success直接打印到浏览器;

5.日志系统
在开发中,我们经常使用 System.out.println() 来打印一些信息,但是这样不好,因为大量的使用 System.out 会增加资源的消耗。我们实际项目中使用的是 slf4j 的 logback 来输出日志,效率挺高 的,Spring Boot 提供了一套日志系统,logback 是最优的选择。

springboot项目的最佳实践就是采用slf4j+logback;

引用百度百科里的一段话:

SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只 服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许 最终用户在部署其应用时使用其所希望的日志系统。

这段的大概意思是:你只需要按统一的方式写记录日志的代码,而无需关心日志是通过哪个日志系统,

**以什么风格输出的。因为它们取决于部署项目时绑定的日志系统。**例如,在项目中使用了 slf4j 记录日

志,并且绑定了 log4j(即导入相应的依赖),则日志会以 log4j 的风格输出;后期需要改为以 logback

的风格输出日志,只需要将 log4j 替换成 logback 即可,不用修改项目中的代码。这对于第三方组件的

引入的不同日志系统来说几乎零学习成本,况且它的优点不仅仅这一个而已,还有简洁的占位符的使用

和日志级别的判断。

5.1 yaml配置**
logging.config 是用来指定项目启动的时候,读取哪个配置文件,这里指定的是日志配置文件是根 路径下的 logback.xml 文件,关于日志的相关配置信息,都放在 logback.xml 文件中了。 logging.level 是用来指定具体的 mapper 中日志的输出级别,上面的配置表示 com.yg.example.dao 包下的所有 mapper 日志输出级别为 trace,会将操作数据库的 sql 打 印出来,开发时设置成 trace 方便定位问题,在生产环境上,将这个日志级别再设置成 error 级别即可

#日志配置
logging:
  config: classpath:config/logback.xml
  level:
    com.yg.example.dao: trace

5.2 logback.xml里的配置(开发环境)
<configuration>
    <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
    <property name="FILE_PATH" value="D:/logs/ssoService/ssoService.%d{yyyy-MM- dd}.%i.log"/>
    <property name="ERROR_FILE_PATH" value="D:/logs/ssoService/error/ssoServiceError.%d{yyyy-MM-dd}.%i.log"/>

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder> <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
        </appender>

<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">

<filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>ERROR</level>
                <onMatch>DENY</onMatch>  <!-- 如果命中就禁止这条日志 -->
                <onMismatch>ACCEPT</onMismatch>  <!-- 如果没有命中就使用这条规则 -->
            </filter>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 按照上面配置的FILE_PATH路径来保存日志 -->
                <fileNamePattern>${FILE_PATH}</fileNamePattern> <!-- 日志保存15天 -->
                <maxHistory>15</maxHistory>
                <timeBasedFileNamingAndTriggeringPolicy
                        class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- 单个日志文件的最大,超过则新建日志文件存储 -->
                    <maxFileSize>10MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <encoder> <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
        </appender>

<!-- error日志 -->
        <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 过滤日志 -->
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>ERROR</level>
            </filter>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 按照上面配置的FILE_PATH路径来保存日志 -->
                <fileNamePattern>${ERROR_FILE_PATH}</fileNamePattern> <!-- 日志保存15天 -->
                <maxHistory>14</maxHistory>
                <timeBasedFileNamingAndTriggeringPolicy
                        class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- 单个日志文件的最大,超过则新建日志文件存储 -->
                    <maxFileSize>10MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <encoder> <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
        </appender>

<root level="INFO">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="INFO"/>
            <appender-ref ref="ERROR"/>
        </root>
    </configuration>

5.23logback-pro.xml里的配置(生产环境)
<configuration>
    <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
    <property name="FILE_PATH" value="/data/logs/ssoService/ssoService.%d{yyyy-MM- dd}.%i.log"/>
    <property name="ERROR_FILE_PATH" value="/data/logs/ssoService/error/ssoServiceError.%d{yyyy-MM-dd}.%i.log"/>

<!--正式环境-->
    <!--<property name="FILE_PATH" value="/data/logs/draw.%d{yyyy-MM- dd}.%i.log"/>-->

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder> <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">

<filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>  <!-- 如果命中就禁止这条日志 -->
            <onMismatch>ACCEPT</onMismatch>  <!-- 如果没有命中就使用这条规则 -->
        </filter>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 按照上面配置的FILE_PATH路径来保存日志 -->
            <fileNamePattern>${FILE_PATH}</fileNamePattern> <!-- 日志保存15天 -->
            <maxHistory>15</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- 单个日志文件的最大,超过则新建日志文件存储 -->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder> <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

<!-- error日志 -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 按照上面配置的FILE_PATH路径来保存日志 -->
            <fileNamePattern>${ERROR_FILE_PATH}</fileNamePattern> <!-- 日志保存15天 -->
            <maxHistory>14</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- 单个日志文件的最大,超过则新建日志文件存储 -->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder> <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

<root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="ERROR"/>
    </root>
</configuration>

测试代码:

private final static Logger logger = LoggerFactory.getLogger(HelloController.class);

@RequestMapping("logger")
    //测试日志
    public String testLogger() {
        logger.debug("debug测试");
        logger.info("info 测试");
        logger.error("error 测试");
        logger.warn("warn测试");
        //占位符
        String s1="hahaha";
        String s2="hehehe";
        logger.info("我{};你{}", s1, s2);
    return "success";
    }

打印参数必须使用{};

6.Spring Boot中的项目属性配置
1.少量配置
在微服务架构中,最常见的就是某个服务需要调用其他服务来获取其提供的相关信息,那么 在该服务的配置文件中需要配置被调用的服务地址,比如在当前服务里,我们需要调用订单微服务获取 订单相关的信息,假设 订单服务的端口号是 8002,那我们可以做如下配置:

# 配置微服务的地址 
url:
# 订单微服务的地址 
  orderUrl: http://localhost:8002

@Value("${url.orderUrl}")
    private  String orderUrl;
    @RequestMapping("/config")
    public String testUrl() {
        logger.info("orderUrl:{}",orderUrl);
        return "success";
    }

打印结果:

orderUrl:http://localhost:8002

2.多个配置
随着业务复杂度的增加,一个项目中可能会有越来越多的微服务,某个模块可能 需要调用多个微服务获取不同的信息,那么就需要在配置文件中配置多个微服务的地址。可是,在需要 调用这些微服务的代码中,如果这样一个个去使用 @Value 注解引入相应的微服务地址的话,太过于繁 琐,也不科学。

所以,在实际项目中,业务繁琐,逻辑复杂的情况下,需要考虑封装一个或多个配置类。举个例子:假 如在当前服务中,某个业务需要同时调用订单微服务、用户微服务和购物车微服务,分别获取订单、用 户和购物车相关信息,然后对这些信息做一定的逻辑处理。那么在配置文件中,我们需要将这些微服务 的地址都配置好:

例子:
yaml配置:

# 配置多个微服务的地址
url:
# 订单微服务的地址
  orderUrl: http://localhost:8002
  # 用户微服务的地址
  userUrl: http://localhost:8003
  # 购物车微服务的地址
  shoppingUrl: http://localhost:8004

配置类:

省去get,set方法;

@Component
@ConfigurationProperties(prefix = "url")
public class MicroServiceUrl {
    private String orderUrl;
    private String userUrl;
    private String shoppingUrl;

测试类:

@Resource
private MicroServiceUrl microServiceUrl;

@RequestMapping("urls")
    public String testConfig() {
        logger.info("OrderUrl{}",microServiceUrl.getOrderUrl());
        logger.info("userUrl{}",microServiceUrl.getUserUrl());
        logger.info("shoppingUrl{}",microServiceUrl.getShoppingUrl());

return "success";
    }

打印结果:

OrderUrlhttp://localhost:8002
userUrlhttp://localhost:8003
shoppingUrlhttp://localhost:8004

使用@ConfigurationProperties 注解并且使用 prefifix 来指定一个前缀, 然后该类中的属性名就是配置中去掉前缀后的名字,一一对应即可。即:前缀名 + 属性名就是配置文件 中定义的 key。同时,该类上面需@Component 注解,把该类作为组件放到Spring容器中,让 Spring 去管理,我们使用的时候直接注入即可。

@Resource(这个注解属于J2EE的),默认安照名称进行装配,名称可以通过name属性进行指定,
如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。 当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。

推荐使用:@Resource注解在字段上,且这个注解是属于J2EE的,减少了与spring的耦合。最重要的这样代码看起就比较优雅。

7.集成 Swagger
7.1Swagger简介
​ API构建工具

优点:

​ 对使用接口的人 来说,开发人员不需要给他们提供文档,只要告诉他们一个 Swagger 地址,即可展示在线的 API 接口 文档,除此之外,调用接口的人员还可以在线测试接口数据,同样地,开发人员在开发接口时,同样可以利用 Swagger 在线接口文档测试接口数据,这给开发人员提供了便利。

7.2 Swagger的使用:
7.2.1 maven导入依赖
springboot项目整合swagger要注意两者的版本,springboot项目的版本低,相应的swagger版本不能太高,反之亦然,避免项目报如下错误。

Parameter 0 of method linkDiscoverers in org.springframework.hateoas.config.HateoasConfiguration required a single bean, but 15 were found:

<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>

2.Swagger的配置类
@EnableSwagger2是swagger提供的注解;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                // 指定构建api文档的详细信息的方法:apiInfo()
                .apiInfo(apiInfo()).select()
                // 指定要生成api接口的包路径,这里把controller作为包路径,生成controller 中的所有接口
                .apis(RequestHandlerSelectors.basePackage("com.example.controller"))
                .paths(PathSelectors.any())
                .build();
    }

/*** 构建api文档的详细信息 * @return */
    private ApiInfo apiInfo()
    { return new ApiInfoBuilder()
        // 设置页面标题
         .title("Spring Boot集成Swagger2接口总览")
        // 设置接口描述
        .description("Swagger2测试")
        // 设置联系方式
         .contact("hello word")
        // 设置版本
         .version("2.0")
        // 构建
         .build();
    }
}

3.对实体类进行添加注解
@ApiModel 注解用于实体类,表示对类进行说明,用于参数用实体类接收。

@ApiModelProperty 注解用于类中属性,表示对 model 属性的说明或者数据操作更改。

@Component
@ConfigurationProperties(prefix = "url")
@ApiModel(value = "地址实体类")
public class MicroServiceUrl {
    @ApiModelProperty(value = "订单服务链接")
    private String orderUrl;
    @ApiModelProperty(value = "用户服务链接")
    private String userUrl;
    @ApiModelProperty(value = "购物服务链接")
    private String shoppingUrl;

4.对controller类添加注解
@Api 注解用于类上,表示标识这个类是 swagger 的资源。

@ApiOperation 注解用于方法,表示一个 http 请求的操作。

@ApiParam 注解用于参数上,用来标明参数信息。

@Controller
@RequestMapping("/test")
@Api(value = "Swagger2 在线接口文档")
public class HelloController {
@RequestMapping("urls")
    @ApiOperation(value = "打印服务地址")
    public String testConfig() {
        logger.info("OrderUrl{}",microServiceUrl.getOrderUrl());
        logger.info("userUrl{}",microServiceUrl.getUserUrl());
        logger.info("shoppingUrl{}",microServiceUrl.getShoppingUrl());

return "success";
    }

在浏览器中输入 localhost:8080/swagger-ui.html 看一下 Swagger 页面的接口状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JEA7A1Bg-1587288707513)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1586159908009.png)]

8.springBoot对json格式的处理
8.1jackson对json格式进行去null值处理**
在实际项目中,我们难免会遇到一些 null 值出现,我们转 json 时,是不希望有这些 null 出现的,比如 我们期望所有的 null 在转 json 时都变成 “” 这种空字符串

1.新建一个json配置类

@Configuration
public class JsonConfig {
    @Bean
    @Primary
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
            @Override
            public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                jsonGenerator.writeString("");
            }
        });
        return objectMapper;
    }

测试类

@RestController
@RequestMapping("/json")
public class JsonController {

@RequestMapping("/testJson")
    public Map testJson() {
        Map<Integer, UserPO> map=new HashMap<Integer,UserPO>();
        UserPO userPO = new UserPO();
        userPO.setName("lisi");
        userPO.setAge(15);
        map.put(1,userPO);
        return map;

}

结果

{"1":{"age":15,"name":"lisi","son":""}}

1
2
8.2定义统一的 json 结构
省去set,get方法;

public class JsonUtil<T> {
        private T data;
        private String code;
        private String msg;

/*** 若没有数据返回,默认状态码为0,提示信息为:操作成功! */
        public JsonUtil() {
            this.code = "0";
            this.msg = "操作成功!";
        }

/*** 若没有数据返回,可以人为指定状态码和提示信息 * @param code * @param msg */
        public JsonUtil(String code, String msg) {
            this.code = code;
            this.msg = msg;
        }

/*** 有数据返回时,状态码为0,默认提示信息为:操作成功! * @param data */
        public JsonUtil(T data) {
            this.data = data;
            this.code = "0";
            this.msg = "操作成功!";
        }

/*** 有数据返回,状态码为0,人为指定提示信息 * @param data * @param msg */
        public JsonUtil(T data, String msg) {
            this.data = data;
            this.code = "0";
            this.msg = msg;
        }

测试类

@RestController
@RequestMapping("/json")
public class JsonController {
    @RequestMapping("jsonresult")
    public JsonUtil<Map> jsonresult() {
        Map<Integer, UserPO> map=new HashMap<Integer,UserPO>();
        UserPO userPO = new UserPO();
        userPO.setName("lisi");
        userPO.setAge(15);
        map.put(1,userPO);
        return new JsonUtil<Map>(map);
    }
}

结果

{"data":{"1":{"age":15,"name":"lisi","son":""}},"code":"0","msg":"操作成功!"}

1
2
9.Spring Boot中的全局异常处理
9.1.用JsonUtil作为统一json还回格式
public class JsonUtil<T> {
        private T data;
        private String code;
        private String msg;

/*** 若没有数据返回,默认状态码为0,提示信息为:操作成功! */
        public JsonUtil() {
            this.code = "0";
            this.msg = "操作成功!";
        }

/*** 若没有数据返回,可以人为指定状态码和提示信息 * @param code * @param msg */
        public JsonUtil(String code, String msg) {
            this.code = code;
            this.msg = msg;
        }

/*** 有数据返回时,状态码为0,默认提示信息为:操作成功! * @param data */
        public JsonUtil(T data) {
            this.data = data;
            this.code = "0";
            this.msg = "操作成功!";
        }

/*** 有数据返回,状态码为0,人为指定提示信息 * @param data * @param msg */
        public JsonUtil(T data, String msg) {
            this.data = data;
            this.code = "0";
            this.msg = msg;
        }

9.2.定义全局异常配置类**
@ControllerAdvice 注解包含了 @Component 注 解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。除此之外,该注解还有个 basePackages 属性,该属性是用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项 目工程中的所有异常。 @ResponseBody 注解是为了异常处理完之后给调用方输出一个 json 格式的封装 数据。

@ExceptionHandler 注解来指定具体的 异常,然后在方法中处理该异常信息,最后将结果通过统一的 json 结构体返回给调用者。

@ResponseStatus 指定异常代码默认INTERNAL_SERVER_ERROR(500, “Internal Server Error”),

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

//空指针异常处理
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonUtil handerNullPointerException(NullPointerException ex) {
        logger.error("空指针异常:{}", ex.getMessage());
        return new JsonUtil("500","空指针异常");
    }
}

测试类

@RestController
@RequestMapping("/hander")
public class ExceptionController {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@RequestMapping("/testNullPointException")
    public JsonUtil testNullPointException() {
        String str=null;
        logger.info("长度:{}", str.length());
        return new JsonUtil();

}
}

结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t4BSNh6c-1587288707515)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1586230459516.png)]

1).对Exception进行统一拦截
由于 Exception 异常是父类,所有异常都会继承该异常,所以我们可以直接拦截 Exception 异常,一劳永 逸

1.全局异常处理类

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

//异常统一处理
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonUtil handerException(Exception ex) {
        logger.error("异常:{}", ex.getMessage());
        return new JsonUtil("500", "服务器异常");
    }
}

如果同时配置了 @ExceptionHandler(Exception.class)和@ExceptionHandler(NullPointerException.class)时如果发生空指针异常会进入@ExceptionHandler(NullPointerException.class)配置的方法,也就是说如果对Exception 的异常处理和对具体异常的处理同时存在是,具体异常发生时优先采用具体异常处理方法;

全局异常处理的最佳实践:

配置最常出现的具体异常(如@ExceptionHandler(NullPointerException.class))然后在配置一个@ExceptionHandler(Exception.class)异常处理方法进行补充;

2.) 拦截自定义异常
在实际项目中,除了拦截一些系统异常外,在某些业务上,我们需要自定义一些业务异常,比如在微服

务中,服务之间的相互调用很平凡,很常见。要处理一个服务的调用时,那么可能会调用失败或者调用

超时等等,此时我们需要自定义一个异常,当调用失败时抛出该异常,给 GlobalExceptionHandler 去

捕获

1.自定义异常信息的枚举类

PARMETER_EXCEPTION(“102”, “参数异常!”), SERVICE_TIME_OUT(“103”, “服务调用超时!”)它们都相当于是构建了一个枚举类的实例 例如:PARMETER_EXCEPTION.getCode()就是12,PARMETER_EXCEPTION.getMessage()就是"参数异常!";

public enum BusinessMsgEnum {
    /**
     * 参数异常
     */
    PARMETER_EXCEPTION("102", "参数异常!"),

/**
     * 等待超时
     */
    SERVICE_TIME_OUT("103", "服务调用超时!"),
    /**
     * 参数过大
     */
    PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),

/**
     * 500 : 一劳永逸的提示也可以在这定义
     */
    UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");
    // 还可以定义更多的业务异常
    /*** 消息码 */
    private String code;
    /*** 消息内容 */
    private String msg;

private BusinessMsgEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

//省略get,set方法
}

2.构建异常处理类

当出现业务异常时,我们就抛这个自定义的业务异常即可。

在构造方法中,传入我们上面自定义的异常枚举类,所以在项目中,如果有新的异常信息需要添加,我 们直接在枚举类中添加即可,很方便,做到统一维护,然后再拦截该异常时获取即可。

public class BusinessErrorException extends RuntimeException {
    private static final long serialVersionUID = -7480022450501760611L;
    /*** 异常码 */
    private String code;
    /*** 异常提示信息 */
    private String message;

public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
        this.code = businessMsgEnum.getCode();
        this.message = businessMsgEnum.getMsg();
    }
//省去Set,get方法
}

3.拦截业务异常

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(BusinessErrorException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonUtil handleBusinessError(BusinessErrorException ex) {
        String code = ex.getCode();
        String message = ex.getMessage();
        return new JsonUtil(code, message);
    }
    }

4.测试

捕获异常,抛出我们自定义的异常类,然后被 GlobalExceptionHandler 拦截进行处理;

@RestController
@RequestMapping("/hander")
public class ExceptionController {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@RequestMapping("/business")
   public JsonUtil business() {
       try {
           int i = 1 / 0;
       } catch (Exception e) {
           throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
       }
       return new JsonUtil();
   }
   }

结果

{
    "data": "",
    "code": "500",
    "msg": "系统发生异常,请联系管理员!"
}

spring boot之的全局异常优雅封装实践
链接: https://blog.csdn.net/ailiandeziwei/article/details/105416995

10.Spring Boot中的切面AOP处理
10.1** 什么是AOP
​ AOP:Aspect Oriented Programming 的缩写,意为:面向切面编程。面向切面编程的目标就是分离 关注点。

核心思想: 就是你只需要关心你需要关心的核心业务的实现,而像日志,权限管理等 系统功能会在程序运行时织入进去;

10.2 Spring Boot 中的 AOP 处理
1)导入AOP依赖

<!--   AOP的支持-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2) 实现 AOP 切面

Spring Boot 中使用 AOP 非常简单,假如我们要在项目中打印一些 log,在引入了上面的依赖之后,我

们新建一个类 LogAspectHandler,用来定义切面和处理方法。只要在类上加个 @Aspect 注解即可。

@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。 @Component 注解让该类

交给 Spring 来管理。

这里主要介绍几个常用的注解及使用:

@Pointcut:定义一个切面,即上面所描述的关注的某件事入口。

@Before:在做某件事之前做的事。

@After:在做某件事之后做的事。

@AfterReturning:在做某件事之后,对其返回值做增强处理。

@AfterThrowing:在做某件事抛出异常时,处理。

@Pointcut 注解

@Pointcut 注解:用来定义一个切面(切入点),即上文中所关注的某件事情的入口。切入点决定了

连接点关注的内容,使得我们可以控制通知什么时候执行。

@Pointcut 注解指定一个切面,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution() ,另一个是使用 annotation() 。

以 @Pointcut(“execution(* com.example.controller….(…))”)表达式为例,语法如下:

execution() 为表达式主体

第一个 * 号的位置:表示返回值类型, * 表示所有类型

包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,

com.example.controller包、子包下所有类的方法

第二个 * 号的位置:表示类名, * 表示所有类

*(…) :这个星号表示方法名, * 表示所有的方法,后面括弧里面表示方法的参数,两个句点

表示任何参数

annotation() 方式是针对某个注解来定义切面,比如我们对具有 @GetMapping 注解的方法做切面,

可以如下定义切面:

@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") public void annotationCut() {}

1
2
然后使用该切面的话,就会切入注解是 @GetMapping 的方法。因为在实际项目中,可能对于不同的注

解有不同的逻辑处理,比如 @GetMapping 、 @PostMapping 、 @DeleteMapping 等。所以这种按照注

解的切入方式在实际项目中也很常用。

@Before 注解

前置通知(Before advice):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)

@Aspect
@Component
public class LogAspectHandler {
    private final Logger logger = LoggerFactory.getLogger(LogAspectHandler.class);

/*** 定义一个切面,拦截com.example.controller包和子包下的所有方法 */
    @Pointcut("execution(* com.example.controller..*.*(..))")
    public void pointcut() {
    }

@Before("pointcut()")
    public void testAfter(JoinPoint joinPoint) {
        logger.info("testAfter方法进入!");
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        String declaringTypeName = signature.getDeclaringTypeName();
        logger.info("包名为:{},方法名为:{}", declaringTypeName, methodName);
         // 也可以用来记录一些信息,比如获取请求的url和ip
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //获取URL
        String requestURL = request.getRequestURL().toString();
        //获取ip地址
        String remoteAddr = request.getRemoteAddr();
        logger.info("请求的url地址为:{},ip地址为:{}",requestURL,remoteAddr);
    }

}

JointPoint 对象很有用,可以用它来获取一个签名,然后利用签名可以获取请求的包名、方法名,包括

参数(通过 joinPoint.getArgs() 获取)等等。

测试结果

11:45:13.439 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - testAfter方法进入!
11:45:13.441 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - 包名为:com.example.controller.AspectController,方法名为:testBefore
11:45:13.442 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - 请求的url地址为:http://localhost:8080/aop/before,ip地址为:0:0:0:0:0:0:0:1

@Afert 注解

后通知(After (finally) advice):当某连接点退出的时候执行的通知(一定执行)。

@After("pointcut()")
    public void testAfter(JoinPoint joinPoint) {
        logger.info("after方法进入");
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        logger.info("方法{}执行完成", name);

}

结果省略最后一次性展示

@AfterReturning 注解

返回后通知(After returning advice):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

@AfterReturning(value = "pointcut()", returning = "result")
    public void testAfterReturning(JoinPoint joinPoint, Object result) {
        logger.info("afterReturning方法进入,返回参数为:{}", result);
        logger.info("增强后的返回值为:{}", result + "增强");
    }

需要注意的是:在 @AfterReturning 注解 中,属性 returning 的值必须要和参数保持一致,否则会

检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回

值进行增强,可以根据业务需要做相应的封装。

@AfterThrowing 注解

抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。 (它和返回后通知总是只执行一个)

@AfterThrowing(value = "pointcut()", throwing = "ex")
    public void testAfterThrowing(JoinPoint joinPoint, Throwable ex) {
        logger.info("testAfterThrowing方法进入");
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        logger.info("方法{}执行出错,异常为:{}", name,ex.getMessage());
    }

上述配置好后的测试结果

12:12:56.589 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - testAfter方法进入!
12:12:56.590 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - 包名为:com.example.controller.AspectController,方法名为:testAop
12:12:56.591 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - 请求的url地址为:http://localhost:8080/aop/test,ip地址为:0:0:0:0:0:0:0:1
12:12:56.595 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - after方法进入
12:12:56.596 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - 方法testAop执行完成
12:12:56.596 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - testAfterThrowing方法进入
12:12:56.596 [http-nio-8080-exec-1] INFO  c.e.h.l.LogAspectHandler - 方法testAop执行出错,异常为:/ by zero

从上述结果看注解的执行顺序为 @Before -> @After -> @AfterReturning/ @AfterThrowing(这两个注解的方法任何时候只有一个生效,无异常则@AfterReturning生效,有异常则@AfterThrowing生效)

11.springboot整合mybatis
11.1)导入依赖
尤其注意mysql依赖的版本,如果不设置可能由于你自己mysql安装版本太低而无法适应springboot默认配置的高版本,会使得数据库连接失败;

<!-- 对mybatis的支持 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

<dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
            <version>5.1.24</version>
        <scope>runtime</scope>
    </dependency>

11.2)yaml配置
配置完要检查属性是否配置成功,否则后序出问题很难发现;

datasource:
  url: localhost:3306/movies

spring:
  #关闭thymeleaf的缓存
  thymeleaf:
    cache: false

#  整合mybatis
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects =10
    username: root
    password: root
    hikari:
      maximum-pool-size: 10 # 最大连接池数
      max-lifetime: 1770000

mybatis:
  mapper-locations: classpath:mapping/*Mapper.xml  #xml配置路径
  # 设定别名
  type-aliases-package: com.example.po
  configuration:
    map-underscore-to-camel-case: true #驼峰法命名

11.3)xxmapper.java 和xxmapper.xml文件的配置
xxmapper.java 和xxmapper.xml文件的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ycbJaIDx-1587288707517)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1586400537122.png)]

xxmapper.java配置

public interface FilmInfoMapper {
    public FilminfoPO findById(Integer id);
    public int updateById(int id);

}

xxmapper.xml配置

namespace(命名空间)的作用:

1.隔离sql语句,同一个命名空间的sql彼此可见,不同的命名空间彼此不可见,也就是说同一命名空间不能有相同id的sql语句,不同命名空间可以有;

2.通过命名空间可以找到与之对应的xxmapper接口;

<?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.example.mapper.FilmInfoMapper">

<!--   public FilminfoPO findById(Integer id);-->
    <select id="findById" resultType="com.example.po.FilminfoPO" parameterType="Integer">
        select  *from tb_filminfo where filmid=#{id}
    </select>

<!--     public int updateById(int id);-->
    <update id="updateById">
       update tb_filminfo  set ticketprice=20 where filmid=#{id}
    </update>
</mapper>

11.4)启动类加扫描mapper接口的注解
如果不加@MapperScan注解,那么每个mapper接口都得加@Mapper注解否则Spring Boot 找不到 Mapper

@SpringBootApplication
@MapperScan("com.example.mapper")

public class DemoApplication {

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
class FilmInfoTest {
    @Autowired
    FilmInfoMapper mapper;

@Test
    public void findByIdTest() {
        Integer i=1;
        System.out.println("FilmInfoPO:"+mapper.findById(i));
    }

12.Spring Boot事务配置管理
场景:我们在开发企业应用时,由于数据操作在顺序执行的过程中,线上可能有各种无法预知的问题,

任何一步操作都有可能发生异常,异常则会导致后续的操作无法完成。此时由于业务逻辑并未正确的完

成,所以在之前操作过数据库的动作并不可靠,需要在这种情况下进行数据的回滚。

12.1导入依赖
springboot的事务管理需要导入spring-boot-starter-jdbc;而我们导入的mybatis-spring-boot-starter包含了它,所以无需重复导入;

<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

12.2.事务测试
1)mapper接口

@Insert("insert into tb_filminfo (typeid,filmname,ticketprice) values(#{typeid},#{filmname},#{ticketprice})")
    int insert(FilminfoPO po);

2)service接口

public interface IFilmInfoService {
    //插入一条记录
    public int insert(FilminfoPO po);

3)service实现类

@Service
public class FilmInfoServiceImpl implements IFilmInfoService {
   @Resource
    private FilmInfoMapper mapper;

@Transactional
    @Override
    public int insert(FilminfoPO po) {

return mapper.insert(po);
    }

4)controller类

@Controller
@RequestMapping("/film")
public class FilmInfoController {

@Resource
    IFilmInfoService service;

@RequestMapping("/insert")
    public String insert(FilminfoPO po) {
       if (po!=null) {
            int i = service.insert(po);
            return "success";
        } else {
            return "false";
        }
    }
}

当没有异常抛出时添加成功,有异常出现添加失败;

12.3事务处理的一些特殊情况
1)异常并没有被捕获到

异常并没有被 ”捕获“ 到,导致事务并没有回滚。

Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序 错误(Error)才会回滚。但是抛出 SQLException 就无法回滚了。

@Transactional
    @Override
    public void insert(FilminfoPO po) throws SQLException {
        // 手动抛出异常
        mapper.insert(po);
        throw new SQLException("数据库异常");

虽然抛出异常但是数据插入成功;

解决方案:针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional 注解中使用 rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor = Exception.class) ,这样就没有问题了,所以在实际项目中,一定要指定异常。

@Transactional(rollbackFor = Exception.class)
    @Override
    public void insert(FilminfoPO po) throws SQLException {
        // 手动抛出异常
        mapper.insert(po);
        throw new SQLException("数据库异常");
    }

这样就插入失败了;

2)异常在方法中被捕获导致事务回滚失败

我们在处理异常时,有两种方式, 要么抛出去,让上一层来捕获处理;要么把异常 try catch 掉,在异常出现的地方给处理掉。就因为有 这中 try…catch,所以导致异常被 ”吃“ 掉,事务无法回滚。

@Transactional
    @Override
    public int insert(FilminfoPO po) {
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            e.getMessage();
        }
        return mapper.insert(po);
    }

记录成功被插入;

解决方法:直接往上抛,给上一层来处理即可

3)事务的范围 冲突导致回滚失败

许多业务需要在高并发的情况下保证数据唯一性所比要加synchronized关键字如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库 中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一 用户信息,不会出现同一数据库中插入了两条相同用户的信息。

@Transactional(rollbackFor = Exception.class)
    @Override
    public synchronized void insert(FilminfoPO po) throws SQLException {
        // 手动抛出异常
        mapper.insert(po);
     
    }

但是在压测时,数据库中确实可能有两条同一用户的信息,分析其原因,在于事务的

范围和锁的范围问题。

在执行该方法开始时,事务启动,执行 完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。 也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来 了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状 态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插 入动作,导致了脏数据。

解决方案:

​ 1.把事务去掉即可(不推荐);

​ 2. 在调用该 service 的地方加锁,保证锁 的范围比事务的范围大即可。

13.Spring Boot中使用拦截器
13.1)应用场景:
拦截器是 AOP 的一种实现,专门拦截对动态资源的后台请求,即拦截对控制层的请 求。使用场景比较多的是判断用户是否有权限请求后台,更拔高一层的使用场景也有,比如拦截器可以 结合 websocket 一起使用,用来拦截 websocket 请求,然后做相应的处理等等。拦截器不会拦截静态 资源,Spring Boot 的默认静态目录为 resources/static,该目录下的静态页面、js、css、图片等等, 不会被拦截.

13.2定义拦截器
定义拦截器,只需要实现 HandlerInterceptor 接口, 该接口中有三个方法:

preHandle(……) 、 postHandle(……) 和 afterCompletion(……) 。

preHandle(……) 方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某

个方法,且在这个方法执行之前。所以 preHandle(……) 方法可以决定是否将请求放行,这是通

过返回值来决定的,返回 true 则放行,返回 false 则不会向后执行。

postHandle(……) 方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某

个方法,且在执行完了该方法,但是在 DispatcherServlet 视图渲染之前。所以在这个方法中有

个 ModelAndView 参数,可以在此做一些修改动作。

afterCompletion(……) 方法:顾名思义,该方法是在整个请求处理完成后(包括视图渲染)执

行,这时做一些资源的清理工作,这个方法只有在 preHandle(……) 被成功执行后并且返回 true

才会被执行。

13.2.1)自定义拦截类实现HandlerInterceptor接口
public class MyInterceptor implements HandlerInterceptor {
    private Logger logger = LoggerFactory.getLogger(MyInterceptor.class);

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        String methodName = method.getName();
        logger.info("方法{}被拦截", methodName);
        return true;
    }

@Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.info("方法被执行,但是视图还未渲染");
    }

@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("方法执行完毕,进行资源清理");
    }
}

13.2.2)实现WebMvcConfigurer接口进行拦截配置
实现WebMvcConfigurer的这种配置会自动过滤静态资源;

@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
    }
}

省略controller类

测试结果

10:52:16.316 [http-nio-8080-exec-1] INFO  c.e.h.interceptors.MyInterceptor - 方法test被拦截
10:52:16.344 [http-nio-8080-exec-1] INFO  c.e.h.interceptors.MyInterceptor - 方法被执行,但是视图还未渲染
10:52:16.344 [http-nio-8080-exec-1] INFO  c.e.h.interceptors.MyInterceptor - 方法执行完毕,进行资源清理

定义哪些不用拦截

取消拦截操作

如果我要拦截所有 /admin 开头的 url 请求的话,需要在拦截器配置中添加这个前缀,但是 在实际项目中,可能会有这种场景出现:某个请求也是 /admin 开头的,但是不能拦截,比如 /admin/login 等等

解决方案:

1.使用excludePathPatterns(“/adminUser/login”)
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("/adminUser/login");
    }

2.可以定义一个注解
该注解专门用来取消拦截操作,如果某个 Controller 中的方法我们 不需要拦截掉,即可在该方法上加上我们自定义的注解即可,下面先定义一个注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UnInterception {
}

测试
controller类
@RestController
@RequestMapping("/intercept")
public class IntercepController {

@UnInterception
    @RequestMapping("/hello")
    public String test() {
        return "success";
    }

interceptor拦截类
   @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        String methodName = method.getName();
        if (method.getAnnotation(UnInterception.class)!=null) {
            logger.info("方法{}不被拦截", methodName);
            return true;
        }
        logger.info("方法{}被拦截", methodName);
        return false;
    }

访问 http://localhost:8080/intercept/hello

结果

11:34:24.648 [http-nio-8080-exec-1] INFO  c.e.h.interceptors.MyInterceptor - 方法test不被拦截
11:34:24.676 [http-nio-8080-exec-1] INFO  c.e.h.interceptors.MyInterceptor - 方法被执行,但是视图还未渲染
11:34:24.679 [http-nio-8080-exec-1] INFO  c.e.h.interceptors.MyInterceptor - 方法执行完毕,进行资源清理

14.Spring Boot中集成Redis
14.1)简介:
redis是一款高性能的NOSQL系列的非关系型数据库

14.1.1)非关系型数据库的优势:
​ 1)性能NOSQL是基于键值对的,可以想象成表中的主键和值的对应关系,而且不需要经过SQL层的解析,所以性能非常高。
​ 2)可扩展性同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。

14.1.2) 关系型数据库的优势:
​ 1)复杂查询可以用SQL语句方便的在一个表以及多个表之间做非常复杂的数据查询。
​ 2)事务支持使得对于安全性能很高的数据访问要求得以实现。对于这两类数据库,对方的优势就是自己的弱势,反之亦然。

14.1.3) 总结
​ 关系型数据库与NoSQL数据库并非对立而是互补的关系,即通常情况下使用关系型数据库,在适合使用NoSQL的时候使用NoSQL数据库,
​ 让NoSQL数据库对关系型数据库的不足进行弥补。
​ 一般会将数据存储在关系型数据库中,在nosql数据库中备份存储关系型数据库的数据

14.2)使用场景
​ •缓存(数据查询、短连接、新闻内容、商品内容等等)
​ • 聊天室的在线好友列表
​ • 任务队列。(秒杀、抢购、12306等等)
​ • 应用排行榜
​ • 网站访问统计
​ • 数据过期处理(可以精确到毫秒
​ • 分布式集群架构中的session分离

14.3)安装Redis
14.3.1)Windows下载安装
官网:https://redis.io

中文网:http://www.redis.net.cn/ (https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100)

下载解压后直接可以使用:

redis.windows.conf:配置文件
redis-cli.exe:redis的客户端
redis-server.exe:redis服务器端
14.3.1) Linux下载安装
1.安装 gcc 编译

因为后面安装redis的时候需要编译,所以事先得先安装gcc编译。阿里云主机已经默认安装了 gcc,如

果是自己安装的虚拟机,那么需要先安装一下 gcc:

yum install gcc-c++

2.下载 redis

有两种方式下载安装包,一种是去官网上下载(https://redis.io),然后将安装包考到 centos 中,另 种方法是直接使用 wget 来下载:

wget http://download.redis.io/releases/redis-3.2.8.tar.gz

如果没有安装过 wget,可以通过如下命令安装:

yum install wget

3.解压安装

解压安装包:

tar –vzxf redis-3.2.8.tar.gz

然后将解压的文件夹 redis-3.2.8 放到 /software下,software文件夹是自己在根目录下创建的,然后 进入 /software/redis-3.2.8/ 文件夹下,执行 make 命令即可完成安装。

【注】如果 make 失败,可以尝试如下命令:

make MALLOC=libc 
make install

4.修改配置文件

安装成功之后,需要修改一下配置文件,包括允许接入的 ip,允许后台执行,设置密码等等。 打开 redis 配置文件:

vi redis.conf

在命令模式下输入 /bind 来查找 bind 配置,按 n 来查找下一个,找到配置后,将 bind 配置成

0.0.0.0,允许任意服务器来访问 redis,即:

bind 0.0.0.0

使用同样的方法,将 daemonize 改成 yes (默认为 no),允许 redis 在后台执行。

将 requirepass 注释打开,并设置密码为 123456(密码自己设置)。

5.启动 redis

在 redis-3.2.8 目录下,指定刚刚修改好的配置文件 redis.conf 来启动 redis:

redis-server ./redis.conf

由于我们设置了密码,在启动客户端之后,必须输入 auth root(设置的密码) 才可登录进入客户端。

[root@VM_0_16_centos src]# redis-cli 
127.0.0.1:6379> get test
(error) NOAUTH Authentication required.
127.0.0.1:6379> AUTH root
OK
127.0.0.1:6379> get test
"1"

Linux下redis运维的一些指令:

redis 的启动、关闭 判断其是否在运行中
#检查后台进程是否正在运行

ps -ef |grep redis

ps aux | grep redis

#检测6379端口是否在监听

netstat -lntp | grep 6379

#使用配置文件启动redis服务

./redis-server /etc/redis/redis.conf

#使用`redis-cli`客户端检测连接是否正常

./redis-cli -h 127.0.0.1 -p 6379    (登陆客户端)

#关闭redis:    
redis-cli shutdown

14.4)集成redis
14.4.1)依赖导入
用于业务中将对象转换为json格式的字符串;

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!--springboot 2.x 使用的Lettuce 依赖org.apache.commons-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

<!--阿里巴巴fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.35</version>
        </dependency>

14.4.2)yaml配置文件
#redis相关配置
redis:
  database: 5
  # 配置redis的主机地址,需要修改成自己的
  host: 127.0.0.1
  port: 6379
  password:
  timeout: 5000
  lettuce:
        pool:
          # 连接池中的最大空闲连接,默认值也是8。
          max-idle: 50
          # 连接池中的最小空闲连接,默认值也是0。
          min-idle: 0
          # 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool 的状态为exhausted(耗尽)
          max-active: 50
          # 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接 抛出JedisConnectionException
          max-wait: 1

14.43)redis配置类

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
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.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;

@Configuration
public class RedisConfiguration extends CachingConfigurerSupport {

@Resource
    private LettuceConnectionFactory lettuceConnectionFactory;

@Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(lettuceConnectionFactory);
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        template.setConnectionFactory(lettuceConnectionFactory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(serializer);
        //value hashmap序列化
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }

@Bean("myKeyGenerator")
    @Override
    public KeyGenerator keyGenerator() {
        return (Object target, Method method, Object... objects) -> method.getName() + "(" + Arrays.toString(objects) + ")";
    }

@Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(lettuceConnectionFactory);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(3))//只有通过注解的方式设置缓存才生效
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()))
                .disableCachingNullValues();
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }

}

14.5)常用 api 介绍
Redis提供了StringRedisTemplate和RedisTemplate两种模板,后者需要进行序列化比较麻烦,所以本文选择StringRedisTemplate;

14.5.1) redis:string 类型
新建一个 RedisServiceImpl,注入 StringRedisTemplate,使用 stringRedisTemplate.opsForValue() 可以获取 ValueOperations<String, String> 对象,通过该对象即可读写 redis 数据库了。

@Service
public class RedisServiceImpl {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

public void setString(String key, String value) {
        ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
        valueOperations.set(key,value);

}
     public String GetString(String key) {

return stringRedisTemplate.opsForValue().get(key);
    }

测试类

value也可以为对象,只要最后以json格式存进去即可;

@Resource
    RedisServiceImpl redisService;
    @Test
    public void testString() {
        String key="lisi";
        String value="第一名";
        redisService.setString(key,value);
        System.out.println(redisService.GetString(key));
    }

14.5.2)redis:hash 类型
hash 类型其实原理和 string 一样的,但是有两个 key,使用 stringRedisTemplate.opsForHash() 可以获取 HashOperations<String, Object, Object> 对象。比如我们要存储订单信息,所有订单 信息都放在 order 下,针对不同用户的订单实体,可以通过用户的 id 来区分,这就相当于两个 key 了。

public void setHash(String key,String filedKey,String value){
         stringRedisTemplate.opsForHash().put(key,filedKey,value);
    }

public String getHash(String key,String filedKey){
        return (String) stringRedisTemplate.opsForHash().get(key,filedKey);
    }

测试类

@Test
    public void testHash() {
        String key="userId";
        String userId="1";
        UserPO po=new UserPO();
        po.setAge(12);
        po.setName("lisi");
        redisService.setHash(key,userId, JSON.toJSONString(po));
        System.out.println(redisService.getHash(key,userId));
    }

结果

{"age":12,"name":"lisi"}

1
2
以java对象的形式取出json格式的对象

UserPO po1 = JSON.parseObject(redisService.getHash(key,userId),UserPO.class);

@Test
    public void testHash() {
        String key="userId";
        String userId="1";
        UserPO po=new UserPO();
        po.setAge(12);
        po.setName("lisi");
        redisService.setHash(key,userId, JSON.toJSONString(po));
        UserPO po1 = JSON.parseObject(redisService.getHash(key,userId),UserPO.class);
        System.out.println("po1:"+po1);

}

结果

po1:UserPO{age=12, name='lisi', son=null}

1
2
14.5.3) redis:list 类型
list 类型左右两边都可以进行添加,可以用来模拟消息队列;

**stringRedisTemplate.opsForList().range(key,start,end)**是获取对应key的start到end下标的所有者,当start为0,end为-1时表示对应key的所有值

public void leftPushList(String key, String value) {
        stringRedisTemplate.opsForList().leftPush(key,value);
    }

public String rightPopList(String key) {
        return stringRedisTemplate.opsForList().rightPop(key);
    }

public List<String> getRange(String key, int start, int end) {
        return stringRedisTemplate.opsForList().range(key,start,end);
    }

测试

@Test
    public void TestList() {
        String key="lisi";
//        redisService.leftPushList(key,"1");
//        redisService.leftPushList(key,"2");
//        redisService.leftPushList(key,"3");
        System.out.println(redisService.rightPopList(key));
        System.out.println("all:"+redisService.getRange(key,0,-1));

}

15.Spring Boot中集成ActiveMQ
15.1 JMS** 和 ActiveMQ 介绍
15.1.1 JMS** 简介
百度百科的解释:

JMS 即 Java 消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息

中间件(MOM)的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

Java 消息服务是一个与具体平台无关的 API,绝大多数 MOM 提供商都对 JMS 提供支持。

JMS 只是接口,不同的提供商或者开源组织对其有不同的实现,ActiveMQ 就是其中之一,它支持

JMS,是 Apache 推出的。JMS 中有几个对象模型:

连接工厂:ConnectionFactory

JMS连接:Connection

JMS会话:Session

JMS目的:Destination

JMS生产者:Producer

JMS消费者:Consumer

JMS消息两种类型:点对点和发布/订阅。

可以看出 JMS 实际上和 JDBC 有点类似,JDBC 是可以用来访问许多不同关系数据库的 API,而 JMS 则

提供同样与厂商无关的访问方法,以访问消息收发服务。本文主要使用 ActiveMQ。

15.1.2 ActiveMQ简介
ActiveMQ 是 Apache 的一个能力强劲的开源消息总线。ActiveMQ 完全支持JMS1.1和J2EE 1.4规范,尽

管 JMS 规范出台已经是很久的事情了,但是 JMS 在当今的 Java EE 应用中间仍然扮演着特殊的地位。

ActiveMQ 用在异步消息的处理上,所谓异步消息即消息发送者无需等待消息接收者的处理以及返回,

甚至无需关心消息是否发送成功。

​ 异步消息主要有两种目的地形式,队列(queue)和主题(topic),队列用于点对点形式的消息通信,

**主题用于发布/订阅式的消息通信。**本章节主要来学习一下在 Spring Boot 中如何使用这两种形式的消

息。

15.2 ActiveMQ安装
使用 ActiveMQ 首先需要去官网下载,官网地址为:http://activemq.apache.org/

本课程使用的版本是 apache-activemq-5.15.12

​ 在使用 ActiveMQ 之前,首先得先启动,刚才解压后的目录中有个 bin 目录,里面有 win32 和 win64

两个目录,根据自己电脑选择其中一个打开运行里面的 activemq.bat 即可启动 ActiveMQ。

消息生产者生产消息发布到queue中,然后消息消费者从queue中取出,并且消费消息。这里需要注

意:消息被消费者消费以后,queue中不再有存储,所以消息消费者不可消费到已经被消费的消息。

Queue支持存在多个消息消费者,但是对一个消息而言,只会有一个消费者可以消费

启动完成后,在浏览器中输入 http://127.0.0.1:8161/admin/ 来访问 ActiveMQ 的服务器,用户名

和密码是 admin/admin。如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMh9ix8G-1587288707521)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1586664046951.png)]

我们可以看到有 Queues 和 Topics 这两个选项,这两个选项分别是点对点消息和发布/订阅消息的查看

窗口。

**点对点消息:**消息生产者生产消息发布到 queue 中,然后消息消费者从 queue 中取出,并且消费消

息。这里需要注意:消息被消费者消费以后,queue 中不再有存储,所以消息消费者不可消费到已经被

消费的消息。Queue 支持存在多个消息消费者,但是对一个消息而言,只会有一个消费者可以消费。

**发布/订阅消息:**消息生产者(发布)将消息发布到 topic 中,同时有多个消息消费者(订阅)消费该消

息。和点对点方式不同,发布到 topic 的消息会被所有订阅者消费。下面分析具体的实现方式。

15.3 springboot集成ActiveMQ
15.3.1 导入maven依赖
<!-- ActiveMQ集成-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
        </dependency>

15.3.2 springboot的yaml配置文件
spring:
  activemq:
    password: admin
    user: admin
    broker-url: tcp://localhost:61616
    pool:
    # 如果此处设置为true,需要添加activemq-pool的依赖包,否则会自动配置失败,无法注入 JmsMessagingTemplate
      enabled: false

15.3.3 Queue 和 Topic 的创建
使用ActiveMqConfig创建,并注入spring容器中

connectionFactory的配置需要打来服务端,配置了它在 http://127.0.0.1:8161/admin/queues.jsp (activeMq的控制台)就能看到消息信息,将@Bean注释后表示不配置connectionFactory,这时我们就不需要连接控制台,依然可以进行学习操作;

@Configuration
public class ActiveMqConfig {
    /*** 发布/订阅模式队列名称 */
    public static final String TOPIC_NAME = "activemq.topic";
    /*** 点对点模式队列名称 */
    public static final String QUEUE_NAME = "activemq.queue";
    @Value("${spring.activemq.user}")
    private String user;
    @Value("${spring.activemq.password}")
    private String password;
    @Value("${spring.activemq.broker-url}")
    private  String brokerUrl;

// @Bean
    public ActiveMQConnectionFactory connectionFactory() {
        return new ActiveMQConnectionFactory(user, password, brokerUrl);
    }

@Bean
    public Destination topic() {
        return new ActiveMQTopic(TOPIC_NAME);
    }

@Bean
    public Destination queue() {
        return  new ActiveMQQueue(QUEUE_NAME);
    }

}

Destination是javax.jms.Destination包下的接口,且ActiveMQTopic和ActiveMQQueue是它的间接子类;

创建 Queue 和 Topic 两种消息,分别使用 new ActiveMQQueue 和 new ActiveMQTopic 来 创建,分别跟上对应消息的名称即可。这样在其他地方就可以直接使用Destination遵循依赖反转原则代替二者注入进来;

15.3.4消息提供者
根据传入的destination类型决定将消息发往何处;

@Service
public class MsgProducer {
    @Resource
    JmsMessagingTemplate jmsTemplate;

public void sendMessage(Destination destination, String msg) {
            jmsTemplate.convertAndSend(destination,msg);

}
}

15.3.5点对点消息生产与消费
1)点对点消息的生产

@RestController
@RequestMapping("/activeMq")
public class ActiveMqController {
    private static final Logger logger = LoggerFactory.getLogger(ActiveMqController.class);

@Resource
    MsgProducer msgProducer;

@Resource
    Destination queue;
     @RequestMapping("/send/queue")
    public void sendMessage() {
        String msg = "hello queue";
        logger.info("消息发送");
        msgProducer.sendMessage(queue, msg);
    }
    }

2)点对点消息的消费

@Service
public class MsgConsumer {
    private static final Logger logger = LoggerFactory.getLogger(ActiveMqController.class);

@JmsListener(destination = ActiveMqConfig.QUEUE_NAME)
    public void getMessage(String msg) {
        logger.info("收到的消息为:{}",msg);
    }
    }

3)测试

访问 http://localhost:8080/activeMq/send/queue

结果

13:46:36.168 [http-nio-8080-exec-4] INFO  c.e.controller.ActiveMqController - 消息发送
13:46:36.177 [DefaultMessageListenerContainer-1] INFO  c.e.controller.ActiveMqController - 收到的消息为:hello queue

10.3.6发布订阅消息的生产和消费
1)发布订阅消息的生产

@RestController
@RequestMapping("/activeMq")
public class ActiveMqController {
    private static final Logger logger = LoggerFactory.getLogger(ActiveMqController.class);

@Resource
    MsgProducer msgProducer;

@Resource
    Destination topic;
    
   @RequestMapping ("/send/topic")
    public String sendTopicMessage() {
        logger.info("消息发送");
        msgProducer.sendMessage(topic, "Topic: hello activemq!");
        return "success";
    }
    }

2)发布订阅消息的消费

@Service
public class MsgProducer {
    @Resource
    JmsMessagingTemplate jmsTemplate;

public void sendMessage(Destination destination, String msg) {
            jmsTemplate.convertAndSend( destination,msg);

}
}

发布/订阅消息的消费和点对点不同,订阅消息支持多个消费者一起消费。其次,Spring Boot 中默认的

时点对点消息,所以在使用 topic 时,会不起作用,我们需要在配置文件 application.yml 中添加一个

配置:

spring:

jms:

pub-sub-domain: true

​ 该配置是 false 的话,则为点对点消息,也是 Spring Boot 默认的。这样是可以解决问题,但是如果这

样配置的话,上面提到的点对点消息又不能正常消费了。所以二者不可兼得,这并非一个好的解决办

法。

​ 比较好的解决办法是,我们定义一个工厂, @JmsListener 注解默认只接收 queue 消息,如果要接收

topic 消息,需要设置一下 containerFactory。我们还在上面的那个 ActiveMqConfig 配置类中添加:

@Bean
    public JmsListenerContainerFactory topicListenerContainer(ConnectionFactory connectionFactory) {
        DefaultJmsListenerContainerFactory factory=new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        // 相当于在application.yml中配置:spring.jms.pub-sub-domain=true
        factory.setPubSubDomain(true);
        return factory;
    }

经过这样的配置之后,我们在消费的时候,在 @JmsListener 注解中指定这个容器工厂即可消费 topic

消息。如下

@JmsListener(destination = ActiveMqConfig.TOPIC_NAME,containerFactory ="topicListenerContainer" )
    public void getTopicMessage(String msg) {
        logger.info("收到的消息为:{}",msg);
    }

3)测试

访问 http://localhost:8080/activeMq/send/topic

4)结果

13:54:20.164 [http-nio-8080-exec-7] INFO  c.e.controller.ActiveMqController - 消息发送
13:54:20.192 [DefaultMessageListenerContainer-1] INFO  c.e.controller.ActiveMqController - 收到的消息为:Topic: hello activemq!

16.Spring Boot中集成Shiro
1.shiro简介
Apache Shiro是一个功能强大且灵活的开源安全框架,可以干净地处理身份验证,授权,企业会话管理和加密。

2.shiro功能介绍
Apache Shiro是具有许多功能的全面的应用程序安全框架。下图显示了Shiro核心功能

Shiro以Shiro开发团队所谓的“应用程序安全性的四个基石”为目标-身份验证,授权,会话管理和密码术:

**身份验证:**有时称为“登录”,这是证明用户就是他们所说的身份的行为。
**授权:**访问控制的过程,即确定“谁”有权访问“什么”。
**会话管理:**即使在非Web或EJB应用程序中,也可以管理用户特定的会话。
**密码术:**使用密码算法保持数据安全,同时仍然易于使用。
3.shiro核心组件
Subject(org.apache.shiro.subject.Subject)
当前与软件交互的实体(用户,第三方服务,计划任务等)的特定于安全性的“视图”。(把操作交给SecurityManager)
SecurityManager(org.apache.shiro.mgt.SecurityManager)
安全管理器(关联Realm)
Realm(org.apache.shiro.realm.Realm)
领域充当Shiro与应用程序的安全数据之间的“桥梁”或“连接器”。当真正需要与安全性相关的数据(如用户帐户)进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个Realms中查找许多此类内容。您可以根据Realms需要配置任意数量(通常每个数据源一个),并且Shiro会根据需要进行协调,以进行身份验证和授权。
4.shiro实战
4.1 环境构建
1.maven依赖:

<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

2.自定义Realm

自定义 realm 需要继承 AuthorizingRealm 类,因 为该类封装了很多方法,它也是一步步继承自 Realm 类的,继承了 AuthorizingRealm 类后,需要重写 两个方法:

doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息

doGetAuthorizationInfo() 方法:用来为当前登陆成功的用户授予权限和角色

public class MyRealm extends AuthorizingRealm {
    @Resource
    UserService userService;

@Override
    //用来为当前登陆成功的用户授予权限和角色
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

return null;
    }

@Override
    //用来验证当前登录的用户,获取认证信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
       
            return null;

}

}

后续实现具体功能再对这两个方法进行具体实现和讲解;

3.shiro配置文件

1.ShiroFileterFactoryBean : 可以进行权限设置,访问控制等功能,由DefaultWebSecurityManager 对设置进行实现;

2.DefaultWebSecurityManager :根据自定义的Realm进行安全管理;

3.自定义Realm: 自定义验证规则;

@Configuration
public class ShiroConfig {
/*
1.ShiroFileterFactoryBean
* */
@Bean
public ShiroFilterFactoryBean getshShiroFilterFactoryBean(@Qualifier("securityManager") DefaultSecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
    //添加shiro内置过滤器
    /*
    * 常用过滤器:
    *   anon:无需认证(登录)可以访问
    *   authc:必须认证才可以访问
    *   user: 如果使用remmemberMe的功能可以直接访问
    *   perms: 该资源必须得到资源权限才可以访问
    *   roles:该资源必须得到角色权限才可以访问
    * */

//配置过滤规则
    Map<String,String> filter=new LinkedHashMap<>();
    filter.put("/user/add","anon");
    filter.put("/user/update","authc");
    //放行hello.html
    filter.put("/hello.html","anon");
    //拦截所有请求包括html也会被拦截
    filter.put("/*","authc");
    //将过滤规则设置到shiroFilterFactoryBean
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filter);
    //访问被拦截后访问的地址,如/user/update被拦截时就会访问/user/toLogin;
    shiroFilterFactoryBean.setLoginUrl("/user/toLogin");
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    return shiroFilterFactoryBean;
}

/*
* 2.DefaultWebSecurityManager
* */
@Bean(name = "securityManager")
public DefaultSecurityManager getDefaultSecurityManager(@Qualifier("MyRealm")Realm myRealm) {
    DefaultSecurityManager securityManager = new DefaultWebSecurityManager(myRealm);
    return securityManager;
}

/*
* 3.自定义Realm
* */
@Bean(name = "MyRealm")
public Realm getRealm() {
    return new MyRealm();
}
}

配置的过滤规则只有访问controller层进行跳转时才能生效,直接访问xxx.html页面是不生效的,也就是说即使你配置了拦截/add,也可以访问add.html;

4.2 登入验证
1.创建数据库表

暂时只用到用户表,角色表和权限表用于后续授权;

#角色表
CREATE TABLE `t_role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `rolename` varchar(20) DEFAULT NULL COMMENT '角色名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

#用户表
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户主键', `username` varchar(20) NOT NULL COMMENT '用户名', `password` varchar(20) NOT NULL COMMENT '密码' ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

#权限表
CREATE TABLE `t_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `permissionname` varchar(50) NOT NULL COMMENT '权限名', `role_id` int(11) DEFAULT NULL COMMENT '外键关联role', PRIMARY KEY (`id`), KEY `role_id` (`role_id`), CONSTRAINT `t_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8

#用户与权限表
CREATE TABLE `user_role` (
  `user_id` int(11) DEFAULT NULL,
  `role_id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.自定义Realm的doGetAuthenticationInfo方法添加具体实现

public class MyRealm extends AuthorizingRealm {
    @Resource
    UserService userService;

@Override
    //用来为当前登陆成功的用户授予权限和角色
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

return null;
    }

@Override
    //用来验证当前登录的用户,获取认证信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 根据token获取用户名,此Token是controller层穿来的,具体看下一步
        String userName = (String) authenticationToken.getPrincipal();
        // 根据用户名从数据库中查询该用户
        User user = userService.getByUsername(userName);
        if (user != null) {
     //返回SimpleAuthenticationInfo来验证密码,只有user.getPassword()为必填参数,其他的可以填“”;         //验证失败 subject.login(token);方法会出现IncorrectCredentialsException异常      
            return SimpleAuthenticationInfo(user.getUserName(), user.getPassword(), "MyRealm");
        } else {
            //返回null subject.login(token)方法会报UnknownAccountException异常;
            return null;
        }

}

}

3.编写controller

subject.login(token)这一步会带着存有前端传来的用户信息去自定义的Realm中的doGetAuthenticationInfo验证用户信息;

@Controller
@RequestMapping("/user")
public class UserController {

@RequestMapping("/login")
    public String login(User user, Model model) {
        if (user == null) {
            return "user/login";
        } else {
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token=new UsernamePasswordToken(user.getUserName(), user.getPassword());
            try {
                subject.login(token);
            } catch (UnknownAccountException e) {
                model.addAttribute("msg", "用户不存在");
                return "user/login";
            } catch (IncorrectCredentialsException e) {
                model.addAttribute("msg","密码错误");
               return  "user/login";
            }
        }
        return "success";
    }
}

4.前端代码login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p style="color: red" th:text="${msg}"></p>
<form action="/user/login" >
    <p>请输入userName:</p>
    <input type="text" name="userName">
    <p>请输入password:</p>
    <input type="text" name="password" >
    <input type="submit" value="submit">
</form>
</body>
</html>

超详细的springBoot学习教程相关推荐

  1. 超详细的springBoot学习教程,springboot学习看这篇就够了

    springBoot学习 https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/html/index.html (官方文档) ...

  2. 超详细的springBoot学习笔记

    Spring Boot 1.Spring Boot简介 Spring诞生时是Java企业版(Java Enterprise Edition,JEE,也称J2EE)的 轻量级代替品.无需开发重量级的En ...

  3. 【Redis二三事】一套超详细的Redis学习教程(步骤图片+实操)---第二集

    ⭐️写在前面 这里是温文艾尔の学习之路

  4. 超详细的 MySQL 学习教程二(多实例、附练习、视频讲解持续更新)

    目录 一.表记录的检索(单表) 1.1 基本查询语句 1.2 单表查询 一.表记录的检索(单表) 1.1 基本查询语句 SELECT 语句是最常用的查询语句,它的使用方式有些复杂,但功能是相当强大的. ...

  5. 超详细的 MySQL 学习教程(多实例、附练习、视频讲解持续更新)

    目录 一.启动.连接.断开和停止MySQL服务器命令 1.1 本小节练习 二.MySQL数据库管理 2.1 本小节练习 三.MySQL表结构管理 3.1 MySQL 数据类型 3.2 创建表.修改表. ...

  6. springboot做网站_Github点赞接近100k的SpringBoot学习教程+实战推荐!牛批!

    如果觉得看完文章对你有帮助记得点个赞,不要做收藏党.丢进收藏夹吃灰哦! 很明显的一个现象,除了一些老项目,现在 Java 后端项目基本都是基于 Spring Boot 进行开发,毕竟它这么好用以及天然 ...

  7. 【排序】什么都能排的C语言qsort排序详解【超详细的宝藏级别教程】深度理解qsort排序

    [排序]什么都能排的C语言qsort排序详解[超详细的宝藏级别教程]深度理解qsort排序 作者: @小小Programmer 这是我的主页:@小小Programmer 在食用这篇博客之前,博主在这里 ...

  8. 超详细的MySQL入门教程(四)

    MySQL:简单的增删改查 查询数据 基本语法介绍 打印任意值 查询表中全部数据 查询表中部分字段 限定条件查询 例1:查询编号值小于指定值的记录 例2:查询地址不等于某值的记录 例3:查询一级地址等 ...

  9. 【数据的存储】浮点数在内存中的存储详解【超详细的保姆级别教程,让面试官心服口服】手撕浮点数存储使用方式

    [数据的存储]浮点数在内存中的存储详解[超详细的保姆级别教程,让面试官对你心服口服]手撕浮点数存储使用方式 作者: @小小Programmer 这是我的主页:@小小Programmer 在食用这篇博客 ...

  10. 超详细的gnuplot使用教程【2】

    超详细的gnuplot使用教程 1.gnuplot参数介绍及演示 1.1首先来解释一下会用到的各类参数以及其解释 1.2 画图实际测试 1.3 其它参数介绍(约定范围.坐标轴设定) 1.3.1 约束画 ...

最新文章

  1. 开源的Blink和Spark3.0,谁将称霸大数据领域?
  2. 最让青年科学家们困惑的 10 个问题,是什么?
  3. 挂在“棒棒”上的音乐世家,“我因为父亲给的木棍和筷筒子,从此四代人走上音乐路”...
  4. UI5 metadata usage in the runtime
  5. 【好程序员笔记分享】C语言之break和continue
  6. data为long 怎么设置vue_vue基础之data
  7. 努力学计算机四年,终于进腾讯了!
  8. 【HC资料合集】2019华为全联接大会主题资料一站式汇总,免费下载!
  9. matlab 删掉空行,在MATLAB中使用XLSWRITE:如何删除空单元格?
  10. 蓝桥杯 BASIC-10 十进制转十六进制
  11. 计算机win7截长屏,电脑截长图【应对法子】
  12. 基于贝叶斯分类的中文人名用字特征的性别识别
  13. PTA 哈利·波特的考试(Floyd计算最短距离并输出路径)
  14. python绘制小提琴图_Python:matplotlib 和 Seaborn 之热图、小提琴图和箱线图 (三十四)...
  15. 2019 年第 25 周 DApp 影响力排行榜 | TokenInsight
  16. Windows10+MinGW+Codelite完成C++编写平台安装
  17. gltf文件的几种形式
  18. node.js清除缓存命令
  19. Hive 外部表的练习(多表关联查询,以及分组,子查询)
  20. 源码篇-2048小游戏(需要EasyX图形库)

热门文章

  1. arch linux 看图软件,菠萝看图
  2. SASS与 PASS的基本定义、适用范围以及差异
  3. matlab飞机高度控制,基础知识 | 飞行中的各种高度(三)
  4. 2021-11-02 Kafka、Zookeeper的下载、打开、关闭
  5. 数据字典的一个简单案例
  6. python 删除字典数据,Python简单遍历字典及删除元素的方法
  7. matlab s变换
  8. 无标度网络的生成模型
  9. 零基础入门渗透测试教程
  10. AUTOSAR SWS SOME/IP Transformer