Spring Boot高级应用(整合)

1 Spring Boot 与缓存

1.1 JSR107简介

  Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry 和 Expiry。
  CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
  CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
  Cache:是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
  Entry:是一个存储在Cache中的key-value对。
  Expiry: 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。
在这里插入图片描述

  并不是所有的缓存组件都提供了JSR107的实现,而当我们使用到这些组件时就需要自己来写实现,此时就会非常麻烦,Spring为解决上述问题就提供了它自己的缓存抽象。

1. 2 Spring缓存抽象

  Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口来统一不同的缓存技术。

  Spring缓存抽象同样支持使用JCache(JSR-107)注解简化开发。
  Cache接口为缓存的组件规范定义,包含缓存的各种操作集合,Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等

  每次调用需要进行缓存的方法时,Spring会检查指定参数的指定目标方法是否已经被调用过,如果调用过就直接从缓存中获取方法结果,如果没有就调用方法并缓存结果,下次调用就可以直接从缓存中获取。

使用Spring缓存抽象时我们需要关注以下两点:
  1、确定方法需要被缓存以及他们的缓存策略。
  2、从缓存中读取之前缓存存储的数据。

在这里插入图片描述

重要概念及注解
Cache  缓存接口,定义缓存操作,实现类有:RedisCache、EhCacheCache、ConcurrentMapCache等。
CacheManager  缓存管理器,管理各种缓存(Cache)组件。
@Cacheable  主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
@CacheEvict  清空缓存。
@CachePut  保证即调用方法,又更新缓存,常用于缓存更新。其与@Cacheable注解不同的是该注解标记的方法每次执行都会将结果放到缓存中。
  1、先调用目标方法(目标方法修改数据库数据)
  2、将目标方法的结果缓存起来(更新缓存)
@EnableCaching  开启基于注解的缓存。
keyGenerator  缓存数据时key生成策略。
serialize  缓存数据时value序列化策略。

1.3 使用缓存

  • 使用@EnableCaching开启基于注解的缓存
@SpringBootApplication
@EnableCaching //开启基于注解的缓存
public class Springboot01CacheApplication {
  • 标注缓存注解,这里以@Cacheable为例
@Cacheable //件方法返回的结果放入到缓存中
public Employee getEmp(Integer id){
     System.out.println("查询"+id+"号员工");
     Employee emp = employeeMapper.getEmpById(id);
     return emp;
 }

1.4 缓存的自动配置原理

  根据之前我们了解的Spring Boot的自动配置原理(参看Spring Boot入门中的第五章)我们可以知道缓存对应的配置类为CacheAutoConfiguration。

@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
		RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {

  我们主要来看@Import(CacheConfigurationImportSelector.class)的内容

/**
* {@link ImportSelector} to add {@link CacheType} configuration classes.
 */
static class CacheConfigurationImportSelector implements ImportSelector {

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		CacheType[] types = CacheType.values();
		String[] imports = new String[types.length];
		for (int i = 0; i < types.length; i++) {
			imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
		}
		return imports;
	}
}

  我们通过Debug追踪上述的selectImports方法可以发现该方法最终会返回11配置类分别是:

org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration
org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration【默认】

  这十一个配置类在默认情况下只有SimpleCacheConfiguration会生效,其给容器中注册了一个CacheManager(缓存管理器),这个缓存管理器是ConcurrentMapCacheManager,源码如下。

@Configuration
@ConditionalOnMissingBean(CacheManager.class) //判断条件,每个配置类都有,满足条件配置类才会生效。
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {

	private final CacheProperties cacheProperties;

	private final CacheManagerCustomizers customizerInvoker;

	SimpleCacheConfiguration(CacheProperties cacheProperties,
			CacheManagerCustomizers customizerInvoker) {
		this.cacheProperties = cacheProperties;
		this.customizerInvoker = customizerInvoker;
	}

	//给容器中添加一个缓存管理器
	@Bean
	public ConcurrentMapCacheManager cacheManager() {
		ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
		List<String> cacheNames = this.cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
			cacheManager.setCacheNames(cacheNames);
		}
		return this.customizerInvoker.customize(cacheManager);
	}

}

运行流程:

  1. 在标注有@Cacheable注解的方法运行之前,CacheManager先获取相应的缓存,根据cacheNames指定的名字查询Cache(缓存组件)并获取,如果第一次获取时并没有相应的缓存则会自动创建。
  2. 根据一个key值去Cache中查找缓存的内容,key是按照某种策略生成的,默认使用keyGenerator生成。keyGenerator是一个接口,这里默认使用的是其实现类SimpleKeyGenerator。

SimpleKeyGenerator生成key的默认策略;
  如果没有参数;key=new SimpleKey();
  如果有一个参数:key=参数的值
  如果有多个参数:key=new SimpleKey(params);

  1. 如果没有查到缓存就调用加了@Cacheable注解的目标方法。
  2. 将目标方法返回的结果,放进缓存中。

总结: @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存, 如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据.

1.5 缓存注解的使用

1.5.1 @Cacheable的使用

  在@Cacheable注解中有几个属性,其他注解也基本一致,下面将根据 @Cacheable的源码进行简单的介绍。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {

    /**
     * value和cacheNames都是用来指定缓存的名字的。
     * CacheManager管理多个Cache组件,而对缓存真正的CRUD操作是在Cache中进行的,
     * 每个缓存组件都有至少一个和其它组件不同的名字以方便识别管理。
     *
     * 必须指定至少一个缓存的名字,例如:
     * @Cacheable(value=”mycache”)
     * @Cacheable(value={”cache1”,”cache2”}
     */
    @AliasFor("cacheNames")
    String[] value() default {};
    
    @AliasFor("value")
    String[] cacheNames() default {};
    /**
     * 缓存的 key,可以为空,
     * 如果指定要按照 SpEL 表达式编写,
     * 如果不指定,则缺省按照方法的所有参数进行组合,
     * 即默认是使用方法参数的值。
     * 如何使用SpEL指定key将在下面给出介绍。
     */
    String key() default "";
    /**
     * key的生成器,可以自己指定key的生成器的组件id
     * 注意:key/keyGenerator:二选一使用;
     */
    String keyGenerator() default "";
    /**
     * cacheManager是缓存管理器,
     * 我们可以通过该属性指定要使用的缓存管理器
     * 根据前面的示意图可以看出会存在多个缓存管理器。
     */
    String cacheManager() default "";
    /**
     * cacheResolver:缓存解析器
     * 和上面的cacheManager缓存管理器作用一直,二者选择其一使用即可。
     */
    String cacheResolver() default "";
    /**
     *condition:缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,
     * 只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断。
     *
     * 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
     */
    String condition() default "";
    /**
     * 用于否决缓存的,不像condition,该表达式只在方法执行之后判断,
     * 此时可以拿到返回值result进行判断,条件为true不会缓存,fasle才缓存。
     * 例如:@Cacheable(value=”testcache”,unless=”#result == null”)
     */
    String unless() default "";
    /**
     * sync:指定缓存是否使用异步模式。
     */
    boolean sync() default false;
}
缓存中SpEL的使用
名字位置描述示例
methodNameroot object  当前被调用的方法名  #root.methodName
methodroot object  当前被调用的方法  #root.method.name
targetroot object  当前被调用的目标对象  #root.target
targetClassroot object  当前被调用的目标对象类  #root.targetClass
argsroot object  当前被调用的方法的参数列表  #root.args[0]
cachesroot object  当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache  #root.caches[0].name
argument nameevaluation context  方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引  #iban 、 #a0 、 #p0
resultevaluation context  方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false)  #result

1.5.2 @CachePut注解的使用

错误示范:

@CachePut(value = "emp")
public Employee updateEmp(Employee employee){
      System.out.println("updateEmp:"+employee);
      employeeMapper.updateEmp(employee);
      return employee;
  }

  上面的使用方式虽然能够修改数据库的数据并将返回结果放入缓存,但是由于放入缓存时默认使用的key是方法的参数,而查询方法中的参数和这里的如果不同则无法更新相应的缓存而只是新增了一条缓存,这也就造成修改数据无法实时更新到缓存中。
  为解决上述问题,我们可以在这里指定缓存的key为查询方法对应的key,如下:

@CachePut(value = "emp",key = "#result.id")
public Employee updateEmp(Employee employee){
     System.out.println("updateEmp:"+employee);
     employeeMapper.updateEmp(employee);
     return employee;
 }

1.5.3 @CacheEvict注解的使用

 @CacheEvict(value="emp",beforeInvocation = true,key = "#id",/*allEntries = true*/)
 public void deleteEmp(Integer id){
      System.out.println("deleteEmp:"+id);
      employeeMapper.deleteEmpById(id);
  }

@CacheEvict:缓存清除
  key:指定要清除的数据
  allEntries = true:指定清除这个缓存中所有的数据
  beforeInvocation = false:缓存的清除是否在方法之前执行

  beforeInvocation:默认值是false,此时是在方法执行后清除缓存,如果方法在执行中出现了错误则缓存不会被清除。当其值为true时代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除。

1.5.4 @Caching注解的使用

源码:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {

	Cacheable[] cacheable() default {};

	CachePut[] put() default {};

	CacheEvict[] evict() default {};

}

使用:

// @Caching 定义复杂的缓存规则
    @Caching(
            cacheable = {
                    @Cacheable(value="emp",key = "#lastName")
            },
            put = {
                    @CachePut(value="emp",key = "#result.id"),
                    @CachePut(value="emp",key = "#result.email")
            }
    )
    public Employee getEmpByLastName(String lastName) {

        return employeeMapper.getEmpByLastName(lastName);
    }

1.5.5 @CacheConfig的使用

@CacheConfig(cacheNames = "emp") //抽取缓存的公共配置,之后方法上的缓存注解就不需要再写这里定义了的属性了。
@Service
public class EmployeeService {

1.6 使用Redis

  Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

1.6.1 搭建Redis

  我们这里使用Docker来搭建Redis,相关命令如下:

[root@localhost ~]# docker pull redis
Using default tag: latest
Trying to pull repository docker.io/library/redis ... 
latest: Pulling from docker.io/library/redis
27833a3ba0a5: Already exists 
cde8019a4b43: Pull complete 
97a473b37fb2: Pull complete 
c6fe0dfbb7e3: Pull complete 
39c8f5ba1240: Pull complete 
cfbdd870cf75: Pull complete 
Digest: sha256:000339fb57e0ddf2d48d72f3341e47a8ca3b1beae9bdcb25a96323095b72a79b
Status: Downloaded newer image for docker.io/redis:latest
[root@localhost ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
docker.io/redis     latest              a55fbf438dfd        2 weeks ago         95 MB
docker.io/mysql     5.5                 4ee01d11a48b        2 weeks ago         205 MB
[root@localhost ~]# docker run -d -p 6379:6379--name myredis docker.io/redis
/usr/bin/docker-current: Invalid containerPort: 6379--name.
See '/usr/bin/docker-current run --help'.
[root@localhost ~]# docker run -d -p 6379:6379 --name myredis docker.io/redis
fffb1de7b937bd3f989618e6dc5c7b23e7cd349576491164b421919b8a3a666b
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
fffb1de7b937        docker.io/redis     "docker-entrypoint..."   11 seconds ago      Up 8 seconds        0.0.0.0:6379->6379/tcp   myredis

  在Windows下我们可以使用Redis Desktop Manager来管理Redis,这里不在介绍其下载与安装。

Redis的相关命令请翻阅官方资料:http://www.redis.cn/commands.html

1.6.2 整合Redis

  • 引入Redis的Starter
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 配置Redis
#指定Redis的主机地址
spring.redis.host=192.168.1.14
  • 使用Redisemplate操作Redis
public class Employee implements Serializable { // 一定要序列化
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot01CacheApplicationTests {

    @Autowired
    EmployeeMapper employeeMapper;

    @Autowired
    StringRedisTemplate stringRedisTemplate;  //操作k-v都是字符串的,因为对字符串操作较多,所以专门抽取出这个对象

    @Autowired
    RedisTemplate redisTemplate;  //k-v都是对象的

    @Autowired
    RedisTemplate<Object, Employee> empRedisTemplate; //修改序列化规则后的RedisTemplate

    /**
     * Redis常见的五大数据类型
     *  String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)
     *  stringRedisTemplate.opsForValue()[String(字符串)]
     *  stringRedisTemplate.opsForList()[List(列表)]
     *  stringRedisTemplate.opsForSet()[Set(集合)]
     *  stringRedisTemplate.opsForHash()[Hash(散列)]
     *  stringRedisTemplate.opsForZSet()[ZSet(有序集合)]
     */
    @Test
    public void test01(){
        //给redis中保存字符串
        stringRedisTemplate.opsForValue().append("msg","hello");
		/*
		相对应Redis的操作命令,我们都可以找到相应的方法:
		String msg = stringRedisTemplate.opsForValue().get("msg");
		stringRedisTemplate.opsForList().leftPush("mylist","1");
		stringRedisTemplate.opsForList().leftPush("mylist","2");
		*/
    }

    //测试保存对象,方法和stringRedisTemplate一致
    @Test
    public void test02(){
        Employee empById = employeeMapper.getEmpById(1);
        //将对象保存到Redis时,默认使用jdk序列化机制,序列化后的数据保存到redis中。
        redisTemplate.opsForValue().set("emp-01",empById);
    }
}

  使用默认的jdk序列化机制保存到Redis的数据不便于阅读,如下:
在这里插入图片描述
  我们更习惯于将数据以json的形式展示,为达到这一目的,我们可以修改序列化格则:

@Configuration
public class MyRedisConfig {

    @Bean
    public RedisTemplate<Object, Employee> empRedisTemplate(//该部分代码是参考Redis的自动配置类进行修改得到
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        //指定序列化的目标
        RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>();
        template.setConnectionFactory(redisConnectionFactory);
        //使用Jackson2JsonRedisSerializer序列化
        Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        template.setDefaultSerializer(ser);
        return template;
    }
}
 @Test
    public void test02(){
        Employee empById = employeeMapper.getEmpById(1);
        //想要将数据已json的形式保存到Redis,我们可以修改序列换规则
        empRedisTemplate.opsForValue().set("emp-01",empById);
    }

在这里插入图片描述

1.6.3 使用Redis进行缓存

  缓存的原理是通过缓存管理器来创建缓存组件,之后缓存组件来实际来操作缓存中的数据。当我们引入Redis后,RedisCacheConfiguration会起作用,而默认的SimpleCacheConfiguration则不再起作用了。

@Configuration
@ConditionalOnMissingBean(CacheManager.class) //在容器中没有缓存管理器时起作用
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {

  当我们引入Redis后,RedisCacheConfiguration会在容器中添加一个RedisCacheManager组件,此时默认的SimpleCacheConfiguration在判断时就不满足容器中不存在CacheManager的条件,便不会起作用。RedisCacheConfiguration的源码如下:

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisTemplate.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {

	private final CacheProperties cacheProperties;

	private final CacheManagerCustomizers customizerInvoker;

	RedisCacheConfiguration(CacheProperties cacheProperties,
			CacheManagerCustomizers customizerInvoker) {
		this.cacheProperties = cacheProperties;
		this.customizerInvoker = customizerInvoker;
	}

	@Bean
	public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
		RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
		cacheManager.setUsePrefix(true);
		List<String> cacheNames = this.cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
			cacheManager.setCacheNames(cacheNames);
		}
		return this.customizerInvoker.customize(cacheManager);
	}
}

  RedisCacheManager会帮我们创建 RedisCache来作为缓存组件,而RedisCache就是通过操作Redis来缓存数据的。
  由于Redis的相关配置已经自动配置完成,我们可以使用之前的接口来进行测试,所使用的注解是一致的。

@GetMapping("/emp/{id}")
 public Employee getEmployee(@PathVariable("id") Integer id){
     Employee employee = employeeService.getEmp(id);
     return employee;
 }

  我们可以发现,此时缓存到Redis的还是序列化之后的结果,因为我们引入redis的Starter后容器中默认的CacheManager变为RedisCacheManager,而RedisCacheManager操作redis的时候使用的是RedisTemplate<Object, Object>,RedisTemplate<Object, Object> 默认使用是jdk的序列化机制。
  如果我们想要将数据以json的形式存储到Redis中,我们就需要自定义CacheManager。

@Configuration
public class MyRedisConfig {

    @Bean
    public RedisTemplate<Object, Employee> empRedisTemplate(
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        template.setDefaultSerializer(ser);
        return template;
    }

    //CacheManagerCustomizers可以来定制缓存的一些规则
    @Primary  //将某个缓存管理器作为默认的
    @Bean
    public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> empRedisTemplate){
        RedisCacheManager cacheManager = new RedisCacheManager(empRedisTemplate);
        //使用前缀,默认会将CacheName作为key的前缀,如前缀是emp,缓存的数据id为1,那么在Redis中对应缓存的key就是emp:1
        cacheManager.setUsePrefix(true);

        return cacheManager;
    }
}

  此时,如果我们需要将部门的相关信息放到Redis中,且直接在Service接口处添加@Cacheable注解进行缓存,可以发现数据能够正确的放到Redis中,但是当从Redis取数据时会出错。因为在容器中使用的是employeeCacheManager,当取数据进行反序列化时Redis中的Department数据和employeeCacheManager中的指定的Employee泛型的字段将无法匹配,进而出现错误。
  为了解决上述问题,我们可以添加操作部门的CacheManager。

@Configuration
public class MyRedisConfig {

	//操作员工信息的RedisTemplate
    @Bean
    public RedisTemplate<Object, Employee> empRedisTemplate(
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        template.setDefaultSerializer(ser);
        return template;
    }
    //操作部门信息的RedisTemplate
    @Bean
    public RedisTemplate<Object, Department> deptRedisTemplate(
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Department> template = new RedisTemplate<Object, Department>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Department> ser = new Jackson2JsonRedisSerializer<Department>(Department.class);
        template.setDefaultSerializer(ser);
        return template;
    }

	//操作员工信息的RedisCacheManager 
    @Primary  //将某个缓存管理器作为默认的,存在多个缓存管理器时必须指定。
    @Bean
    public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> empRedisTemplate){
        RedisCacheManager cacheManager = new RedisCacheManager(empRedisTemplate);
        //使用前缀,默认会将CacheName作为key的前缀
        cacheManager.setUsePrefix(true);

        return cacheManager;
    }

	//操作部门信息的RedisCacheManager 
    @Bean
    public RedisCacheManager deptCacheManager(RedisTemplate<Object, Department> deptRedisTemplate){
        RedisCacheManager cacheManager = new RedisCacheManager(deptRedisTemplate);
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }
}

  容器中存在多个缓存管理器时我们要指定使用哪个缓存管理器,有两种方法。
  第一种方法是在类注解上指定。

//使用cacheManager指定要使用的缓存管理器
@CacheConfig(cacheNames = "emp",cacheManager = "employeeCacheManager") //抽取缓存的公共配置
@Service
public class EmployeeService {

  第二种方法是在方法注解上指定。

//使用cacheManager指定要使用的缓存管理器
@Cacheable(cacheNames = "dept",cacheManager = "deptCacheManager")
    public Department getDeptById(Integer id){

注意:
  1. 存在多个缓存管理器时一定要指定默认生效的缓存管理器,可以使用@Primary注解指定。
  2. 在多个缓存管理器的情况下,我们应该指定系统提供得能处理Object的缓存管理器。

//能处理Object的缓存管理器
@Bean
public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
	RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
	cacheManager.setUsePrefix(true);
	List<String> cacheNames = this.cacheProperties.getCacheNames();
	if (!cacheNames.isEmpty()) {
		cacheManager.setCacheNames(cacheNames);
	}
	return this.customizerInvoker.customize(cacheManager);
}

  上述部分我们都是使用的注解方式将缓存放到Redis中,我们还可以使用编码的方式将数据放入Redis。

@Service
public class DeptService {

    @Autowired
    DepartmentMapper departmentMapper;

    @Qualifier("deptCacheManager")
    @Autowired
    RedisCacheManager deptCacheManager; //1. 注入缓存管理器

    //2. 使用缓存管理器得到缓存,进行api调用
    public Department getDeptById(Integer id){
        System.out.println("查询部门"+id);
        Department department = departmentMapper.getDeptById(id);

        //获取某个缓存
        Cache dept = deptCacheManager.getCache("dept");
        dept.put("dept:1",department);

        return department;
    }
}

2 Spring Boot 与消息

  大多应用中,都可以通过消息服务中间件来提升系统异步通信、扩展解耦能力,下面将对几个应用场景进行介绍。
  传统项目的注册功能是采用同步调用的方式依次完成信息写入数据库、发送邮件、发送信息,如下图:
在这里插入图片描述
  很显然,在信息写入数据库之后,邮件和信息的发送并不用保证一定的先后顺序,此时我们可以使用多线程同步执行邮件和信息的发送。,如下图:
在这里插入图片描述
  即便使用了多线程,对用户的响应也依旧是在邮件和信息发送完成之后进行的,这依旧是同步的,此时我们可以使用消息队列,利用异步消息来优化系统,给用户更快的响应。
在这里插入图片描述
  我们还可以使用消息队列来进行应用解耦。在传统项目中,下定单功能需要调用库存接口,此时订单系统和库存系统直接就存在着耦合。
在这里插入图片描述
  我们可以使用消息队列来进行解耦,如下图:
在这里插入图片描述
  我们还可以通过消息队列实现流量削峰,比如在秒杀系统中,我们可以给消息队列定一个长度,用户请求按照请求的先后顺序进入队列,当请求占满队列之后就给之后的请求直接响应秒杀失败,而在队列中的请求便是秒杀成功的用户请求,此时可以根据队列中的信息来处理这些秒杀成功的请求。
在这里插入图片描述

2.1 概述

  消息服务中有两个重要概念:
  1. 消息代理(message broker):消息中间件的服务器
  2. 目的地(destination):当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
  消息队列主要有两种形式的目的地:
  1. 队列(queue):点对点消息通信(point-to-point)
  2. 主题(topic):发布(publish)/订阅(subscribe)消息通信

点对点式通讯机制:
   – 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列。
   – 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者,也就是说消息只能有一个发送者,可以有多个接收者但是当其中一个接收着收到消息后,该消息就会被销毁,其它接收者就无法再接受该消息。
发布订阅式通讯机制:
   – 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息,也就是说消息有一个发送者,有多个接收者,接收者都可以接收该消息。

两个消息服务的规范:
   1. JMS(Java Message Service):JAVA消息服务
     基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现。
   2. AMQP(Advanced Message Queuing Protocol):高级消息队列协议
     也是一个消息代理的规范,兼容JMS。RabbitMQ是AMQP的实现。

JMSAMQP
定义Java api网络线级协议
跨语言
跨平台
Model提供两种消息模型:
  1. Peer-2-Peer
  2. Pub/sub
提供了五种消息模型:
  1. direct exchange
  2. fanout exchange
  3. topic change
  4. headers exchange
  5. system exchange

  本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分.
支持消息类型多种消息类型:
  TextMessage
  MapMessage
  BytesMessage
  StreamMessage
  ObjectMessage
  Message (只有消息头和属性)
byte[],当实际应用时,有复杂的消息,可以将消息序列化后发送。
综合评价  JMS 定义了JAVA API层面的标准,在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。  AMQP定义了wire-level层的协议标准,天然具有跨平台、跨语言特性。

2.2 RabbitMQ简介

  RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。

核心概念
名称描述
Message  消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
Publisher  消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Exchange  交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别
Queue  消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
Binding  绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和Queue的绑定可以是多对多的关系。
Connection  网络连接,比如一个TCP连接。
Channel  信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
Consumer  消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
Virtual Host  虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
Broker  表示消息队列服务器实体。
  • 交互流程

  消息生产者(Publisher)将消息(Message)发送到消息代理服务器(Broker)中的虚拟主机(Virtual Host),虚拟主机中有很多自己的交换器(Exchange)和消息队列(Queue),当虚拟主机接收到消息之后会将该消息交给指定的交换器,交换器会根据消息的路由键(routing-key)判断将消息路由到那个队列中,路由规则由绑定关系(Binding)来表示。当消息到达消息队列之后,消费者就可以从消息队列中取得消息。消费者想要取得消息需要和队列建立网络连接(Connection),而为了节省资源在一个网络连接中开辟了很多的信道(Channel),从消息队列拿到的数据就通过这些信道交给消费者。
在这里插入图片描述

2.3 RabbitMQ运行机制

  AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了ExchangeBinding的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。

2.3.1 Exchange 类型

  Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键, headers 交换器和direct 交换器完全一致,但性能差很多,目前几乎用不到了,因此这里将不再进行介绍。

  • Direct Exchange

  消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
在这里插入图片描述

  • Fanout Exchange

  每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
在这里插入图片描述

  • Topic Exchange

  topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,*匹配一个单词。
在这里插入图片描述

2.4 RabbirMQ安装测试

  • 使用Docker下载镜像
docker pull rabbitmq:3-management

  tag带management的镜像是带有Web管理界面的。

  • 运行RabbitMQ镜像
docker images #查看本地镜像信息
docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq df6f26ea3e53 #根据镜像ID运行镜像
docker ps #查看运行中的镜像

  带Web管理界面的RabbitMQ需要向外暴露两个端口,其中5672是客户端与RabbitMQ进行通信的端口,15672是访问Web管理界面的端口。

  启动容器之后在浏览器中访问192.168.1.16:15672,可以看到如下界面。
在这里插入图片描述
  之后我们可以使用默认的guest账号密码来登录(账号和密码都是guest),登录之后将进入管理主页。

  • 测试RabbitMQ的消息路由机制
      1. 创建交换器
      在RabbitMQ管理界面选中Exchange标签。
    在这里插入图片描述
      之后找到Add a new exchange选项,点击,之后根据提示创建交换器。
    在这里插入图片描述
      同上创建另外两种类型的交换器,总共创建三个,如下:
    在这里插入图片描述
      2. 创建队列
      在RabbitMQ管理界面选中Queues标签。
    在这里插入图片描述
      之后找到Add a new queue选项,之后根据提示创建队列。
    在这里插入图片描述
      按照上述步骤创建如下的队列:
    在这里插入图片描述
      3. 绑定交换器和队列
      点击目标交换器。
    在这里插入图片描述
      绑定队列。
    在这里插入图片描述
      按照上述步骤将每一个交换器都和四个队列进行绑定。
      exchange.direct的绑定:
    在这里插入图片描述
      exchange.fanout的绑定
    在这里插入图片描述
      exchange.topic的绑定
    在这里插入图片描述
      完成绑定后我们就可以发消息进行测试。
    在这里插入图片描述
      之后找到Publish message选项
    在这里插入图片描述
      之后我们就可以在队列中查看发送的消息,由于direct机制是完全匹配,所以只有sk队列能够收到该消息。
    在这里插入图片描述

2.5 RabbitMQ整合

  • 引入依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

  在spring-boot-starter-amqp中引入了如下的依赖。

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-messaging</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.amqp</groupId>
		<artifactId>spring-rabbit</artifactId>
	</dependency>
</dependencies>
  • RabbitMQ的自动配置原理

  在SpringBoot中只要引入了相关场景依赖,它就会进行自动配置。根据自动配置原理,我们可以查找RabbitAutoConfiguration类来查看相关配置。
  1. 自动配置了连接工厂CachingConnectionFactory,部分源码如下:

@Bean
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties config)
		throws Exception {
	RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
	if (config.determineHost() != null) {
		factory.setHost(config.determineHost());
	}
	factory.setPort(config.determinePort());
	if (config.determineUsername() != null) {
		factory.setUsername(config.determineUsername());
	}
	if (config.determinePassword() != null) {
		factory.setPassword(config.determinePassword());
	}
	if (config.determineVirtualHost() != null) {
		factory.setVirtualHost(config.determineVirtualHost());
	}

  相关的配置信息都是从RabbitProperties中获取的,其封装了RabbitMQ的所有相关配置,部分源码如下:

@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties {

	/**
	 * RabbitMQ host.
	 */
	private String host = "localhost";

	/**
	 * RabbitMQ port.
	 */
	private int port = 5672;

	/**
	 * Login user to authenticate to the broker.
	 */
	private String username;

	/**
	 * Login to authenticate against the broker.
	 */
	private String password;

	/**
	 * SSL configuration.
	 */
	private final Ssl ssl = new Ssl();

	/**
	 * Virtual host to use when connecting to the broker.
	 */
	private String virtualHost;

	/**
	 * Comma-separated list of addresses to which the client should connect.
	 */
	private String addresses;

	/**
	 * Requested heartbeat timeout, in seconds; zero for none.
	 */
	private Integer requestedHeartbeat;

	/**
	 * Enable publisher confirms.
	 */
	private boolean publisherConfirms;

	/**
	 * Enable publisher returns.
	 */
	private boolean publisherReturns;

	/**
	 * Connection timeout, in milliseconds; zero for infinite.
	 */
	private Integer connectionTimeout;

	/**
	 * Cache configuration.
	 */
	private final Cache cache = new Cache();

	/**
	 * Listener container configuration.
	 */
	private final Listener listener = new Listener();

	private final Template template = new Template();

	private List<Address> parsedAddresses;

  我们可以在application.properties配置文件中对RabbitMQ进行简单的配置,内容如下:

#RabbitMQ的主机地址
spring.rabbitmq.host=192.168.1.16
#RabbitMQ的端口,默认就是5672,所以可以不配置
#spring.rabbitmq.port=5672
#RabbitMQ的登录用户名
spring.rabbitmq.username=guest
#RabbitMQ的登录密码
spring.rabbitmq.password=guest
#RabbitMQ的访问路径,默认值是/
#spring.rabbitmq.virtual-host=/

  从RabbitAutoConfiguration的源码我们可以看到其给容器中添加了RabbitTemplate组件负责和RabbitMQ进行交互;添加了AmqpAdmin组件负责提供系统管理功能。

  • 测试RabbitTemplate
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot02AmqpApplicationTests {

	@Autowired
	RabbitTemplate rabbitTemplate;  //1. 注入RabbitTemplate

	//2. 测试发送消息
	@Test
	public void contextLoads() {
		//Message需要自己构造一个;定义消息体内容和消息头
		//rabbitTemplate.send(exchage,routeKey,message);

		//object默认当成消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq;
		//rabbitTemplate.convertAndSend(exchage,routeKey,object);
		Map<String,Object> map = new HashMap<>();
		map.put("msg","这是第一个消息");
		map.put("data", Arrays.asList("helloworld",123,true));
		//对象被默认序列化以后发送出去
		rabbitTemplate.convertAndSend("exchange.direct","sk.news",map);
	}

	//3. 测试接受数据
	@Test
	public void receive(){
		Object o = rabbitTemplate.receiveAndConvert("sk.news");
		System.out.println(o.getClass());
		System.out.println(o);
	}
}

  通过测试结果可以发现上述代码可以成功将消息发送到RabbitMQ也可以成功从RabbitMQ获取到消息,但是此时在RabbitMQ的Web管理界面看到的消息是使用Java序列化后的内容,这是因为RabbitTemplate默认使用的是Java的序列化方式。
  在RabbitTemplate中有一个消息转换器MessageConverter,如下:

private volatile MessageConverter messageConverter = new SimpleMessageConverter();

  这个消息转化器指定了序列化规则,如果我们想要将数据以json的形式放入到RabbitMQ,我们可以来定制自己的MessageConverter。

@Configuration
public class MyAMQPConfig {

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

  上面是一个单播模式,我们还可以进行广播:

 @Test
 public void sendMsg() {
      //广播不用指定路由键
      rabbitTemplate.convertAndSend("exchange.fanout", "", new Book("红楼梦", "曹雪芹"));
    }

2.6 @RabbitListener和@EnableRabbit

  前面是使用RabbitTemplate来操作RabbitMQ发送和接收消息,在实际开发中我们需要一些监听场景,比如前面例子中的订单系统下单之后将相关信息放到消息队列,而库存系统则对该队列进行监听,一旦有新的订单内容库存系统就能够及时得到消息并进行相关的操作。
  下面将介绍如何使用@RabbitListener和@EnableRabbit注解进行对消息队列的监听。

  • 使用@RabbitListener进行监听
@Service
public class BookService {

    @RabbitListener(queues = "sk.news")
    public void receive(Book book){
        System.out.println("收到消息:"+book);
    }

    @RabbitListener(queues = "sk")
    public void receive02(Message message){//使用Message可以获取消息头信息,上面的只是能获取消息内容(Body)
        System.out.println(message.getBody());
        System.out.println(message.getMessageProperties());
    }
}
  • 开启注解模式
@EnableRabbit  //开启基于注解的RabbitMQ模式
@SpringBootApplication
public class Springboot02AmqpApplication {

2.7 AmqpAdmin管理组件的使用

  前面所有的测试都是在Exchange和Queue已经创建完成的基础上,像这样手动创建很明显过于繁琐,下面将介绍如何使用AmqpAdmin自动化管理交换器、队列和绑定规则。
  在自动配置类中已经往容器中添加了AmqpAdmin组件,我们可以直接自动注入,之后就可以使用了。

@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot02AmqpApplicationTests {
 
    @Autowired
    AmqpAdmin amqpAdmin;

    @Test
    public void createExchange() {
		//创建交换器,可以根据需求创建TopicExchange、DirectExchange等
		amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
		//创建消息队列
		amqpAdmin.declareQueue(new Queue("amqpadmin.queue",true));
        //创建绑定规则
        //参数分别是:
        //  目的地:指定要发送到哪个队列
        //  绑定类型:这里要绑定到队列,所以是Binding.DestinationType.QUEUE
        //  EXchange名:指定目标交换器
        //  路由键:指定路由键
		amqpAdmin.declareBinding(new Binding("amqpadmin.queue", Binding.DestinationType.QUEUE,"amqpadmin.exchange","amqp.haha",null));
    }
}

3 Spring Boot 与检索

  我们的应用经常需要添加检索功能,开源的 ElasticSearch 是目前全文搜索引擎的首选。他可以快速的存储、搜索和分析海量数据。Spring Boot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持。
  Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用多shard(分片)的方式保证数据安全,并且提供自动resharding的功能。

官方中文文档请参考:https://www.elastic.co/guide/cn/index.html
官方文档有详细的使用指南,可自行翻阅学习。

  一个 Elasticsearch 集群可以包含多个索引,相应的每个索引可以包含多个类型。 这些不同的类型存储着多个文档 ,每个文档又有多个属性。可以类比MySQL进行理解。
在这里插入图片描述

3.1 安装Elasticsearch

  这里是在Docker中安装Elasticsearch,步骤如下:

  • 1. 下载镜像
docker pull docker.io/elasticsearch
  • 2. 运行镜像

  Elasticsearch是使用Java写的,它默认初始占用两个G的堆内存空间,如果我们是在虚拟机上测且虚拟机的内存不够将无法运行成功。

docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 --name ES01 imageid
#-Xms256m:设置初始的堆内存大小为256兆
#-Xmx256m:设置最大使用的堆内存大小为256m
#elasticsearch默认外部通信使用9200端口,分布式下各个节点间的通信使用9300端口

-测试
  打开浏览器访问虚拟机的9200端口,当出现json数据响应便证明安装成功。

3.2 整合Elasticsearch

  SpringBoo支持Jest和SpringData ElasticSearch两种技术来和ES交互,其中Jest默认不生效,如果想让其生效需要导入jest的工具包(io.searchbox.client.JestClient),SpringData ElasticSearch默认生效但ES版本有可能不适配。

3.2.1 SpringBoot整合Jest操作ES

  • 引入依赖
<!-- https://mvnrepository.com/artifact/io.searchbox/jest -->
<dependency>
	<groupId>io.searchbox</groupId>
	<artifactId>jest</artifactId>
	<version>5.3.3</version>
</dependency>
  • 进行配置
spring.elasticsearch.jest.uris=http://192.168.1.16:9200
  • 在实体类中指定主键
public class Article {
	//指定主键
    @JestId
    private Integer id;
    private String author;
    private String title;
    private String content;
  • 测试JestClient
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot03ElasticApplicationTests {

	@Autowired
	JestClient jestClient;

	@Test
	public void contextLoads() {
		//1、给Es中索引(保存)一个文档;
		Article article = new Article();
		article.setId(1);
		article.setTitle("好消息");
		article.setAuthor("zhangsan");
		article.setContent("Hello World");

		//构建一个索引功能
		Index index = new Index.Builder(article).index("sk").type("news").build();

		try {
			//执行
			jestClient.execute(index);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	//测试搜索
	@Test
	public void search(){

		//查询表达式
		String json ="{\n" +
				"    \"query\" : {\n" +
				"        \"match\" : {\n" +
				"            \"content\" : \"hello\"\n" +
				"        }\n" +
				"    }\n" +
				"}";

		//构建搜索功能
		Search search = new Search.Builder(json).addIndex("sk").addType("news").build();

		//执行
		try {
			SearchResult result = jestClient.execute(search);
			System.out.println(result.getJsonString());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

更多操作请参考:https://github.com/searchbox-io/Jest/tree/master/jest

3.2.2 整合SpringData Elasticsearch

  SpringData Elasticsearch的相关配置类帮我们自动配置了一个连接ES的客户端Client,一个操作ES的ElasticsearchTemplate。
  客户端Client需要我们指定clusterNodes和clusterName两个节点信息。
  SpringData Elasticsearch除了为我们提供了ElasticsearchTemplate来操作ES外还给我们提供了编写一个 ElasticsearchRepository 子接口来操作ES的方式。
  如果版本不匹配,我们可以升级SpringBoot版本也可以安装对应版本的ES。

版本适配说明:https://github.com/spring-projects/spring-data-elasticsearch

  • 引入依赖
<!--SpringBoot默认使用SpringData ElasticSearch模块进行操作-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
  • 配置
spring.data.elasticsearch.cluster-name=elasticsearch
spring.data.elasticsearch.cluster-nodes=192.168.1.16:9301
  • 使用

共有两种用法,参考:https://github.com/spring-projects/spring-data-elasticsearch

  1. 编写一个 ElasticsearchRepository
//该注解标注数据存储到哪个索引、哪个类型下
@Document(indexName = "sk",type = "book")
public class Book {
    private Integer id;
    private String bookName;
    private String author;
//两个泛型分别是要存取数据的类型和要存取数据的主键类型
public interface BookRepository extends ElasticsearchRepository<Book,Integer> {

    //自定义方法的命名规则请参考官方文档
   public List<Book> findByBookNameLike(String bookName);
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot03ElasticApplicationTests {

	@Autowired
	BookRepository bookRepository;

	@Test
	public void test02(){
//		Book book = new Book();
//		book.setId(1);
//		book.setBookName("西游记");
//		book.setAuthor("吴承恩");
//		bookRepository.index(book); 索引(存储)数据,存储在哪个索引、类型下在实体类中用注解标注。
		for (Book book : bookRepository.findByBookNameLike("游")) {//匹配所有带“游”的文档。
			System.out.println(book);
		}
	}
}

更多使用请参考官方文档:https://docs.spring.io/spring-data/elasticsearch/docs/3.0.6.RELEASE/reference/html/

4 Spring Boot 与任务

4.1 异步任务

  在Java应用中,绝大多数情况下都是通过同步的方式来实现交互处理的;但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,其实,在Spring 3.x之后,就已经内置了@Async来完美解决这个问题。

两个注解:@EnableAysnc、@Aysnc

@Service
public class AsyncService {

    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("处理数据中...");
    }
}
@RestController
public class AsyncController {

    @Autowired
    AsyncService asyncService;

    @GetMapping("/hello")
    public String hello(){
        asyncService.hello();
        return "success";
    }
}

  上述代码在同步调用service中的hello方法时就需要等待三秒才能给用户响应,如果我们并不需要等待service中的hello方法执行完成,可以直接给用户响应,那么此时的等待就没有意义了,此时我们就可以使用异步任务,下面将在上述代码的基础上进行修改。

  1. 在可以异步执行的方法上添加@Async注解
 //告诉Spring这是一个异步方法
 @Async
 public void hello(){
  1. 在入口函数使用@EnableAsync开启异步注解功能
@EnableAsync  //开启异步注解功能
@SpringBootApplication
public class Springboot04TaskApplication {

4.2 定时任务

  项目开发中经常需要执行一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息。Spring为我们提供了异步执行任务调度的方式,提供TaskExecutor 、TaskScheduler 接口。

两个注解:@EnableScheduling、@Scheduled

@Service
public class ScheduledService {

    /**
     * second(秒), minute(分), hour(时), day of month(日), month(月), day of week(周几).
     * 0 * * * * MON-FRI
     *  【0 0/5 14,18 * * ?】 每天14点整,和18点整,每隔5分钟执行一次
     *  【0 15 10 ? * 1-6】 每个月的周一至周六10:15分执行一次
     *  【0 0 2 ? * 6L】每个月的最后一个周六凌晨2点执行一次
     *  【0 0 2 LW * ?】每个月的最后一个工作日凌晨2点执行一次
     *  【0 0 2-4 ? * 1#1】每个月的第一个周一凌晨2点到4点期间,每个整点都执行一次;
     */
   // @Scheduled(cron = "0 * * * * MON-SAT")
    //@Scheduled(cron = "0,1,2,3,4 * * * * MON-SAT")
   // @Scheduled(cron = "0-4 * * * * MON-SAT")
    @Scheduled(cron = "0/4 * * * * MON-SAT")  //每4秒执行一次
    public void hello(){
        System.out.println("hello ... ");
    }
}
@EnableScheduling //开启基于注解的定时任务
@SpringBootApplication
public class Springboot04TaskApplication {
  • cron表达式:
字段允许值允许的特殊字符
0-59, - * /
0-59, - * /
小时0-23, - * /
日期1-31, - * ? / L W C
月份1-12, - * /
星期0-7或SUN-SAT 0,7是SUN, - * ? / L C #
特殊字符代表含义
,枚举
-区间
*任意
/步长
?日/星期冲突匹配
L最后
W工作日
C和calendar联系后计算过的值
#星期,4#2,第2个星期四

4.3 邮件任务

  • 引入依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  • 配置
spring.mail.username=534096091@qq.com
spring.mail.password=gtstkoszjelabijb
spring.mail.host=smtp.qq.com
spring.mail.properties.mail.smtp.ssl.enable=true  #开启ssl连接,如果不设置会提示需要一个安全的链接

  由于配置文件中直接写邮箱密码不安全,我们可以使用授权码,下面将介绍如何获取授权码。
  以QQ邮箱为例,我们进入邮箱设置中的账户选项,然后找到“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”栏目,之后将栏目下的服务都开启。
在这里插入图片描述
  服务开启后点击生成授权码根据提示得到授权码,之后将该授权码填写到配置文件中的密码项即可。
  spring.mail.host的配置可以参考邮箱的提示,如图:
在这里插入图片描述
  点击红色方框的链接,然后找到相关SMTP服务器的配置,如下:
在这里插入图片描述

  • 测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot04TaskApplicationTests {

	@Autowired
	JavaMailSenderImpl mailSender; //1. 注入

	@Test //发送简单邮件
	public void contextLoads() {
		SimpleMailMessage message = new SimpleMailMessage();
		//邮件设置
		//设置邮件的标题
		message.setSubject("通知-今晚开会");
		//设置邮件的内容
		message.setText("今晚7:30开会");
		//设置邮件发往哪个地址,可以写多个
		message.setTo("17512080611@163.com");
		//设置邮件发送方
		message.setFrom("534096091@qq.com");

		mailSender.send(message);
	}

	@Test //发送复杂邮件
	public void test02() throws  Exception{
		//1、创建一个复杂的消息邮件
		MimeMessage mimeMessage = mailSender.createMimeMessage();
		//如果要上传文件就需要设置为true,默认是false
		MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);

		//邮件设置
		helper.setSubject("通知-今晚开会");
		//如果邮件内容是html则要设置为true,这样才会解析html,默认是false
		helper.setText("<b style='color:red'>今天 7:30 开会</b>",true);

		helper.setTo("17512080611@163.com");
		helper.setFrom("534096091@qq.com");

		//上传文件
		helper.addAttachment("1.jpg",new File("C:\\Users\\lfy\\Pictures\\Saved Pictures\\1.jpg"));
		helper.addAttachment("2.jpg",new File("C:\\Users\\lfy\\Pictures\\Saved Pictures\\2.jpg"));

		mailSender.send(mimeMessage);
	}
}

5 Spring Boot 与安全

  市面上有两个比较常用的安全框架,分别是Apache Shiro和Spring Security,我们在这里只对Spring Security进行介绍。
  Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型。他可以实现强大的web安全控制。对于安全控制,我们仅需引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理。

几个重要的类:
  WebSecurityConfigurerAdapter:自定义Security策略
  AuthenticationManagerBuilder:自定义认证策略
  @EnableWebSecurity:开启WebSecurity模式

  应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两个主要区域是Spring Security 的两个目标。

  • 引入依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 如果想在thymeleaf中使用security相关的属性,需要引入整合包 -->
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在引入依赖时一定要注意版本匹配问题!

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	<java.version>1.8</java.version>
	<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
	<thymeleaf-layout-dialect.version>2.3.0</thymeleaf-layout-dialect.version>
	<thymeleaf-extras-springsecurity4.version>3.0.2.RELEASE</thymeleaf-extras-springsecurity4.version>
</properties>
  • 界面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta charset="UTF-8">
		<title>Insert title here</title>
	</head>
	<body>
		<h1 align="center">欢迎登陆武林秘籍管理系统</h1>
		<hr>
		<div align="center">
			<form th:action="@{/userlogin}" method="post">
				用户名:<input name="user"/><br>
				密码:<input name="pwd"><br/>
				<input type="checkbox" name="remeber"> 记住我<br/>
				<input type="submit" value="登陆">
			</form>
		</div>
	</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<!-- 注意要引入thymeleaf和security与thymeleaf整合后的命名空间 -->
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<title>Insert title here</title>
	</head>
	<body>
		<h1 align="center">欢迎光临武林秘籍管理系统</h1>
		<!-- authorize表示授权isAuthenticated表示身份信息认证通过 -->
		<div sec:authorize="!isAuthenticated()">
			<h2 align="center">游客您好,如果想查看武林秘籍 <a th:href="@{/userlogin}">请登录</a></h2>
		</div>
		<div sec:authorize="isAuthenticated()">
			<!-- authentication:认证信息 -->
			<h2>
				<span sec:authentication="name"></span>,您好,您的角色有:
				<!-- 可以通过principal.authorities获取用户所有的角色信息 -->
				<span sec:authentication="principal.authorities"></span>
			</h2>
			<form th:action="@{/logout}" method="post">
				<input type="submit" value="注销"/>
			</form>
		</div>
		<hr>
		<!-- hasRole('VIP1')判断用户是否有VIP1角色 -->
		<div sec:authorize="hasRole('VIP1')">
			<h3>普通武功秘籍</h3>
			<ul>
				<li><a th:href="@{/level1/1}">罗汉拳</a></li>
				<li><a th:href="@{/level1/2}">武当长拳</a></li>
				<li><a th:href="@{/level1/3}">全真剑法</a></li>
			</ul>

		</div>
		<div sec:authorize="hasRole('VIP2')">
			<h3>高级武功秘籍</h3>
			<ul>
				<li><a th:href="@{/level2/1}">太极拳</a></li>
				<li><a th:href="@{/level2/2}">七伤拳</a></li>
				<li><a th:href="@{/level2/3}">梯云纵</a></li>
			</ul>

		</div>

		<div sec:authorize="hasRole('VIP3')">
			<h3>绝世武功秘籍</h3>
			<ul>
				<li><a th:href="@{/level3/1}">葵花宝典</a></li>
				<li><a th:href="@{/level3/2}">龟派气功</a></li>
				<li><a th:href="@{/level3/3}">独孤九剑</a></li>
			</ul>
		</div>
	</body>
</html>
  • 编写配置类


@EnableWebSecurity //使用该注解开启Security
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
	
	//无论是定义授权规则还是定义认证规则都是使用的configure,只是参数不同
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //定制请求的授权规则,如/level1/**请求的授权用户有VIP1,即只有具有VIP1角色的用户才能访问该请求
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("VIP1")
                .antMatchers("/level2/**").hasRole("VIP2")
                .antMatchers("/level3/**").hasRole("VIP3");

        //开启自动配置的登陆功能,效果,如果没有登陆,没有权限就会来到登陆页面
        http.formLogin().usernameParameter("user").passwordParameter("pwd")
                .loginPage("/userlogin");
        //1、/login来到登陆页
        //2、重定向到/login?error表示登陆失败
        //3、更多详细规定
        //4、默认post形式的 /login代表处理登陆
        //5、一但定制loginPage;那么 loginPage的post请求就是登陆


        //开启自动配置的注销功能。
        http.logout().logoutSuccessUrl("/");//logoutSuccessUrl指定注销成功后跳转的请求
        //1、访问 /logout 表示用户注销,清空session
        //2、注销成功会返回 /login?logout 页面;

        //开启记住我功能
        http.rememberMe().rememberMeParameter("remeber"); //rememberMeParameter方法指定记住我选择框的参数名
        //登陆成功以后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
        //点击注销会删除cookie

    }

    //定义认证规则
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //super.configure(auth);
		//这里为测试方便从内存获取用户数据,实际开发中应该是从数据库中获取,auth中有相应方法。
        auth.inMemoryAuthentication()
                .withUser("zhangsan").password("123456").roles("VIP1","VIP2")
                .and()
                .withUser("lisi").password("123456").roles("VIP2","VIP3")
                .and()
                .withUser("wangwu").password("123456").roles("VIP1","VIP3");

    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值