spring-boot进阶

本文为第二篇
主要是中间件与部署方面的内容

主要参考:
spring-boot-learning

redis

简介

Spring Boot 提供了对 Redis 集成的组件包:spring-boot-starter-data-redis,它依赖于 spring-data-redis 和 lettuce。Spring Boot 1.0 默认使用的是 Jedis 客户端,2.0 替换成了 Lettuce

1
Lettuce → Spring Data Redis → Spring Data → spring-boot-starter-data-redis
  1. Lettuce:是一个可伸缩线程安全的 Redis 客户端,多个线程可以共享同一个 RedisConnection,它利用优秀 Netty NIO 框架来高效地管理多个连接
  2. Spring Data Redis:是 Spring Data 项目中的一个主要模块,实现了对 Redis 客户端 API 的高度封装,使对 Redis 的操作更加便捷。
  3. Spring Data:是 Spring 框架中的一个主要项目,目的是为了简化构建基于 Spring 框架应用的数据访问,包括非关系数据库、Map-Reduce 框架、云数据服务等,另外也包含对关系数据库的访问支持。

安装

与postgres的安装相同,在docker-compse.yml中添加:

1
2
3
4
5
6
7
8
9
redis:
image: redis:4.0.13
container_name: redis
restart: always
command: --appendonly yes
ports:
- 6379:6379
volumes:
- ./redis_data:/data

然后docker-compose up -d 等待即可

使用

  • 依赖

    1
    2
    3
    4
    5
    6
    7
    8
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    </dependency>

    引入 commons-pool 2 是因为 Lettuce 需要使用 commons-pool 2 创建 Redis 连接池。

  • 配置

    配置文件application.properties:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # Redis 数据库索引(默认为 0)
    spring.redis.database=0
    # Redis 服务器地址
    spring.redis.host=localhost
    # Redis 服务器连接端口
    spring.redis.port=6379
    # Redis 服务器连接密码(默认为空)
    spring.redis.password=
    # 连接池最大连接数(使用负值表示没有限制) 默认 8
    spring.redis.lettuce.pool.max-active=8
    # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
    spring.redis.lettuce.pool.max-wait=-1
    # 连接池中的最大空闲连接 默认 8
    spring.redis.lettuce.pool.max-idle=8
    # 连接池中的最小空闲连接 默认 0
    spring.redis.lettuce.pool.min-idle=0

    配置类RedisConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Configuration
    @EnableCaching
    public class RedisConfig extends CachingConfigurerSupport{

    @Bean
    public KeyGenerator keyGenerator() {
    return new KeyGenerator() {
    @Override
    public Object generate(Object target, Method method, Object... params) {
    StringBuilder sb = new StringBuilder();
    sb.append(target.getClass().getName());
    sb.append(method.getName());
    for (Object obj : params) {
    sb.append(obj.toString());
    }
    return sb.toString();
    }
    };
    }
    }

    以上配置了主键生成的策略,如不配置会默认使用参数名作为主键,这里添加了类名+函数名+参数名

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class TestRedisTemplate {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testString() {
    redisTemplate.opsForValue().set("neo", "ityouknow");
    Assert.isTrue("ityouknow".equals(redisTemplate.opsForValue().get("neo")), "测试出错");
    }
    }

session

  • 简介
    使用redis来存储http的sessionId信息,这个spring boot对这块都进行了封装。封装到牙齿的感觉。

  • 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    </dependency>
  • 配置

    application.properties:
    这里的配置就是redis的配置即可

    配置类:

    1
    2
    3
    4
    @Configuration
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
    public class SessionConfig {
    }

    maxInactiveIntervalInSeconds: 设置 Session 失效时间

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @PostMapping(value = "/login")
    public String login (HttpServletRequest request,String userName,String password){
    String msg="logon failure!";
    User user= userRepository.getByUserName(userName);
    if (user!=null && user.getPassword().equals(password)){
    request.getSession().setAttribute("user",user);
    msg="login successful!";
    }
    return msg;
    }

    @GetMapping(value = "/logout")
    public String logout (HttpServletRequest request){
    request.getSession().removeAttribute("user");
    return "loginout successful!";
    }
  • 问题

    这里,如果有登录的要求,需要在每个路由中都去判断是否登录,这样在写的时候会比较费劲,适合使用spring切片的方式来进行登录与否的验证

缓存

简介

这里的缓存,是将从关系型数据库中读取数据,变成先从缓存中读取,若没有则从关系型数据库中读取,这里用的缓存,也是用的redis。spring对这些操作也进行了封装,只需要用几个注解,就可以完成

@Cacheable是读取完之后,再次读取时候会做缓存,主要用在Get上
@CachePut,这个是数据发生变化之后,将更新变化数据,主要用在Put上
@CacheEvict,这个是数据删除之后,同时清除缓存

他们共有的属性:value:缓存的名称;key:缓存的key值(redis的hash结构);condition:在什么条件上做缓存
CacheEvict,还有两个属性,allEntries:这个是true清除缓存的所有内容,不是删hash的key,而是删这个整个对象,默认是false;beforeInvocation:这个是值true指在执行Delele前,清除缓存,false是之后清除,默认是false;

依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

使用

  • @Cacheable:读取数据之后,缓存数据

    1
    2
    3
    4
    5
    6
    @GetMapping(value = "user/{id}")
    @Cacheable(value="user", key="#id")
    public User getId(@PathVariable("id") Long id){
    User user = this.userRepository.getById(id);
    return user;
    }
  • @CachePut:数据修改之后,更新缓存数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @ApiOperation(value ="修改用户信息", notes ="根据传参修改用户")
    @PutMapping(value = "user")
    @CachePut(value="user",key="#id")
    public User modify(String nickName, Long id) throws Exception {
    int r = this.userRepository.upateNickNameById(nickName, id);
    if (r < 0){
    throw new Exception();
    }

    User u = this.userRepository.getById(id);
    return u;
    }
  • @CacheEvict:数据删除之后,删除缓存数据

    1
    2
    3
    4
    5
    6
    7
    @DeleteMapping(value = "user/{id}")
    @CacheEvict(value="user", key="#id")
    public BaseResult<String> Delete (@PathVariable("id") Long id){
    this.userRepository.deleteById(id);

    return BaseResult.success();
    }

RabbitMQ

简介

借鉴

  • 说RabbitMQ,要先介绍AMQP
    AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种客户端

  • 基本概念
    通常我们谈到队列服务,会有三个概念:生产者(Producer)、队列(Queue)、消费者(Consumer)。RabbitMQ 在这个基本概念之上,多做了一层抽象,在发消息者和队列之间加入了交换机(Exchange)。这样发消息者和队列就没有直接联系,转而变成发消息者把消息给交换器,交换器根据调度策略再把消息再给队列。
    rabbitmq流转图.png

    1. 生产者发送消息的时候指定RoutingKey,然后消息被发送到Exchange
    2. Exchange根据一些列规则(BindingKey)将消息路由到指定的队列中
    3. 消费者从队列中消费消息
  • 交换机
    交换机有4种类型:Direct Exchange(默认)、Topic Exchange、Headers Exchange、Fanout Exchange
    前3种都是消息队列,虽然交换机与队列之间的映射机制不同(路由方式),但对于一条消息,只有一个生产者与消费者。Fanout是广播,对于一条消息可以有多个消费者。
    Direct Exchange把消息路由到BindingKey和RoutingKey完全匹配的队列中
    Topic Exchange把消息路由到BindingKey可以模糊匹配的队列中,Binding Key可以认为是消息名称,Binding Key是一种模糊匹配,代表一类消息,其中”*”表示一个单词,”#”表示多个单词(包括0,1)
    Fanout Exchange把消息发送与该交换机绑定的所有队列上,不光BindingKey,用来广播

准备

  • 安装

    docker-compose.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    rabbitmq:
    image: rabbitmq:3-management
    container_name: rabbitmq
    restart: always
    environment:
    RABBITMQ_DEFAULT_USER: "rabbitmq"
    RABBITMQ_DEFAULT_PASS: "rabbitmq"
    ports:
    - 15672:15672
    - 5672:5672
    volumes:
    - ./rabbitmq_data:/var/lib/rabbitmq
  • 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  • 配置

    1
    2
    3
    4
    spring.rabbitmq.host=192.168.0.1
    spring.rabbitmq.port=5672
    spring.rabbitmq.username=rabbitmq
    spring.rabbitmq.password=rabbitmq

使用

  • Direct Exchange
    spring boot默认使用时,应该自己封装了exchange,只需定义routeKey即可
    可以发送对象,二进制发送,需要实现Serializable

    1. 定义Queue

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Configuration
      public class RabbitConfig {

      @Bean
      public Queue Queue() {
      return new Queue("hello");
      }

      }
  1. 定义生产者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class HelloSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send() {
    String context = "hello " + new Date();
    System.out.println("Sender : " + context);
    this.rabbitTemplate.convertAndSend("hello", context);
    }

    }
  2. 定义接收者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    @RabbitListener(queues = "hello")
    public class HelloReceiver {

    @RabbitHandler
    public void process(String hello) {
    System.out.println("Receiver : " + hello);
    }

    }
  • topic exchange
    需要定义queue、exchange,并且binding queue与exchange

    1.定义queue、exchange

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    @Configuration
    public class TopicRabbitConfig {

    final static String message = "topic.message"; // queue名
    final static String messages = "topic.messages"; // queue名

    //定义队列
    @Bean
    public Queue queueMessage() {
    return new Queue(TopicRabbitConfig.message);
    }

    @Bean
    public Queue queueMessages() {
    return new Queue(TopicRabbitConfig.messages);
    }

    //交换机
    @Bean
    TopicExchange exchange() {
    return new TopicExchange("exchange");
    }

    //将队列和交换机绑定
    @Bean
    Binding bindingExchangeMessage(Queue queueMessage, TopicExchange exchange) {
    return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
    }

    @Bean
    Binding bindingExchangeMessages(Queue queueMessages, TopicExchange exchange) {
    return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
    }
    }

    注意定义queue时使用,只是queue名,在接收者时用来指定某个queue使用。
    后边binding时候使用with才是bindingKey

    1. 发送者

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public void send1() {
      String context = "hi, i am message 1";
      System.out.println("Sender : " + context);
      this.rabbitTemplate.convertAndSend("exchange", "topic.message", context);
      }

      public void send2() {
      String context = "hi, i am messages 2";
      System.out.println("Sender : " + context);
      this.rabbitTemplate.convertAndSend("exchange", "topic.messages", context);
      }

    这个“exchange”是指明交换机,topic.message与top.messages是routeKey

    1. 接收者

    接收者同上,没有改变。

  • fanout
    fanout与topic类似,定义部分稍有不同,生产者与消费者使用相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @Configuration
    public class FanoutRabbitConfig {

    //定义队列
    @Bean
    public Queue AMessage() {
    return new Queue("fanout.A");
    }

    @Bean
    public Queue BMessage() {
    return new Queue("fanout.B");
    }

    //定义交换机
    @Bean
    FanoutExchange fanoutExchange() {
    return new FanoutExchange("fanoutExchange");
    }

    //分部进行绑定
    @Bean
    Binding bindingExchangeA(Queue AMessage,FanoutExchange fanoutExchange) {
    return BindingBuilder.bind(AMessage).to(fanoutExchange);
    }

    @Bean
    Binding bindingExchangeB(Queue BMessage, FanoutExchange fanoutExchange) {
    return BindingBuilder.bind(BMessage).to(fanoutExchange);
    }
    }

Elasticsearch

简介

elasticsearch也借鉴了一篇文章,可惜是微信的,链接有signature,访问会受限,这里没法给出原链接了。

  • 是什么
    Elasticsearch 是一个 real-time, distributed storage, search engine

  • 特性
    Elasticsearch是专门做搜索的,我们在用搜狗、google时候,输入关键字没那么匹配都可以搜索的出来,用elaticsearch就可以做到:

    1. Elasticsearch对模糊搜索非常擅长(模糊查询)
    2. 没有那么准确的关键字也能搜出相关的结果(相关性查询)
    3. 从Elasticsearch搜索到的数据可以根据评分过滤掉大部分的,只要返回评分高的给用户就好了(原生就支持排序)
  • 简单介绍原理
    分词: 写入到Elasticsearch的数据会进行分词
    存储:
    elasticsearch数据结构.png
    从右往左看:position是记录所在的位置,term dictionary是分词字典, term index是为了快速查找分词所做的索引

  • 基本术语

关系型数据库 Elasticsearch
Table Index(Type)
Row Document
Column Filed
Schema Mapping
SQL DSL

这个术语与mongo的术语基本相同,mongo中与Table对应的概念是Collection。所以在分布式存储这块,elasticsearch应该用的NoSQL,至于与mongo的关系还需要探索一下

准备

  • 安装
    借鉴1
    借鉴2

    docker-compose.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    version: '3'
    services:
    es1:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.8.6
    container_name: es1
    environment:
    - node.name=es01
    - cluster.name=es-cluster
    - bootstrap.memory_lock=true
    - "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
    ulimits:
    nproc: 65535
    memlock:
    soft: -1
    hard: -1
    volumes:
    - ./elasticsearch_data/es1_data:/usr/share/elasticsearch/data
    ports:
    - 9200:9200
    - 9300:9300
    networks:
    - esnet

    es2:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.8.6
    container_name: es2
    environment:
    - node.name=es02
    - cluster.name=es-cluster
    - bootstrap.memory_lock=true
    - "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
    - "discovery.zen.ping.unicast.hosts=es1"
    ulimits:
    nproc: 65535
    memlock:
    soft: -1
    hard: -1
    volumes:
    - ./elasticsearch_data/es2_data:/usr/share/elasticsearch/data
    ports:
    - 9201:9200
    - 9301:9300
    networks:
    - esnet

    kibana:
    image: docker.elastic.co/kibana/kibana:6.8.6
    container_name: kibana
    environment:
    SERVER_NAME: localhost
    ELASTICSEARCH_URL: http://es1:9200
    ports:
    - 5601:5601
    ulimits:
    nproc: 65535
    memlock:
    soft: -1
    hard: -1
    networks:
    - esnet

    networks:
    esnet:

    这里ulimits是对资源的限制,nproc是线程的个数,memlock
    浏览器打开http://localhost:5601即可监控集群状态

    在启动过程中出现了max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]意思是最大的虚拟内存太小了,要求扩容,解决方案:
    修改本机的 /etc/sysctl.conf,在文件末尾增加:vm.max_map_count=262144
    sysctl -p生效

    由于本机的内存限制,使用过程中,将es2节点删去之后进行的实验

  • 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
  • 配置

    1
    2
    3
    spring.data.elasticsearch.cluster-name=es-cluster
    # 集群节点地址列表,用逗号分隔
    spring.data.elasticsearch.cluster-nodes=localhost:9300

使用

spring-boot下使用elasticsearch与JPA很相似,因为它们都是在从相同的父类下继承而来

  • 定义对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Document(indexName = "customer", type = "customer", shards = 1, replicas = 0, refreshInterval = "-1")
    public class Customer {
    @Id
    private String id;
    private String userName;
    private String address;
    private int age;
    //省略部分 getter/setter
    }

    @Document 注解会对实体中的所有属性建立索引
    indexName = “customer” 表示创建一个名称为 “customer” 的索引
    type = “customer” 表示在索引中创建一个名为 “customer” 的 type
    shards = 1 表示只使用一个分片
    replicas = 0 表示不使用复制
    refreshInterval = “-1” 表示禁用索引刷新

  • repository

    1
    2
    3
    4
    5
    public interface CustomerRepository extends ElasticsearchRepository<Customer, String> {
    public List<Customer> findByAddress(String address);
    public Customer findByUserName(String userName);
    public int deleteByUserName(String userName);
    }
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    @SpringBootTest
    public class CustomerRepositoryTest {
    @Autowired
    private CustomerRepository repository;

    @Test
    public void saveCustomers() {
    repository.save(new Customer("Alice", "北京",13));
    repository.save(new Customer("Bob", "北京",23));
    repository.save(new Customer("Hibe", "上海",30));
    }

    @Test
    public void fetchAllCustomers() {
    for (Customer customer : repository.findAll()) {
    System.out.println(customer);
    }
    }

    @Test
    public void deleteCustomers() {
    repository.deleteByUserName("Hibe");
    }

    @Test
    public void updateCustomers() {
    Customer customer= repository.findByUserName("Bob");
    customer.setAddress("北京市海淀区");
    repository.save(customer);
    Customer xcustomer=repository.findByUserName("Bob");
    System.out.println(xcustomer);
    }

    @Test
    public void fetchIndividualCustomers() {
    for (Customer customer : repository.findByAddress("北京")) {
    System.out.println(customer);
    }
    }
    }

Quartz

简介

Quartz其实就是linux的cron,能够定时执行某些任务,spring boot内部也集成了一个简单的定时任务调度。说怎么实现原理的话,可能有2种,一种的用定时器来实现,另一种封装系统的cron来实现,在内核层都应该是软中断。

spring boot自带

  1. application上增加@EnableScheduling

    1
    2
    3
    4
    5
    6
    7
    8
    @Spring BootApplication
    @EnableScheduling
    public class Application {

    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }
  2. 对应函数上增加配置@Scheduled(),cron与fixedRate都可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Component
    public class SchedulerTask {

    private int count=0;

    @Scheduled(cron="*/6 * * * * ?")
    private void process(){
    System.out.println("this is scheduler task runing "+(count++));
    }

    @Scheduled(fixedRate = 6000)
    public void reportCurrentTime() {
    System.out.println("现在时间:" + dateFormat.format(new Date()));
    }
    }

    cron:秒、时、分,日、月、星期、年; *表示每, /表示步长,也就是间隔, ?表示不确定(因为周与日是不会共存的,所以选择一种,另外一个就用?来表示)

Quartz

  • 4个基本概念

    Job: 一个接口,表示要执行的任务
    JobDetail: Job的实例
    Trigger: 什么情况下触发,类似cron或者fixedRate
    Scheduler: 调度器,包含这Job与Trigger

  • 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
  • 使用

    1. 定义job,需要实现Job接口

      1
      2
      3
      4
      5
      6
      public class ScheduledJob implements Job {
      @Override
      public void execute(JobExecutionContext context) throws JobExecutionException {
      System.out.println("schedule job1 is running ...");
      }
      }
    2. 调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private void scheduleJob(Scheduler scheduler) throws SchedulerException{
    // 这里生成JobDetail
    JobDetail jobDetail = JobBuilder.newJob(ScheduledJob.class) .withIdentity("job", "group1").build();

    // 这里生成Trigger
    CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/6 * * * * ?");
    CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("trigger", "group") .withSchedule(scheduleBuilder).build();

    // 用Scheduler,注册JobDetail与Trigger
    scheduler.scheduleJob(jobDetail,cronTrigger);
    }
    1. 调用
    1
    2
    3
    4
    public void scheduleJobs() throws SchedulerException {
    Scheduler scheduler = schedulerFactoryBean.getScheduler();
    scheduleJob(scheduler);
    }
    1. 启动时开始运行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Component
    public class MyStartupRunner implements CommandLineRunner {

    @Autowired
    public CronSchedulerJob scheduleJobs;

    @Override
    public void run(String... args) throws Exception {
    scheduleJobs.scheduleJobs();
    System.out.println(">>>>>>>>>>>>>>>定时任务开始执行<<<<<<<<<<<<<");
    }
    }

邮件系统

  • 简介

    sendmail.png
    发送的时候使用SMTP协议,服务器推送时候使用POP3或者IMAP协议

  • 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring.mail.host=smtp.163.com //邮箱服务器地址
    spring.mail.username=xxx@oo.com //用户名
    spring.mail.password=xxyyooo //密码
    spring.mail.default-encoding=UTF-8

    //超时时间,可选
    spring.mail.properties.mail.smtp.connectiontimeout=5000
    spring.mail.properties.mail.smtp.timeout=3000
    spring.mail.properties.mail.smtp.writetimeout=5000

    这里的密码需要注意,并不是邮箱的密码,而是开启POP3的客户端授权码

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @Component
    public class MailService{

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String from;

    @Override
    public void sendSimpleMail(String to, String subject, String content) {
    SimpleMailMessage message = new SimpleMailMessage();
    message.setFrom(from);
    message.setTo(to);
    message.setSubject(subject);
    message.setText(content);

    try {
    mailSender.send(message);
    logger.info("简单邮件已经发送。");
    } catch (Exception e) {
    logger.error("发送简单邮件时发生异常!", e);
    }

    }
    }

安全

  • 简介
    Spring Security,是封装401与403的模块。401是未登录问题,这个感觉还不错;403是无权限问题,这个对复杂的,感觉还差一点

  • 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    加上这个依赖,默认所有的路由,都需要登录认证

  • 登录认证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .antMatchers("/", "/home").permitAll()
    .anyRequest().authenticated()
    .and()
    .formLogin()
    // .loginPage("/login")
    .permitAll()
    .and()
    .logout()
    .permitAll()
    .and()
    .csrf()
    .ignoringAntMatchers("/logout");

    }
    }

    这个是统一配置,哪些路由需要登录才能访问,哪个可以直接访问
    antMatchers("/", "/home").permitAll(),这个意思是”/“,”/home”可以直接访问
    anyRequest().authenticated(),这个是其他的都需要登录认证
    后边的几个是对登录与退出说的,对于Restful,可以省略

  • 权限认证
    权限认证,首先要告诉spring boot有哪些角色、与成员,然后可以在config中确定哪些路由需要什么样的角色、权限,或者可以直接在函数上注解权限或角色。

    1. 配置角色

      在SecurityConfig中增加:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Autowired
      public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      auth.inMemoryAuthentication()
      .passwordEncoder(new BCryptPasswordEncoder())
      .withUser("user")
      .password(new BCryptPasswordEncoder()
      .encode("123456")).roles("USER")
      .and()
      .withUser("admin")
      .password(new BCryptPasswordEncoder()
      .encode("admin")).roles("ADMIN", "USER");
      }

      这里是直接通过代码写进去的,没有借鉴意义,只作演示用途

    2. 统一配置路由方式

    同登录认证,在SecurityConfig中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .antMatchers("/resources/**", "/").permitAll()
    .antMatchers("/admin/**").hasRole("ADMIN")
    .antMatchers("/content/**").access("hasRole('ADMIN') or hasRole('USER')")
    .anyRequest().authenticated()
    .and()
    .formLogin()
    // .loginPage("/login")
    .permitAll()
    .and()
    .logout()
    .permitAll()
    .and()
    .csrf()
    .ignoringAntMatchers("/logout");
    }

    这里antMatchers("/admin/**").hasRole("ADMIN"),/admin路由,需要有’ADMIN’角色才可以访问,
    antMatchers("/content/**").access("hasRole('ADMIN') or hasRole('USER')"),/content路由,’ADMIN’,’USER’角色都可以访问

    1. 在函数注解方式

      使用PreAuthorize()来配置,使用之前,需要先打开PreAuthorize的开关,否则不起作用
      在SecurityConfig上,增加@EnableGlobalMethodSecurity(prePostEnabled = true) 注解即可。

      1
      2
      3
      4
      5
      @PreAuthorize("hasAuthority('ADMIN')")
      @RequestMapping("/admin")
      public String admin() {
      return "admin";
      }
    2. 关于权限认证

    权限认证,是可以通过角色,也可以通过权限。如果角色固定,且较少的话,通过角色会好一些;如果角色不定,用权限来做会更佳一些。

监控

简介

对应用的监控,spring boot给出的方案是Actuator,它的监控分成两类:原生端点和用户自定义端点。自定义端点主要是指扩展性,用户可以根据自己的实际应用,定义一些比较关心的指标,在运行期进行监控。
原生端点又可以分成三类:
应用配置类,可以查看应用在运行期的静态信息,例如自动配置信息、加载的 springbean 信息、yml 文件配置信息、环境信息、请求映射信息;
度量指标类,主要是运行期的动态信息,如堆栈、请求连、一些健康指标、metrics 信息等;
操作控制类,主要是指 shutdown,用户可以发送一个请求将应用的监控功能关闭。

Actuator的提供了很多接口常用的:

接口 路径 描述
health /health 报告应用程序的健康指标
info /info 获取应用程序的定制信息,这些信息由 info 打头的属性提供
env /env 获取全部环境属性
metrics /metrics 报告各种应用程序度量信息,比如内存用量和 HTTP 请求计数
heapdump /heapdump dump 一份应用的 JVM 堆信息
threaddump /threaddump 获取线程活动的快照
httptrace /httptrace 显示 HTTP 足迹,最近 100 个 HTTP request/repspons(未成功)
sessions /sessions 如果我们使用了 Spring Session 展示应用中的 HTTP Sessions 信息
logfile /logfile 返回 log file 中的内容(如果 logging.file 或者 logging.path 被设置)

另外,在Actuator基础上,构建了分布式的Admin,来收集各应用的Actuator信息,进行汇总显示

Actuator

  • 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  • 配置

    开启哪些监控,可以进行配置
    management.endpoints.web.exposure.include=*,默认开启health与info,通过这个设置,开启所有
    management.endpoints.web.base-path=/manage, 默认base路径是/actuator/*,这个可以进行配置
    management.endpoint.health.show-details=always,对health监控显示的内容进行配置

Admin

  • server依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <version>2.1.0</version>
    </dependency>
  • sever配置

    server.port=8000 修改启动的端口

  • client依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.1.0</version>
    </dependency>

    spring-boot-admin-starter-client 会自动添加 Actuator 相关依赖

  • client配置

    1
    2
    3
    4
    5
    spring.application.name=Admin Client
    # 配置sever的地址
    spring.boot.admin.client.url=http://localhost:8000
    # 打开所有监控
    management.endpoints.web.exposure.include=*
  • 访问
    通过http://localhost:8000访问页面,就可以查看指标了

部署

普通部署

  • 打包配置
    可以打包成jar包,也可以打成war包,这里只介绍jar包
    配置文件中设置打包方式:

    1
    <packaging>jar</packaging>
  • 打包命令
    cd到项目根目录下,执行
    mvn clean package -Dmaven.test.skip=true
    mvn clean 是清除项目target目录下的文件
    mvn package 是打包,可以跟clean一起执行,类似于管道。默认情况下,打包时候会自动运行test下的测试,如果失败打包就结束,可以通过-Dmaven.test.skip=true来禁用此测试。

    打包成功后,在target目录下,就会生成jar文件

  • 运行

    java -jar target/spring-boot-package-1.0.0.jar
    nohup java -jar spring-boot-package-1.0.0.jar &

  • 多配置文件

    1. pom.xml修改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      <profiles>
      <profile>
      <id>dev</id>
      <properties>
      <env>dev</env>
      </properties>
      <activation>
      <activeByDefault>true</activeByDefault>
      </activation>
      </profile>
      <profile>
      <id>test</id>
      <properties>
      <env>test</env>
      </properties>
      </profile>
      <profile>
      <id>pro</id>
      <properties>
      <env>pro</env>
      </properties>
      </profile>
      </profiles>

      <build>
      <plugins>
      <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      </plugins>
      </build>
    2. 配置文件修改
      在 Spring Boot 中多环境配置文件名需要满足 application-{profile}.properties 的格式
      在 resources 目录下增加以下三个文件。
      application-dev.properties:开发环境
      application-test.properties:测试环境
      application-prod.properties:生产环境

      将原来的application.properties修改为spring.profiles.active=dev,这样默认时候的是dev配置文件

    3. 运行
      打包完成之后,可以通过java -jar target/spring-boot-package-1.0.0.jar --spring.profiles.active=dev 的方式来运行

  • Jenkins简介
    Jenkins,批量部署时候,它是目前CI领域使用最广泛的工具之一,它是一个独立的开源自动化服务器,可用于自动化各种任务,如构建、测试和部署软件。Jenkins 可以通过本机系统包以 Docker 的方式部署项目。

    使用 Jenkin 之后,部署项目的步骤如下:
    push 代码到 Github(或者 SVN) 触发 WebHook
    Jenkins 从仓库拉去代码
    Maven 构建项目、单元测试
    备份项目,停止正在运行的项目
    启动应用
    查看启动日志

docker部署

  • 配置
    在pom.xml中,增加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <!--镜像前缀-->
    <properties>
    <docker.image.prefix>friday</docker.image.prefix>
    </properties>

    <!-- 中间省略 -->

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <!-- Docker maven plugin -->
    <plugin>
    <groupId>com.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>1.0.0</version>
    <configuration>
    <imageName>${docker.image.prefix}/${project.artifactId}</imageName>
    <dockerDirectory>src/main/docker</dockerDirectory>
    <resources>
    <resource>
    <targetPath>/</targetPath>
    <directory>${project.build.directory}</directory>
    <include>${project.build.finalName}.jar</include>
    </resource>
    </resources>
    </configuration>
    </plugin>
    <!-- Docker maven plugin -->
    </plugins>
    </build>

    ${project.build.directory},构建目录,缺省为 target
    ${project.build.finalName},产出物名称,缺省为 ${project.artifactId}-${project.version}

  • Dockerfile

    1
    2
    3
    FROM openjdk:8-jdk-alpine
    ADD account-0.1.0.jar app.jar
    ENTRYPOINT ["java","-jar","/app.jar"]

    account-0.1.0.jar是打的jar包

  • 打包部署

    1. pro的配置文件
      application-pro.properties中的localhost需要修改成对应的docker,如

      1
      2
      3
      spring.datasource.url: jdbc:postgresql://postgresql:5432/account  
      ...
      spring.redis.host=redis
    2. 打包
      mvn clean package -Dmaven.test.skip=true

    3. 启动
      java -jar target/account-0.1.0.jar --spring.profiles.active=pro
      运行正常,若启动失败,需要调试到正常

    4. 打镜像
      mvn docker:build

    5. docker-compose.yml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      account:
      image: friday/account:latest
      container_name: account
      restart: unless-stopped
      environment:
      spring.profiles.active: pro
      ports:
      - 8080:8080
      links:
      - postgresql
      - redis
    6. 启动
      docker-compose up -d

后记

spring boot使用学习到此结束,嚼别人嚼过的馒头,虽速度不慢,但真不香。如果想深入的学习应该去看看spring boot的实现,Druid的实现,Elasticsearch的实现,RabbitMQ的原理等内容。这些全看,太费时,还有更重要的内容要去学习;不看,浅尝辄止,对于自己没有多少提高。那就综合一下,对spring boot的实现进行一下探索。

这里有关于学习有些话想说,使用级别的学习,也是最肤浅的学习,就如同小孩子学会用水杯喝水,我称它为器;对原理与实现的学习,是第二阶段的学习,这就像学习如何制造水杯,我称它为术法;在众多术法以及术法演化中,涌现出了一些共性的东西,我称它为道,道不再局于某个领域,经济、组织、控制、生态、心智无不包含其中。吾辈学习,应以器学术,以术悟道。