超详细的springBoot学习教程,springboot学习看这篇就够了_springboot教程-程序员宅基地

技术标签: 切面AOP处理  springBoot配置文件  全局异常处理  框架  集成常用技术  docker  

springBoot学习

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

1.搭建springBoot项目架构

1.spring boot的Maven基础配置
1.父项目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
name : huang

行内写法:

{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(类路径)下的配置文件;

原则:四个位置的配置文件都会被加载(互补配置,高优先级加载了的属性低优先级不再加载)

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   &

spring.config.location和spring.config.additional-location优先级高于内部配置文件;

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>

浏览器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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":""}}

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":"操作成功!"}

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() {
    }

然后使用该切面的话,就会切入注解是 @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提供了StringRedisTemplateRedisTemplate两种模板,后者需要进行序列化比较麻烦,所以本文选择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"}

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

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核心功能

img

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

  • **身份验证:**有时称为“登录”,这是证明用户就是他们所说的身份的行为。
  • **授权:**访问控制的过程,即确定“谁”有权访问“什么”。
  • **会话管理:**即使在非Web或EJB应用程序中,也可以管理用户特定的会话。
  • **密码术:**使用密码算法保持数据安全,同时仍然易于使用。
3.shiro核心组件
  • Subjectorg.apache.shiro.subject.Subject
    当前与软件交互的实体(用户,第三方服务,计划任务等)的特定于安全性的“视图”。(把操作交给SecurityManager)
  • SecurityManagerorg.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>

5.测试

输入不存在用户时:

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

输入错误密码时:

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

输入正确密码:

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

登录验证成功后 shiroConfig中getshShiroFilterFactoryBean方法所配置的需要验证才能访问的请求(authc)就都能访问了;

4.3 添加角色和添加授权

数据库表结构:

t_user:

“id” “username” “password”
“1” “yg1” “a”
“2” “yg2” “a”
“3” “yg3” “a”

t_role

“id” “rolename”
“1” “admin”
“2” “boss”
“3” “user”

t_permission

“id” “permissionname” “role_id”
“1” “user:*” “1”
“2” “user:add” “2”
“3” “user:update” “3”

user_role

“u_id” “r_id”
“1” “1”
“2” “2”
“3” “3”

1.userMapper接口的编写

public interface UserMapper {
    
    public Set<Role> getRoles(String userName);
    public Set<Permission> getPermissions(String userName);
    public User getByUsername(String userName);
}

2.userMapper.xml的编写

<!--     public User getByUsername(String userName);-->
    <select id="getByUsername" resultType="User">
   select * from t_user where username=#{userName}
    </select>

    <!--     public Set<Role> getRoles(String userName);-->
    <select id="getRoles" resultType="Role">
    SELECT tr.* from t_role tr,user_role ur where
    tr.id=ur.r_id and ur.u_id in (select id from
   t_user where username=#{userName})
    </select>

    <!--    public Set<Permission> getPermissions(String userName);-->
    <select id="getPermissions" resultType="Permission">
SELECT tp.* from t_permission tp,user_role ur
  where tp.role_id=ur.r_id AND ur.u_id in (select id
	 from t_user where username=#{userName})
    </select>

3.userService编写

@Service
public  class UserService {
    

    @Resource
    UserMapper userMapper;
    public Set<Role> getRoles(String userName) {
    
        if (userName == "" || userName == null) {
    
            return null;
        }
        Set<Role> roleList = userMapper.getRoles(userName);
        return roleList;
    }


    public Set<Permission>getPermissions(String userName) {
    
        if (userName == "" || userName == null) {
    
            return null;
        }
        Set<Permission> permissions = userMapper.getPermissions(userName);
        return permissions;
    }
    public User getByUsername(String userName) {
    
        if (userName == "" || userName == null) {
    
            return null;
        }
        return userMapper.getByUsername(userName);
    }
}

4.自定义Realm

AuthorizationInfo方法中添加角色和添加授权;

  @Override
    //用来为当前登陆成功的用户授予权限和角色
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
        System.out.println("授权操作");
                User user = (User) principalCollection.getPrimaryPrincipal();
                String userName=user.getUserName();
       SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //添加角色
       Set<Role> roles = userService.getRoles(userName);
        //将roles转为set<String>roleNames
        Set<String>roleNames=new HashSet<>();
        for (Role role : roles) {
    
          roleNames.add(role.getRoleName());
        }
        authorizationInfo.setRoles(roleNames);

        //添加授权
        Set<Permission> permissionSet = userService.getPermissions(userName);
        //将roles转为set<String>roleNames
        Set<String>permissions=new HashSet<>();
        for (Permission permission : permissionSet) {
    
            permissions.add(permission.getPermissionName());
        }
        authorizationInfo.setStringPermissions(permissions);


        return authorizationInfo;
    }

5.shiroConfig配置文件

在shiroConfig的ShiroFilterFactoryBean方法中添加角色认证和权限认证;

filter.put(“/user/add”,“perms[user:add]”);

filter.put(“/user/update”,“roles[admin,boss]”);

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

    Map<String,String> filter=new LinkedHashMap<>();
    //filter.put("/user/add","anon");
    //filter.put("/user/update","authc");

    //授权过滤器
    filter.put("/user/add","perms[user:add]");
    filter.put("/user/update","roles[user,boss]");
    //未授权就跳转到该页面
    shiroFilterFactoryBean.setUnauthorizedUrl("/user/unAuth");
    //拦截所有请求包括html也会被拦截
   // filter.put("/*","authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filter);
    shiroFilterFactoryBean.setLoginUrl("/user/toLogin");
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    return shiroFilterFactoryBean;
}


测试:

yg1有user:权限,yg2中有user:add权限所以可以访问*/user/add**,yg3没有这些权限不能访问,yg1没有被授权user或boss角色所以不能访问/user/update而yg2被授权boss,yg3被授权user所以能访问;

5 thymeleaf整合shiro
1.导入依赖
 <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

2.shiroConfig配置类添加bean
   @Bean
    public ShiroDialect getShiroDialect() {
        return new ShiroDialect();
    }

3.编写前端代码

xmlns:shiro=“http://www.pollix.at/thymeleaf/shiro” 导入后才能有shiro标签提示;

**xmlns:th=“http://www.thymeleaf.org” ** 导入后才能有thymeleaf标签提示;

shirol:标签后面的条件成立才会显示该标签的内容

常用shirol:标签解释

shiro:guest=“” 验证当前用户是否为“访客”,即未认证(包含未记住)的用户。

<p shiro:guest="">Please <a href="login.html">login</a></p>

shiro:user=“” 认证通过或已记住的用户

<p shiro:user="">Welcome back </p>

shiro:authenticated=“” 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在

<a shiro:authenticated="" href="update.html">Update your  information</a>

shiro:principal 输出当前用户信息,通常为登录帐号信息。

<p>Hello, <shiro:principal/>, how are you today?</p>

shiro:notAuthenticated=“” 未认证通过用户,与authenticated标签相对应。与guest标签的区别是,该标签包含已记住用户。

shiro:hasRole=“xxx” 验证当前用户是否属于xxx角色

<a shiro:hasRole="admin" href="admin.html">Administer the system</a><!-- 拥有该角色 -->

shiro:lacksRole=“xxx” 与hasRole标签逻辑相反,当用户不属于该角色时验证通过

shiro:hasAllRoles=“xx1, xx2” 验证当前用户是否属于以下所有角色

shiro:hasAnyRoles="xx1, xx2 " 验证当前用户是否属于以下任意一个角色

shiro:hasPermission =“” 与Role的使用相同也有对应的lacksPermission等;

<!DOCTYPE html>
<html lang="en"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:text="${msg}"></p>
<div shiro:hasPermission="user:update">
<a href="/user/update">更新:</a>
</div>

<div shiro:hasPermission="user:add">
    <a href="add">添加:</a>
</div>

</body>
</html>

测试结果:

*yg1有user:权限所以都能显示,yg2只有user:add权限只能显示添加,yg2只有user:update权限只能显示更新;

只有经过controller层进行模板解析后跳转的html页面shiro标签才会起效;

17.Docker学习

1 介绍:

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 LinuxWindows 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。

1.1 docker组件介绍
概念 说明
Docker 镜像(Images) Docker 镜像是用于创建 Docker 容器的模板,比如 Ubuntu 系统。
Docker 容器(Container) 容器是独立运行的一个或一组应用,是镜像运行时的实体。
Docker 客户端(Client) Docker 客户端通过命令行或者其他工具使用 Docker SDK (https://docs.docker.com/develop/sdk/) 与 Docker 的守护进程通信。
Docker 主机(Host) 一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。
Docker Registry Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
Docker Machine Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure。
1.2 Docker的应用场景
  • Web 应用的自动化打包和发布。
  • 自动化测试和持续集成、发布。
  • 在服务型环境中部署和调整数据库或其他的后台应用。
  • 从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。
1.3 Docker 的优点

Docker 是一个用于开发,交付和运行应用程序的开放平台。Docker 使您能够将应用程序与基础架构分开,从而可以快速交付软件。借助 Docker,您可以与管理应用程序相同的方式来管理基础架构。通过利用 Docker 的方法来快速交付,测试和部署代码,您可以大大减少编写代码和在生产环境中运行代码之间的延迟。

2 Linux下安装docker
1.检查内核版本,必须是3.10及以上
[root@VM_0_16_centos ~]# uname -r
3.10.0-957.21.3.el7.x86_64

2.安装docker
yum install docker

3.输入y确定安装

4.启动docker
[root@VM_0_16_centos /]# systemctl start docker
 查看docker版本
[root@VM_0_16_centos /]# docker -v
Docker version 1.13.1, build cccb291/1.13.1

5.开机启动doocker
systemctl enable docker

6.停止docker
systemctl stop docker
3.镜像操作

当运行容器时,使用的镜像如果在本地中不存在,docker 就会自动从 docker 镜像仓库中下载,默认是从 Docker Hub (https://hub.docker.com) 公共镜像源下载。

3.1.搜索镜像

docker search redis

 
[root@VM_0_16_centos /]# docker search redis
INDEX       NAME                                       DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
docker.io   docker.io/redis                            Redis is an open source key-value store th...   8018      [OK]       
docker.io   docker.io/bitnami/redis                    Bitnami Redis Docker Image                      141                  [OK]
docker.io   docker.io/sameersbn/redis                                                                  79                   [OK]
docker.io   docker.io/grokzen/redis-cluster            Redis cluster 3.0, 3.2, 4.0 & 5.0               65                   
省略一部分镜像


3.2 下载镜像

docker pull redis:tag(版本号)如果不提供版本号就默认下载版本号为latest的镜像;

Digest: sha256:xxx代表下载完成;

[root@VM_0_16_centos /]# docker pull redis
Using default tag: latest
Trying to pull repository docker.io/library/redis ... 
latest: Pulling from docker.io/library/redis
c499e6d256d6: Pull complete 
bf1bc8a5a7e4: Pull complete 
7564fb795604: Pull complete 
ec6e86f783e4: Pull complete 
1371d6223f46: Pull complete 
021fd554320f: Pull complete 
Digest: sha256:a732b1359e338a539c25346a50bf0a501120c41dc248d868e546b33e32bf4fe4
Status: Downloaded newer image for docker.io/redis:latest


3.3 查看镜像仓库

docker images

[root@VM_0_16_centos /]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
docker.io/redis     latest              4cdbec704e47        12 days ago         98.2 MB


3.4 删除镜像

docker rmi IMAGE ID

如删除上面redis的镜像

docker rmi 4cdbec704e47;

4.容器操作

容器是独立运行的一个或一组应用,是镜像运行时的实体

上面安装了redis的镜像,现在我们来操作它;

1.启动容器

给容器启别名: –name 别名

  • -i: 交互式操作。

  • -t: 终端。

  • -d 指定容器的运行模式

    加了 -d 参数默认不会进入容器,想要进入容器需要使用指令 docker exec(下面会介绍到)

配置端口映射: -p 宿主机端口:容器端口

docker run -itd --name redis-test -p 6379:6379 redis

2.查看容器

  1. 查看运行状态的容器
docker ps

2)查看所有容器

docker ps -a

3.进入容器

在使用 -d 参数时,容器启动后会进入后台。此时想要进入容器,可以通过以下指令进入:

  • docker attach
  • docker exec:推荐大家使用 docker exec 命令,因为此退出容器终端,不会导致容器的停止。

进入redis-test容器

docker exec -it redis-test /bin/bash

4.退出容器

exit 退出容器

[root@VM_0_16_centos src]# docker exec -it redis-test /bin/bash
root@738b09610745:/data# redis-cli 
127.0.0.1:6379> exit
root@738b09610745:/data# 


5.删除容器

docker rm 容器ID/自定义容器名

6.查看容器日志

docker logs 容器ID/自定义容器名

想了解更多的docker知识请移步 https://www.runoob.com/docker/docker-container-usage.html ;

18.springBoot整合lucene

1. lucene简介 和 全文搜索
1.1 lucene简介

Lucene是一套用于全文检索和搜寻的开源程式库,由 Apache 软件基金会支持和提供。Lucene

提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在 Java 开发环境里 Lucene 是

一个成熟的免费开源工具。就其本身而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信

息检索程序库。——《百度百科》

1. 2.全文检索和普通检索的区别

普通检索: 在一个文件中查找某个字符串,最直接的想法就是从头开始检 索,查到了就OK,这种对于小数据量的文件来说,很实用,但是对于大数据量的文件来说,就有点吃 力了。或者说找包含某个字符串的文件,也是这样,如果在一个拥有几十个 G 的硬盘中找那效率可想而 知,是很低的。 文件中的数据是属于非结构化数据,也就是说它没有什么结构可言.

全文搜索: 将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对这些有一定 结构的数据进行搜索,从而达到搜索相对较快的目的。这就叫全文搜索。即先建立索引,再对索引进行 搜索的过程。

2 Lucene 建立索引的方式

假设现在有两篇文章,内容如下:

文章1的内容为:Tom lives in Guangzhou, I live in Guangzhou too.

文章2的内容为:He once lived in Shanghai.

首先第一步是将文档传给分词组件(Tokenizer),分词组件会将文档分成一个个单词,并去除标点符

**号和停词。所谓的停词指的是没有特别意义的词,比如英文中的 a,the,too 等。**经过分词后,得到词

元(Token) 。如下:

文章1经过分词后的结果: [Tom] [lives] [Guangzhou] [I] [live] [Guangzhou]

文章2经过分词后的结果: [He] [lives] [Shanghai]

然后将词元传给语言处理组件(Linguistic Processor),对于英语,语言处理组件一般会将字母变为小

写,将单词缩减为词根形式,如 ”lives” 到 ”live” 等,将单词转变为词根形式,如 ”drove” 到 ”drive”

。然后得到词(Term)。如下:

文章1经过处理后的结果: [tom] [live] [guangzhou] [i] [live] [guangzhou]

文章2经过处理后的结果: [he] [live] [shanghai]

最后将得到的词传给索引组件(Indexer),索引组件经过处理,得到下面的索引结构:

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

它的关键字是按字符顺序排列的,因此 Lucene 可以用二 元搜索算法快速定位关键词。实现时 Lucene 将上面三列分别作为词典文件(Term Dictionary)、频率 文件(frequencies)和位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了 指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。 搜索的过程是先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结 果,然后就可以在具体的文章中根据出现位置找到该词了。所以 Lucene 在第一次建立索引的时候可能 会比较慢,但是以后就不需要每次都建立索引了,就快了.

3.lucene的使用

导入依赖

lucene-core luncene核心jar包

lucene-queryparser 用于查询功能的jar包

lucene-analyzers-common 用于分词的jar包

<dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>7.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>7.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>7.6.0</version>
        </dependency>

3.1 文件的全文搜索

1.建立索引:

public class Indexer {
    

    //索引实例
    private IndexWriter writer;



    public Indexer(String indexDir) throws IOException {
    
        Directory dir = FSDirectory.open(Paths.get(indexDir));
        //标准分词器,会自动去掉空格啊,is a the等单词
        Analyzer analyzer = new StandardAnalyzer();
        //将标准分词器配到写索引的配置中
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //实例化写索引对象
        writer = new IndexWriter(dir, config);

    }

    //索引指定目录下的所有文件
    public int indexAll(String dataDir) throws Exception {
    
        File[] files = new File(dataDir).listFiles();
        if (null != files) {
    
            for (File file : files) {
    
                //调用下面的indexFile方法,对每个文件进行索引
                indexFile(file);
            }
        }
        writer.commit();
        return writer.numDocs();
    }

    private void indexFile(File file) throws Exception {
    
        System.out.println("索引文件的路径:" + file.getCanonicalPath());
        //调用下面的getDocument方法,获取该文件的document
        Document doc = getDocument(file);
        //将doc添加到索引中
        writer.addDocument(doc);
    }

    //获取文档,文档里再设置每个字段,就类似于数据库中的一行记录
    private Document getDocument(File file) throws Exception {
    
        Document doc = new Document();
        //开始添加字段
        // 添加内容
        doc.add(new TextField("contents", new FileReader(file)));
        //添加文件名,并把这个字段存到索引文件里
        doc.add(new TextField("fileName", file.getName(), Field.Store.YES));
        //添加文件路径
        doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES));
        return doc;
    }

    public static void main(String[] args) {
    
        //索引保存到的路径
         String indexDir = "D:/lucene";
         //需要索引的文件数据存放的目录
         String dataDir = "D:/lucene/data";
         Indexer indexer = null;
         int indexedNum = 0;
         //记录索引开始时间
         long startTime = System.currentTimeMillis();
         try {
    
        // 开始构建索引
         indexer = new Indexer(indexDir);
         indexedNum = indexer.indexAll(dataDir);
         } catch (Exception e) {
    
             e.printStackTrace();
         }finally {
    
             try {
    
                 indexer.writer.close();
             } catch (IOException e) {
    
                 e.printStackTrace();
             }
         }
         //记录索引结束时间
         long endTime = System.currentTimeMillis();
         System.out.println("索引耗时" + (endTime - startTime) + "毫秒");
         System.out.println("共索引了" + indexedNum + "个文件");
         }
    }

2.查询索引

public class Searcher {
    

    public static void search(String indexDir, String q) throws Exception {
    
//获取要查询的路径,也就是索引所在的位置
        Directory directory = FSDirectory.open(Paths.get(indexDir));
        IndexReader reader = DirectoryReader.open(directory);
        //构建IndexSearcher
        IndexSearcher searcher = new IndexSearcher(reader);
        //标准分词器,会自动去掉空格啊,is a the等单词
        Analyzer analyzer = new StandardAnalyzer();
        //查询解析器
        QueryParser parser = new QueryParser("contents", analyzer);
        //通过解析要查询的String,获取查询对象,q为传进来的待查的字符串
        Query query = parser.parse(q);
        //记录查询开始的时间
        long startTime = System.currentTimeMillis();
        //查询前十条记录
        TopDocs topDocs = searcher.search(query, 10);
         ScoreDoc[] scoreDocs=topDocs.scoreDocs;
        //记录查询结束的时间
        long endTime = System.currentTimeMillis();
        System.out.println("匹配" + q + "共耗时" + (endTime - startTime) + "毫秒");
        System.out.println("查询到" + topDocs.totalHits + "条记录");
        //取出每条结果
        for (ScoreDoc scoreDoc : scoreDocs) {
    
            //scoreDoc.doc是文档ID,lucene在创建文档时默认分配的;
            Document doc = searcher.doc(scoreDoc.doc);
            //fullPath是刚刚建立索引的时候我们定义的一个字段,表示路径。也可以取其他的内容, 只要我们在建立索引时有定义即可。
            System.out.println(doc.get("fullPath"));

        }
        reader.close();

    }

    public static void main(String[] args) {
    
        String indexDir = "D:/lucene";
        //查询这个字符串
         String q = "body";
        try {
    
            search(indexDir, q);
        } catch (Exception e) {
    
            e.printStackTrace();
        }
    }
}

3.2 数据库的全文搜索

1.UserInforMapper接口的编写

public interface UserInforMapper {
    
    List<UserInforPO> getAllData();
}

2.UserInforMapper .xml文件编写

<mapper namespace="com.example.mapper.UserInforMapper">

<!--List<UserInforPO> getAllData();-->
    <select id="getAllData" resultType="UserInforPO">
        SELECT *from tb_userinfo
    </select>


</mapper>

3.建立索引类

@Component
public class DataBaseIndex {
    

    //写索引
    private  IndexWriter indexWriter;

   @Resource
    private  UserInforService service;


//建立索引
    public void initIndexWriter(String destDir) throws Exception {
    
        //目录文件存放地
        Directory directory = FSDirectory.open(Paths.get(destDir));
        //分词器
        Analyzer analyzer = new StandardAnalyzer();
        //IndexWriter的配置,决定采用那种分词器写索引
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //构建IndexWriter对象
        indexWriter = new IndexWriter(directory, config);
    }

    //获取数据库数据
    public   int indexAll() throws Exception {
    
        System.out.println("service:" + service);
        //重数据库得到数据
        List<UserInforPO> dataList = service.getAllData();

        if (dataList == null) {
    
            return 0;
        }
        for (UserInforPO po : dataList) {
    
            Document doc = getDocument(po);
            indexWriter.addDocument(doc);
        }
        return indexWriter.numDocs();
    }

    //设置文档域
    private   Document getDocument(UserInforPO po) {
    
        Document doc = new Document();
        /*Field 域的使用
         * StringField表示ID属性:
         *    是否分词:否  ,因为id是唯一标识,整个id代表一条记录所以不分词
         *    是否索引:是  ,需要用到id去查找数据所以要索引
         *    是否存储:是 ,不存储能通过该属性查到数据,
         * 但是该属性为null,id比较重要所以要存储;
         * */

        doc.add(new StringField("uid", po.getUid().toString(), Store.YES));
        doc.add(new StringField("tid", po.getTid().toString(), Store.YES));
        doc.add(new TextField("uname", po.getUname(), Store.YES));
        //无需根据密码查询,所以密码只存储,不分词,不索引
        doc.add(new StoredField("upwd", po.getUpwd()));
        doc.add(new StringField("usex", po.getUsex(), Store.YES));
        doc.add(new StringField("uemail", po.getUemail(), Store.YES));
        return doc;
    }

    //启动建立索引
public void startSetIndex(String destDir) {
    
    try {
    
        this.initIndexWriter(destDir);
        long startTime=System.currentTimeMillis();
        int total=this.indexAll();
        long endTime=System.currentTimeMillis();
        System.out.println("索引共用时:"+(endTime-startTime)+"ms");
        //在数据库中则表示索引多少条记录
        System.out.println("总共索引"+total+"个文件");
    } catch (Exception e) {
    
        e.printStackTrace();
    }finally {
    
        try {
    
            indexWriter.close();
        } catch (IOException e) {
    
            e.printStackTrace();
        }
    }
}

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserInfoServiceTest {
    
 
    @Resource
    DataBaseIndex dataBaseIndex;

//建立索引
    @Test
    public void testSetIndex() throws Exception {
    
        dataBaseIndex.startSetIndex("d:/lucene");
    }
    
     //查询索引
    @Test
    public void testSearch() throws Exception {
    
        //1.设置分词器,要和建立索引使用相同的分词器
        Analyzer analyzer=new StandardAnalyzer();
        //2.建立查询对象,第一个参数表示如果搜索关键词中没带域名则默认按域名为name的查询
        QueryParser queryParser = new QueryParser("uname", analyzer);
        //3.设置搜索关键词,
        Query query =queryParser.parse("uid:1");
        //4.创建Directory目录对象,指定索引的位置
        Directory dir= FSDirectory.open(Paths.get("D:/lucene"));
        //5.创建输入流对象
        IndexReader reader = DirectoryReader.open(dir);
        //6.创建搜索对象
        IndexSearcher indexSearcher=new IndexSearcher(reader);
        //7.搜索并返回结果,第二个参数是返回多少个参数,分页使用;
        TopDocs topDocs = indexSearcher.search(query, 4);
        System.out.println("总共多少结果:"+topDocs.totalHits);
        //8.获取结果集
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        //9.遍历结果集
        if (scoreDocs!=null) {
    
            for (ScoreDoc scoreDoc : scoreDocs) {
    
                Document doc=indexSearcher.doc(scoreDoc.doc);
                System.out.println("---------------------------");
                //打印域
                System.out.println("uid: " + doc.get("uid"));
                System.out.println("uname: " + doc.get("uname"));
                System.out.println("uemail: " + doc.get("uemail"));
            }
        }


    }
}

3.3. 索引的维护

对索引的更新和删除;

@Component
public class DataBaseIndex {
    

    //写索引
    private  IndexWriter indexWriter;



//建立索引
    public void initIndexWriter(String destDir) throws Exception {
    
        //目录文件存放地
        Directory directory = FSDirectory.open(Paths.get(destDir));
        //分词器
        Analyzer analyzer = new StandardAnalyzer();
        //IndexWriter的配置,决定采用那种分词器写索引
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //构建IndexWriter对象
        indexWriter = new IndexWriter(directory, config);
    }



//更新索引
    public void updateIndex(String field, String value) throws Exception {
    
        Document doc=new Document();
        //可以用Map<string,String> 封装后传进来;
        doc.add(new StringField("uid", "3", Store.YES));
        doc.add(new StringField("tid", "1", Store.YES));
        doc.add(new TextField("uname", "mumu", Store.YES));

      this.initIndexWriter("d:/lucene");

      //修改,第一个参数:修改条件,第二个参数:修改内容
        indexWriter.updateDocument(new Term(field,value),doc);
        indexWriter.close();
    }

    //删除索引
    public void deleteIndex(String field, String value) throws Exception {
    
        this.initIndexWriter("d:/lucene");
        indexWriter.deleteDocuments(new Term(field,value));
        indexWriter.close();
    }

}

测试

//修改索引
    @Test
    public void testUpdateIndex() throws Exception {
    
        dataBaseIndex.updateIndex("uid","3");
    }

    //删除索引
    @Test
    public void testDeleteIndex() throws Exception {
    
        dataBaseIndex.deleteIndex("uid","1");
    }

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lin1214000999/article/details/105468338

智能推荐

ubi 下ubi_ltree_entry获取与初始化过程_初始化ubi.img-程序员宅基地

文章浏览阅读762次。在ubi中,每次写读一个volume是,都会对当期的这个volume进加锁。用到的就是rw_semaphore。关于读写锁可以查看一下网上别人写的东西。获取这个锁的函数(这里我主要以write为例来说明)是:/** * leb_write_lock - lock logical eraseblock for writing. * @ubi: UBI device descrip_初始化ubi.img

Win10安装Glog0.4.0|Cmake|VS2017-程序员宅基地

文章浏览阅读1k次。Win10安装Glog-0.4.0 Cmake VS2017准备下载CmakeGlogVS2017*Gflags编译及配置环境Cmake编译gflags编译glog配置环境变量测试新建工程平台设置加入头文件、dll、lib文件测试代码致谢准备下载Cmake进入cmake官网点击Download Latest Release下载最新版本,本文档使用Cmake3.19.1。Glog进入glog的github主页点击右侧Releases能够下载各个版本的glog,本文档使用glog0.4.0。VS2

软件工程心理学之5---乙方如何面对甲方之1 -程序员宅基地

文章浏览阅读367次。 在之前的软件工程心理学系列的文章中,谈到了关于在整个软件项目工程范围内,甲方领导,甲方以及乙方的之间的一些关系,以及作为甲方负责人,如何能揣摩好领导以及乙方的心理,更好地有利于工程项目的进行。在接下来的文章里,将开始谈谈作为乙方,如何揣摩好用户(这里是说甲方了)的心理,如何在项目过程中搞好和用户的关系,如何在项目过程中最大限度优化自己的资源,保护好自己的利益,又能跟用户愉快合作,不得失用..._软件工程甲方乙方沟通情景模拟

Unity分辨率适配方案设置_unity 分辨率-程序员宅基地

文章浏览阅读1.4w次,点赞7次,收藏58次。有关适配问题官方文档的连接,有兴趣的小伙伴可以查阅一下。Anchors:https://docs.unity3d.com/Manual/UIBasicLayout.htmlCanvasScaler:https://docs.unity3d.com/Manual/script-CanvasScaler.html多分辨率的适配:https://docs.unity3d.com/Manual/HOWTO-UIMultiResolution.html————————————————讨论一下Un_unity 分辨率

Android中添加书签(浏览器的收藏)_adroid收藏书籍怎么做-程序员宅基地

文章浏览阅读1.6k次。如何实现该功能呢?答案肯定是利用ContentResolver向浏览器的provider中添加相应书签项。 /* * 向浏览器中添加书签 * @param title 书签标题_adroid收藏书籍怎么做

ArrayList自定义排序_c# arraylist 自定义排序-程序员宅基地

文章浏览阅读2.9k次。完整版见https://jadyer.github.io/2013/10/16/arraylist-compare/_c# arraylist 自定义排序

随便推点

VMware-CentOS7网络配置-本机远程连接及访问互联网_vmware远程网页地址-程序员宅基地

文章浏览阅读1.5k次,点赞2次,收藏2次。查看本机IP配置,记录一下网关信息IP地址为:192.1683.2.152查看虚拟机配置设置虚拟机网路为桥接模式点击编辑—虚拟网络编辑器,DHCP设置记一下起始IP和结束IP,和网关信息更改设置:桥接模式中的选项不要选自动进入系统,配置网卡信息关注4个点ip地址:设置和宿主机同网段ip NETMASK:子网掩..._vmware远程网页地址

python爬取豆瓣top250电影榜-程序员宅基地

文章浏览阅读378次。前言beautiful soup库的一些应用,作为笔记代码爬取豆瓣top250电影榜单的一些信息,链接:https://movie.douban.com/top250import bs4import requestsdef find_pages(url): response = open_url(url) soup = bs4.BeautifulSoup(response.text,'html.parser') pages =soup.find('span', c

Python数据分析之使用pandas-datareader获取国家经济信息进行分析-程序员宅基地

文章浏览阅读4.1k次,点赞4次,收藏26次。pandas-datareader背景介绍当熟悉了Pandas的两个主要数据结构:Series和DataFrame之后,我们就可以使用pandas-datareader进行金融财经数据的导入和初步分析了。pandas-datareader是基于Python的专门从一系列的公开在线数据库获取数据的工具库,该接口在urllib3库基础上实现了以客户端身份访问在线数据库的各类金融财经股票数据。相关参考网站官网官方文档GIT源码安装pandas-datareader可以使用标准的pip进_pandas-datareader

xstream中为xml添加cdata标记-程序员宅基地

文章浏览阅读441次。xstream将对象输出为xml时,默认是无cdata标记的,下面的方法是添加cdata标签[code="java"]private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { r..._xml中sql添加cdata

ISTQB software testing certification sample question paper with answers_which of the following is not a mega project in an-程序员宅基地

文章浏览阅读898次。From http://www.softwaretestinghelp.com/istqb-software-testing-certification-sample-question-paper-2/1. Methodologies adopted whil_which of the following is not a mega project in ancient china?

linux 下串口通信程序(转载)_#define timeout_sec(buflen,baud) (buflen*20/baud+2-程序员宅基地

文章浏览阅读699次。现在正在做有关linux下串口通信的工作,在网上找到一篇文章关心com通信的,所以先记下来,看着比较靠谱,随后再试试.转载地址:https://www.cnblogs.com/yklevy/articles/6494726.html/*本程序符合GPL条约 MyCom.c */ #include // printf #i_#define timeout_sec(buflen,baud) (buflen*20/baud+2) //接收超时