SpringBoot 开发实用篇2

2023年 9月 26日 30.4k 0

SpringBoot整合jetcache缓存

目前我们使用的缓存都是要么A要么B,能不能AB一起用呢?这一节就解决这个问题。springboot针对缓存的整合仅仅停留在用缓存上面,如果缓存自身不支持同时支持AB一起用,springboot也没办法,所以要想解决AB缓存一起用的问题,就必须找一款缓存能够支持AB两种缓存一起用,有这种缓存吗?还真有,阿里出品,jetcache。

jetcache严格意义上来说,并不是一个缓存解决方案,只能说他算是一个缓存框架,然后把别的缓存放到jetcache中管理,这样就可以支持AB缓存一起用了。并且jetcache参考了springboot整合缓存的思想,整体技术使用方式和springboot的缓存解决方案思想非常类似。下面咱们就先把jetcache用起来,然后再说它里面的一些小的功能。

做之前要先明确一下,jetcache并不是随便拿两个缓存都能拼到一起去的。目前jetcache支持的缓存方案本地缓存支持两种,远程缓存支持两种,分别如下:

  • 本地缓存(Local)
    • LinkedHashMap
    • Caffeine
  • 远程缓存(Remote)
    • Redis
    • Tair

其实也有人问我,为什么jetcache只支持2+2这么4款缓存呢?阿里研发这个技术其实主要是为了满足自身的使用需要。最初肯定只有1+1种,逐步变化成2+2种。下面就以LinkedHashMap+Redis的方案实现本地与远程缓存方案同时使用。

纯远程方案

步骤①:导入springboot整合jetcache对应的坐标starter,当前坐标默认使用的远程方案是redis


    com.alicp.jetcache
    jetcache-starter-redis
    2.6.2

步骤②:远程方案基本配置

jetcache:
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50

其中poolConfig是必配项,否则会报错

步骤③:启用缓存,在引导类上方标注注解@EnableCreateCacheAnnotation配置springboot程序中可以使用注解的形式创建缓存

@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot20JetCacheApplication.class, args);
    }
}

步骤④:创建缓存对象Cache,并使用注解@CreateCache标记当前缓存的信息,然后使用Cache对象的API操作缓存,put写缓存,get读缓存。

@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @Autowired
    private CodeUtils codeUtils;
    
    @CreateCache(name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
    private Cache jetCache;

    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        jetCache.put(tele,code);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        String code = jetCache.get(smsCode.getTele());
        return smsCode.getCode().equals(code);
    }
}

通过上述jetcache使用远程方案连接redis可以看出,jetcache操作缓存时的接口操作更符合开发者习惯,使用缓存就先获取缓存对象Cache,放数据进去就是put,取数据出来就是get,更加简单易懂。并且jetcache操作缓存时,可以为某个缓存对象设置过期时间,将同类型的数据放入缓存中,方便有效周期的管理。

上述方案中使用的是配置中定义的default缓存,其实这个default是个名字,可以随便写,也可以随便加。例如再添加一种缓存解决方案,参照如下配置进行:

jetcache:
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50

如果想使用名称是sms的缓存,需要再创建缓存时指定参数area,声明使用对应缓存即可

@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @Autowired
    private CodeUtils codeUtils;
    
    @CreateCache(area="sms",name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
    private Cache jetCache;

    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        jetCache.put(tele,code);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        String code = jetCache.get(smsCode.getTele());
        return smsCode.getCode().equals(code);
    }
}
纯本地方案

远程方案中,配置中使用remote表示远程,换成local就是本地,只不过类型不一样而已。

步骤①:导入springboot整合jetcache对应的坐标starter


    com.alicp.jetcache
    jetcache-starter-redis
    2.6.2

步骤②:本地缓存基本配置

jetcache:
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson

为了加速数据获取时key的匹配速度,jetcache要求指定key的类型转换器。简单说就是,如果你给了一个Object作为key的话,我先用key的类型转换器给转换成字符串,然后再保存。等到获取数据时,仍然是先使用给定的Object转换成字符串,然后根据字符串匹配。由于jetcache是阿里的技术,这里推荐key的类型转换器使用阿里的fastjson。

步骤③:启用缓存

@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot20JetCacheApplication.class, args);
    }
}

步骤④:创建缓存对象Cache时,标注当前使用本地缓存

@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.LOCAL)
    private Cache jetCache;

    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        jetCache.put(tele,code);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        String code = jetCache.get(smsCode.getTele());
        return smsCode.getCode().equals(code);
    }
}

cacheType控制当前缓存使用本地缓存还是远程缓存,配置cacheType=CacheType.LOCAL即使用本地缓存。

本地+远程方案

本地和远程方法都有了,两种方案一起使用如何配置呢?其实就是将两种配置合并到一起就可以了。

jetcache:
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50

在创建缓存的时候,配置cacheType为BOTH即则本地缓存与远程缓存同时使用。

@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.BOTH)
    private Cache jetCache;
}

cacheType如果不进行配置,默认值是REMOTE,即仅使用远程缓存方案。关于jetcache的配置,参考以下信息

属性 默认值 说明
jetcache.statIntervalMinutes 0 统计间隔,0表示不统计
jetcache.hiddenPackages 自动生成name时,隐藏指定的包名前缀
jetcache.[local|remote].${area}.type 缓存类型,本地支持linkedhashmap、caffeine,远程支持redis、tair
jetcache.[local|remote].${area}.keyConvertor key转换器,当前仅支持fastjson
jetcache.[local|remote].${area}.valueEncoder java 仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local|remote].${area}.valueDecoder java 仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local|remote].${area}.limit 100 仅local类型的缓存需要指定,缓存实例最大元素数
jetcache.[local|remote].${area}.expireAfterWriteInMillis 无穷大 默认过期时间,毫秒单位
jetcache.local.${area}.expireAfterAccessInMillis 0 仅local类型的缓存有效,毫秒单位,最大不活动间隔

以上方案仅支持手工控制缓存,但是springcache方案中的方法缓存特别好用,给一个方法添加一个注解,方法就会自动使用缓存。jetcache也提供了对应的功能,即方法缓存。

方法缓存

jetcache提供了方法缓存方案,只不过名称变更了而已。在对应的操作接口上方使用注解@Cached即可

步骤①:导入springboot整合jetcache对应的坐标starter


    com.alicp.jetcache
    jetcache-starter-redis
    2.6.2

步骤②:配置缓存

jetcache:
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      keyConvertor: fastjson
      valueEncode: java
      valueDecode: java
      poolConfig:
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50

由于redis缓存中不支持保存对象,因此需要对redis设置当Object类型数据进入到redis中时如何进行类型转换。需要配置keyConvertor表示key的类型转换方式,同时标注value的转换类型方式,值进入redis时是java类型,标注valueEncode为java,值从redis中读取时转换成java,标注valueDecode为java。

注意,为了实现Object类型的值进出redis,需要保障进出redis的Object类型的数据必须实现序列化接口。

@Data
public class Book implements Serializable {
    private Integer id;
    private String type;
    private String name;
    private String description;
}

步骤③:启用缓存时开启方法缓存功能,并配置basePackages,说明在哪些包中开启方法缓存

@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
//开启方法注解缓存
@EnableMethodCache(basePackages = "com.itheima")
public class Springboot20JetCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot20JetCacheApplication.class, args);
    }
}

步骤④:使用注解@Cached标注当前方法使用缓存

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    
    @Override
    @Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
    public Book getById(Integer id) {
        return bookDao.selectById(id);
    }
}
远程方案的数据同步

由于远程方案中redis保存的数据可以被多个客户端共享,这就存在了数据同步问题。jetcache提供了3个注解解决此问题,分别在更新、删除操作时同步缓存数据,和读取缓存时定时刷新数据

更新缓存

@CacheUpdate(name="book_",key="#book.id",value="#book")
public boolean update(Book book) {
    return bookDao.updateById(book) > 0;
}

删除缓存

@CacheInvalidate(name="book_",key = "#id")
public boolean delete(Integer id) {
    return bookDao.deleteById(id) > 0;
}

定时刷新缓存

@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
@CacheRefresh(refresh = 5)
public Book getById(Integer id) {
    return bookDao.selectById(id);
}
数据报表

jetcache还提供有简单的数据报表功能,帮助开发者快速查看缓存命中信息,只需要添加一个配置即可

jetcache:
  statIntervalMinutes: 1

设置后,每1分钟在控制台输出缓存数据命中信息

[DefaultExecutor] c.alicp.jetcache.support.StatInfoLogger  : jetcache stat from 2022-02-28 09:32:15,892 to 2022-02-28 09:33:00,003
cache    |    qps|   rate|   get|    hit|   fail|   expire|   avgLoadTime|   maxLoadTime
---------+-------+-------+------+-------+-------+---------+--------------+--------------
book_    |   0.66| 75.86%|    29|     22|      0|        0|          28.0|           188
---------+-------+-------+------+-------+-------+---------+--------------+--------------

总结

  • jetcache是一个类似于springcache的缓存解决方案,自身不具有缓存功能,它提供有本地缓存与远程缓存多级共同使用的缓存解决方案
  • jetcache提供的缓存解决方案受限于目前支持的方案,本地缓存支持两种,远程缓存支持两种
  • 注意数据进入远程缓存时的类型转换问题
  • jetcache提供方法缓存,并提供了对应的缓存更新与刷新功能
  • jetcache提供有简单的缓存信息命中报表方便开发者即时监控缓存数据命中情况
  • 思考

    jetcache解决了前期使用缓存方案单一的问题,但是仍然不能灵活的选择缓存进行搭配使用,是否存在一种技术可以灵活的搭配各种各样的缓存使用呢?有,咱们下一节再讲。

    SpringBoot整合j2cache缓存

    jetcache可以在限定范围内构建多级缓存,但是灵活性不足,不能随意搭配缓存,本节介绍一种可以随意搭配缓存解决方案的缓存整合框架,j2cache。下面就来讲解如何使用这种缓存框架,以Ehcache与redis整合为例:

    步骤①:导入j2cache、redis、ehcache坐标

    
        net.oschina.j2cache
        j2cache-core
        2.8.4-release
    
    
        net.oschina.j2cache
        j2cache-spring-boot2-starter
        2.8.0-release
    
    
        net.sf.ehcache
        ehcache
    
    

    j2cache的starter中默认包含了redis坐标,官方推荐使用redis作为二级缓存,因此此处无需导入redis坐标

    步骤②:配置一级与二级缓存,并配置一二级缓存间数据传递方式,配置书写在名称为j2cache.properties的文件中。如果使用ehcache还需要单独添加ehcache的配置文件

    # 1级缓存
    j2cache.L1.provider_class = ehcache
    ehcache.configXml = ehcache.xml
    
    # 2级缓存
    j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider
    j2cache.L2.config_section = redis
    redis.hosts = localhost:6379
    
    # 1级缓存中的数据如何到达二级缓存
    j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
    

    此处配置不能乱配置,需要参照官方给出的配置说明进行。例如1级供应商选择ehcache,供应商名称仅仅是一个ehcache,但是2级供应商选择redis时要写专用的Spring整合Redis的供应商类名SpringRedisProvider,而且这个名称并不是所有的redis包中能提供的,也不是spring包中提供的。因此配置j2cache必须参照官方文档配置,而且还要去找专用的整合包,导入对应坐标才可以使用。

    一级与二级缓存最重要的一个配置就是两者之间的数据沟通方式,此类配置也不是随意配置的,并且不同的缓存解决方案提供的数据沟通方式差异化很大,需要查询官方文档进行设置。

    步骤③:使用缓存

    @Service
    public class SMSCodeServiceImpl implements SMSCodeService {
        @Autowired
        private CodeUtils codeUtils;
    
        @Autowired
        private CacheChannel cacheChannel;
    
        public String sendCodeToSMS(String tele) {
            String code = codeUtils.generator(tele);
            cacheChannel.set("sms",tele,code);
            return code;
        }
    
        public boolean checkCode(SMSCode smsCode) {
            String code = cacheChannel.get("sms",smsCode.getTele()).asString();
            return smsCode.getCode().equals(code);
        }
    }
    

    j2cache的使用和jetcache比较类似,但是无需开启使用的开关,直接定义缓存对象即可使用,缓存对象名CacheChannel。

    j2cache的使用不复杂,配置是j2cache的核心,毕竟是一个整合型的缓存框架。缓存相关的配置过多,可以查阅j2cache-core核心包中的j2cache.properties文件中的说明。如下:

    #J2Cache configuration
    #########################################
    # Cache Broadcast Method
    # values:
    # jgroups -> use jgroups's multicast
    # redis -> use redis publish/subscribe mechanism (using jedis)
    # lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
    # rabbitmq -> use RabbitMQ publisher/consumer mechanism
    # rocketmq -> use RocketMQ publisher/consumer mechanism
    # none -> don't notify the other nodes in cluster
    # xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
    #########################################
    j2cache.broadcast = redis
    
    # jgroups properties
    jgroups.channel.name = j2cache
    jgroups.configXml = /network.xml
    
    # RabbitMQ properties
    rabbitmq.exchange = j2cache
    rabbitmq.host = localhost
    rabbitmq.port = 5672
    rabbitmq.username = guest
    rabbitmq.password = guest
    
    # RocketMQ properties
    rocketmq.name = j2cache
    rocketmq.topic = j2cache
    # use ; to split multi hosts
    rocketmq.hosts = 127.0.0.1:9876
    
    #########################################
    # Level 1&2 provider
    # values:
    # none -> disable this level cache
    # ehcache -> use ehcache2 as level 1 cache
    # ehcache3 -> use ehcache3 as level 1 cache
    # caffeine -> use caffeine as level 1 cache(only in memory)
    # redis -> use redis as level 2 cache (using jedis)
    # lettuce -> use redis as level 2 cache (using lettuce)
    # readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
    # memcached -> use memcached as level 2 cache (xmemcached),
    # [classname] -> use custom provider
    #########################################
    
    j2cache.L1.provider_class = caffeine
    j2cache.L2.provider_class = redis
    
    # When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
    # j2cache.L2.config_section = redis
    
    # Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
    # NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
    j2cache.sync_ttl_to_redis = true
    
    # Whether to cache null objects by default (default false)
    j2cache.default_cache_null_object = true
    
    #########################################
    # Cache Serialization Provider
    # values:
    # fst -> using fast-serialization (recommend)
    # kryo -> using kryo serialization
    # json -> using fst's json serialization (testing)
    # fastjson -> using fastjson serialization (embed non-static class not support)
    # java -> java standard
    # fse -> using fse serialization
    # [classname implements Serializer]
    #########################################
    
    j2cache.serialization = json
    #json.map.person = net.oschina.j2cache.demo.Person
    
    #########################################
    # Ehcache configuration
    #########################################
    
    # ehcache.configXml = /ehcache.xml
    
    # ehcache3.configXml = /ehcache3.xml
    # ehcache3.defaultHeapSize = 1000
    
    #########################################
    # Caffeine configuration
    # caffeine.region.[name] = size, xxxx[s|m|h|d]
    #
    #########################################
    caffeine.properties = /caffeine.properties
    
    #########################################
    # Redis connection configuration
    #########################################
    
    #########################################
    # Redis Cluster Mode
    #
    # single -> single redis server
    # sentinel -> master-slaves servers
    # cluster -> cluster servers (数据库配置无效,使用 database = 0)
    # sharded -> sharded servers  (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0)
    #
    #########################################
    
    redis.mode = single
    
    #redis storage mode (generic|hash)
    redis.storage = generic
    
    ## redis pub/sub channel name
    redis.channel = j2cache
    ## redis pub/sub server (using redis.hosts when empty)
    redis.channel.host =
    
    #cluster name just for sharded
    redis.cluster_name = j2cache
    
    ## redis cache namespace optional, default[empty]
    redis.namespace =
    
    ## redis command scan parameter count, default[1000]
    #redis.scanCount = 1000
    
    ## connection
    # Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379
    
    redis.hosts = 127.0.0.1:6379
    redis.timeout = 2000
    redis.password =
    redis.database = 0
    redis.ssl = false
    
    ## redis pool properties
    redis.maxTotal = 100
    redis.maxIdle = 10
    redis.maxWaitMillis = 5000
    redis.minEvictableIdleTimeMillis = 60000
    redis.minIdle = 1
    redis.numTestsPerEvictionRun = 10
    redis.lifo = false
    redis.softMinEvictableIdleTimeMillis = 10
    redis.testOnBorrow = true
    redis.testOnReturn = false
    redis.testWhileIdle = true
    redis.timeBetweenEvictionRunsMillis = 300000
    redis.blockWhenExhausted = false
    redis.jmxEnabled = false
    
    #########################################
    # Lettuce scheme
    #
    # redis -> single redis server
    # rediss -> single redis server with ssl
    # redis-sentinel -> redis sentinel
    # redis-cluster -> cluster servers
    #
    #########################################
    
    #########################################
    # Lettuce Mode
    #
    # single -> single redis server
    # sentinel -> master-slaves servers
    # cluster -> cluster servers (数据库配置无效,使用 database = 0)
    # sharded -> sharded servers  (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0)
    #
    #########################################
    
    ## redis command scan parameter count, default[1000]
    #lettuce.scanCount = 1000
    lettuce.mode = single
    lettuce.namespace =
    lettuce.storage = hash
    lettuce.channel = j2cache
    lettuce.scheme = redis
    lettuce.hosts = 127.0.0.1:6379
    lettuce.password =
    lettuce.database = 0
    lettuce.sentinelMasterId =
    lettuce.maxTotal = 100
    lettuce.maxIdle = 10
    lettuce.minIdle = 10
    # timeout in milliseconds
    lettuce.timeout = 10000
    # redis cluster topology refresh interval in milliseconds
    lettuce.clusterTopologyRefresh = 3000
    
    #########################################
    # memcached server configurations
    # refer to https://gitee.com/mirrors/XMemcached
    #########################################
    
    memcached.servers = 127.0.0.1:11211
    memcached.username =
    memcached.password =
    memcached.connectionPoolSize = 10
    memcached.connectTimeout = 1000
    memcached.failureMode = false
    memcached.healSessionInterval = 1000
    memcached.maxQueuedNoReplyOperations = 100
    memcached.opTimeout = 100
    memcached.sanitizeKeys = false
    

    总结

  • j2cache是一个缓存框架,自身不具有缓存功能,它提供多种缓存整合在一起使用的方案
  • j2cache需要通过复杂的配置设置各级缓存,以及缓存之间数据交换的方式
  • j2cache操作接口通过CacheChannel实现
  • KF-5-2.任务

    springboot整合第三方技术第二部分我们来说说任务系统,其实这里说的任务系统指的是定时任务。定时任务是企业级开发中必不可少的组成部分,诸如长周期业务数据的计算,例如年度报表,诸如系统脏数据的处理,再比如系统性能监控报告,还有抢购类活动的商品上架,这些都离不开定时任务。本节将介绍两种不同的定时任务技术。

    Quartz

    Quartz技术是一个比较成熟的定时任务框架,怎么说呢?有点繁琐,用过的都知道,配置略微复杂。springboot对其进行整合后,简化了一系列的配置,将很多配置采用默认设置,这样开发阶段就简化了很多。再学习springboot整合Quartz前先普及几个Quartz的概念。

    • 工作(Job):用于定义具体执行的工作
    • 工作明细(JobDetail):用于描述定时工作相关的信息
    • 触发器(Trigger):描述了工作明细与调度器的对应关系
    • 调度器(Scheduler):用于描述触发工作的执行规则,通常使用cron表达式定义规则

    简单说就是你定时干什么事情,这就是工作,工作不可能就是一个简单的方法,还要设置一些明细信息。工作啥时候执行,设置一个调度器,可以简单理解成设置一个工作执行的时间。工作和调度都是独立定义的,它们两个怎么配合到一起呢?用触发器。完了,就这么多。下面开始springboot整合Quartz。

    步骤①:导入springboot整合Quartz的starter

    
        org.springframework.boot
        spring-boot-starter-quartz
    
    

    步骤②:定义任务Bean,按照Quartz的开发规范制作,继承QuartzJobBean

    public class MyQuartz extends QuartzJobBean {
        @Override
        protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
            System.out.println("quartz task run...");
        }
    }
    

    步骤③:创建Quartz配置类,定义工作明细(JobDetail)与触发器的(Trigger)bean

    @Configuration
    public class QuartzConfig {
        @Bean
        public JobDetail printJobDetail(){
            //绑定具体的工作
            return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
        }
        @Bean
        public Trigger printJobTrigger(){
            ScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
            //绑定对应的工作明细
            return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(schedBuilder).build();
        }
    }
    

    工作明细中要设置对应的具体工作,使用newJob()操作传入对应的工作任务类型即可。

    触发器需要绑定任务,使用forJob()操作传入绑定的工作明细对象。此处可以为工作明细设置名称然后使用名称绑定,也可以直接调用对应方法绑定。触发器中最核心的规则是执行时间,此处使用调度器定义执行时间,执行时间描述方式使用的是cron表达式。有关cron表达式的规则,各位小伙伴可以去参看相关课程学习,略微复杂,而且格式不能乱设置,不是写个格式就能用的,写不好就会出现冲突问题。

    总结

  • springboot整合Quartz就是将Quartz对应的核心对象交给spring容器管理,包含两个对象,JobDetail和Trigger对象
  • JobDetail对象描述的是工作的执行信息,需要绑定一个QuartzJobBean类型的对象
  • Trigger对象定义了一个触发器,需要为其指定绑定的JobDetail是哪个,同时要设置执行周期调度器
  • 思考

    上面的操作看上去不多,但是Quartz将其中的对象划分粒度过细,导致开发的时候有点繁琐,spring针对上述规则进行了简化,开发了自己的任务管理组件——Task,如何用呢?咱们下节再说。

    Task

    spring根据定时任务的特征,将定时任务的开发简化到了极致。怎么说呢?要做定时任务总要告诉容器有这功能吧,然后定时执行什么任务直接告诉对应的bean什么时间执行就行了,就这么简单,一起来看怎么做

    步骤①:开启定时任务功能,在引导类上开启定时任务功能的开关,使用注解@EnableScheduling

    @SpringBootApplication
    //开启定时任务功能
    @EnableScheduling
    public class Springboot22TaskApplication {
        public static void main(String[] args) {
            SpringApplication.run(Springboot22TaskApplication.class, args);
        }
    }
    

    步骤②:定义Bean,在对应要定时执行的操作上方,使用注解@Scheduled定义执行的时间,执行时间的描述方式还是cron表达式

    @Component
    public class MyBean {
        @Scheduled(cron = "0/1 * * * * ?")
        public void print(){
            System.out.println(Thread.currentThread().getName()+" :spring task run...");
        }
    }
    

    完事,这就完成了定时任务的配置。总体感觉其实什么东西都没少,只不过没有将所有的信息都抽取成bean,而是直接使用注解绑定定时执行任务的事情而已。

    如何想对定时任务进行相关配置,可以通过配置文件进行

    spring:
      task:
       	scheduling:
          pool:
           	size: 1							# 任务调度线程池大小 默认 1
          thread-name-prefix: ssm_      	# 调度线程名称前缀 默认 scheduling-      
            shutdown:
              await-termination: false		# 线程池关闭时等待所有任务完成
              await-termination-period: 10s	# 调度线程关闭前最大等待时间,确保最后一定关闭
    

    总结

  • spring task需要使用注解@EnableScheduling开启定时任务功能

  • 为定时执行的的任务设置执行周期,描述方式cron表达式

  • KF-5-3.邮件

    springboot整合第三方技术第三部分我们来说说邮件系统,发邮件是java程序的基本操作,springboot整合javamail其实就是简化开发。不熟悉邮件的小伙伴可以先学习完javamail的基础操作,再来看这一部分内容才能感触到springboot整合javamail究竟简化了哪些操作。简化的多码?其实不多,差别不大,只是还个格式而已。

    学习邮件发送之前先了解3个概念,这些概念规范了邮件操作过程中的标准。

    • SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于发送电子邮件的传输协议
    • POP3(Post Office Protocol - Version 3):用于接收电子邮件的标准协议
    • IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议

    简单说就是SMPT是发邮件的标准,POP3是收邮件的标准,IMAP是对POP3的升级。我们制作程序中操作邮件,通常是发邮件,所以SMTP是使用的重点,收邮件大部分都是通过邮件客户端完成,所以开发收邮件的代码极少。除非你要读取邮件内容,然后解析,做邮件功能的统一处理。例如HR的邮箱收到求职者的简历,可以读取后统一处理。但是为什么不制作独立的投递简历的系统呢?所以说,好奇怪的需求,因为要想收邮件就要规范发邮件的人的书写格式,这个未免有点强人所难,并且极易收到外部攻击,你不可能使用白名单来收邮件。如果能使用白名单来收邮件然后解析邮件,还不如开发个系统给白名单中的人专用呢,更安全,总之就是鸡肋了。下面就开始学习springboot如何整合javamail发送邮件。

    发送简单邮件

    步骤①:导入springboot整合javamail的starter

    
        org.springframework.boot
        spring-boot-starter-mail
    
    

    步骤②:配置邮箱的登录信息

    spring:
      mail:
        host: smtp.126.com
        username: test@126.com
        password: test
    

    java程序仅用于发送邮件,邮件的功能还是邮件供应商提供的,所以这里是用别人的邮件服务,要配置对应信息。

    host配置的是提供邮件服务的主机协议,当前程序仅用于发送邮件,因此配置的是smtp的协议。

    password并不是邮箱账号的登录密码,是邮件供应商提供的一个加密后的密码,也是为了保障系统安全性。不然外部人员通过地址访问下载了配置文件,直接获取到了邮件密码就会有极大的安全隐患。有关该密码的获取每个邮件供应商提供的方式都不一样,此处略过。可以到邮件供应商的设置页面找POP3或IMAP这些关键词找到对应的获取位置。下例仅供参考:

    image-20220228111251036.png

    步骤③:使用JavaMailSender接口发送邮件

    @Service
    public class SendMailServiceImpl implements SendMailService {
        @Autowired
        private JavaMailSender javaMailSender;
    
        //发送人
        private String from = "test@qq.com";
        //接收人
        private String to = "test@126.com";
        //标题
        private String subject = "测试邮件";
        //正文
        private String context = "测试邮件正文内容";
    
        @Override
        public void sendMail() {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(from+"(小甜甜)");
            message.setTo(to);
            message.setSubject(subject);
            message.setText(context);
            javaMailSender.send(message);
        }
    }
    

    将发送邮件的必要信息(发件人、收件人、标题、正文)封装到SimpleMailMessage对象中,可以根据规则设置发送人昵称等。

    发送多组件邮件(附件、复杂正文)

    发送简单邮件仅需要提供对应的4个基本信息就可以了,如果想发送复杂的邮件,需要更换邮件对象。使用MimeMessage可以发送特殊的邮件。

    发送网页正文邮件

    @Service
    public class SendMailServiceImpl2 implements SendMailService {
        @Autowired
        private JavaMailSender javaMailSender;
    
        //发送人
        private String from = "test@qq.com";
        //接收人
        private String to = "test@126.com";
        //标题
        private String subject = "测试邮件";
        //正文
        private String context = "点开有惊喜";
    
        public void sendMail() {
            try {
                MimeMessage message = javaMailSender.createMimeMessage();
                MimeMessageHelper helper = new MimeMessageHelper(message);
                helper.setFrom(to+"(小甜甜)");
                helper.setTo(from);
                helper.setSubject(subject);
                helper.setText(context,true);		//此处设置正文支持html解析
    
                javaMailSender.send(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    发送带有附件的邮件

    @Service
    public class SendMailServiceImpl2 implements SendMailService {
        @Autowired
        private JavaMailSender javaMailSender;
    
        //发送人
        private String from = "test@qq.com";
        //接收人
        private String to = "test@126.com";
        //标题
        private String subject = "测试邮件";
        //正文
        private String context = "测试邮件正文";
    
        public void sendMail() {
            try {
                MimeMessage message = javaMailSender.createMimeMessage();
                MimeMessageHelper helper = new MimeMessageHelper(message,true);		//此处设置支持附件
                helper.setFrom(to+"(小甜甜)");
                helper.setTo(from);
                helper.setSubject(subject);
                helper.setText(context);
    
                //添加附件
                File f1 = new File("springboot_23_mail-0.0.1-SNAPSHOT.jar");
                File f2 = new File("resources\logo.png");
    
                helper.addAttachment(f1.getName(),f1);
                helper.addAttachment("最靠谱的培训结构.png",f2);
    
                javaMailSender.send(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    总结

  • springboot整合javamail其实就是简化了发送邮件的客户端对象JavaMailSender的初始化过程,通过配置的形式加载信息简化开发过程
  • KF-5-4.消息

    springboot整合第三方技术最后一部分我们来说说消息中间件,首先先介绍一下消息的应用。

    消息的概念

    从广义角度来说,消息其实就是信息,但是和信息又有所不同。信息通常被定义为一组数据,而消息除了具有数据的特征之外,还有消息的来源与接收的概念。通常发送消息的一方称为消息的生产者,接收消息的一方称为消息的消费者。这样比较后,发现其实消息和信息差别还是很大的。

    为什么要设置生产者和消费者呢?这就是要说到消息的意义了。信息通常就是一组数据,但是消息由于有了生产者和消费者,就出现了消息中所包含的信息可以被二次解读,生产者发送消息,可以理解为生产者发送了一个信息,也可以理解为生产者发送了一个命令;消费者接收消息,可以理解为消费者得到了一个信息,也可以理解为消费者得到了一个命令。对比一下我们会发现信息是一个基本数据,而命令则可以关联下一个行为动作,这样就可以理解为基于接收的消息相当于得到了一个行为动作,使用这些行为动作就可以组织成一个业务逻辑,进行进一步的操作。总的来说,消息其实也是一组信息,只是为其赋予了全新的含义,因为有了消息的流动,并且是有方向性的流动,带来了基于流动的行为产生的全新解读。开发者就可以基于消息的这种特殊解,将其换成代码中的指令。

    对于消息的理解,初学者总认为消息内部的数据非常复杂,这是一个误区。比如我发送了一个消息,要求接受者翻译发送过去的内容。初学者会认为消息中会包含被翻译的文字,已经本次操作要执行翻译操作而不是打印操作。其实这种现象有点过度解读了,发送的消息中仅仅包含被翻译的文字,但是可以通过控制不同的人接收此消息来确认要做的事情。例如发送被翻译的文字仅到A程序,而A程序只能进行翻译操作,这样就可以发送简单的信息完成复杂的业务了,是通过接收消息的主体不同,进而执行不同的操作,而不会在消息内部定义数据的操作行为,当然如果开发者希望消息中包含操作种类信息也是可以的,只是提出消息的内容可以更简单,更单一。

    对于消息的生产者与消费者的工作模式,还可以将消息划分成两种模式,同步消费与异步消息。

    所谓同步消息就是生产者发送完消息,等待消费者处理,消费者处理完将结果告知生产者,然后生产者继续向下执行业务。这种模式过于卡生产者的业务执行连续性,在现在的企业级开发中,上述这种业务场景通常不会采用消息的形式进行处理。

    所谓异步消息就是生产者发送完消息,无需等待消费者处理完毕,生产者继续向下执行其他动作。比如生产者发送了一个日志信息给日志系统,发送过去以后生产者就向下做其他事情了,无需关注日志系统的执行结果。日志系统根据接收到的日志信息继续进行业务执行,是单纯的记录日志,还是记录日志并报警,这些和生产者无关,这样生产者的业务执行效率就会大幅度提升。并且可以通过添加多个消费者来处理同一个生产者发送的消息来提高系统的高并发性,改善系统工作效率,提高用户体验。一旦某一个消费者由于各种问题宕机了,也不会对业务产生影响,提高了系统的高可用性。

    以上简单的介绍了一下消息这种工作模式存在的意义,希望对各位学习者有所帮助。

    Java处理消息的标准规范

    目前企业级开发中广泛使用的消息处理技术共三大类,具体如下:

    • JMS
    • AMQP
    • MQTT

    为什么是三大类,而不是三个技术呢?因为这些都是规范,就想JDBC技术,是个规范,开发针对规范开发,运行还要靠实现类,例如MySQL提供了JDBC的实现,最终运行靠的还是实现。并且这三类规范都是针对异步消息进行处理的,也符合消息的设计本质,处理异步的业务。对以上三种消息规范做一下普及

    JMS

    JMS(Java Message Service),这是一个规范,作用等同于JDBC规范,提供了与消息服务相关的API接口。

    JMS消息模型

    JMS规范中规范了消息有两种模型。分别是点对点模型和发布订阅模型。

    点对点模型:peer-2-peer,生产者会将消息发送到一个保存消息的容器中,通常使用队列模型,使用队列保存消息。一个队列的消息只能被一个消费者消费,或未被及时消费导致超时。这种模型下,生产者和消费者是一对一绑定的。

    发布订阅模型:publish-subscribe,生产者将消息发送到一个保存消息的容器中,也是使用队列模型来保存。但是消息可以被多个消费者消费,生产者和消费者完全独立,相互不需要感知对方的存在。

    以上这种分类是从消息的生产和消费过程来进行区分,针对消息所包含的信息不同,还可以进行不同类别的划分。

    JMS消息种类

    根据消息中包含的数据种类划分,可以将消息划分成6种消息。

    • TextMessage
    • MapMessage
    • BytesMessage
    • StreamMessage
    • ObjectMessage
    • Message (只有消息头和属性)

    JMS主张不同种类的消息,消费方式不同,可以根据使用需要选择不同种类的消息。但是这一点也成为其诟病之处,后面再说。整体上来说,JMS就是典型的保守派,什么都按照J2EE的规范来,做一套规范,定义若干个标准,每个标准下又提供一大批API。目前对JMS规范实现的消息中间件技术还是挺多的,毕竟是皇家御用,肯定有人舔,例如ActiveMQ、Redis、HornetMQ。但是也有一些不太规范的实现,参考JMS的标准设计,但是又不完全满足其规范,例如:RabbitMQ、RocketMQ。

    AMQP

    JMS的问世为消息中间件提供了很强大的规范性支撑,但是使用的过程中就开始被人诟病,比如JMS设置的极其复杂的多种类消息处理机制。本来分门别类处理挺好的,为什么会被诟病呢?原因就在于JMS的设计是J2EE规范,站在Java开发的角度思考问题。但是现实往往是复杂度很高的。比如我有一个.NET开发的系统A,有一个Java开发的系统B,现在要从A系统给B系统发业务消息,结果两边数据格式不统一,没法操作。JMS不是可以统一数据格式吗?提供了6种数据种类,总有一款适合你啊。NO,一个都不能用。因为A系统的底层语言不是Java语言开发的,根本不支持那些对象。这就意味着如果想使用现有的业务系统A继续开发已经不可能了,必须推翻重新做使用Java语言开发的A系统。

    这时候有人就提出说,你搞那么复杂,整那么多种类干什么?找一种大家都支持的消息数据类型不就解决这个跨平台的问题了吗?大家一想,对啊,于是AMQP孕育而生。

    单从上面的说明中其实可以明确感知到,AMQP的出现解决的是消息传递时使用的消息种类的问题,化繁为简,但是其并没有完全推翻JMS的操作API,所以说AMQP仅仅是一种协议,规范了数据传输的格式而已。

    AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也是消息代理规范),规范了网络交换的数据格式,兼容JMS操作。
    优点

    具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现

    JMS消息种类

    AMQP消息种类:byte[]

    AMQP在JMS的消息模型基础上又进行了进一步的扩展,除了点对点和发布订阅的模型,开发了几种全新的消息模型,适应各种各样的消息发送。

    AMQP消息模型

    • direct exchange
    • fanout exchange
    • topic exchange
    • headers exchange
    • system exchange

    目前实现了AMQP协议的消息中间件技术也很多,而且都是较为流行的技术,例如:RabbitMQ、StormMQ、RocketMQ

    MQTT

    MQTT(Message Queueing Telemetry Transport)消息队列遥测传输,专为小设备设计,是物联网(IOT)生态系统中主要成分之一。由于与JavaEE企业级开发没有交集,此处不作过多的说明。

    除了上述3种J2EE企业级应用中广泛使用的三种异步消息传递技术,还有一种技术也不能忽略,Kafka。

    KafKa

    Kafka,一种高吞吐量的分布式发布订阅消息系统,提供实时消息功能。Kafka技术并不是作为消息中间件为主要功能的产品,但是其拥有发布订阅的工作模式,也可以充当消息中间件来使用,而且目前企业级开发中其身影也不少见。

    本节内容讲围绕着上述内容中的几种实现方案讲解springboot整合各种各样的消息中间件。由于各种消息中间件必须先安装再使用,下面的内容采用Windows系统安装,降低各位学习者的学习难度,基本套路和之前学习NoSQL解决方案一样,先安装再整合。

    购物订单发送手机短信案例

    为了便于下面演示各种各样的消息中间件技术,我们创建一个购物过程生成订单时为用户发送短信的案例环境,模拟使用消息中间件实现发送手机短信的过程。

    手机验证码案例需求如下:

    • 执行下单业务时(模拟此过程),调用消息服务,将要发送短信的订单id传递给消息中间件

    • 消息处理服务接收到要发送的订单id后输出订单id(模拟发短信)

      由于不涉及数据读写,仅开发业务层与表现层,其中短信处理的业务代码独立开发,代码如下:

    订单业务

    业务层接口

    public interface OrderService {
        void order(String id);
    }
    

    模拟传入订单id,执行下订单业务,参数为虚拟设定,实际应为订单对应的实体类

    业务层实现

    @Service
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private MessageService messageService;
        
        @Override
        public void order(String id) {
            //一系列操作,包含各种服务调用,处理各种业务
            System.out.println("订单处理开始");
            //短信消息处理
            messageService.sendMessage(id);
            System.out.println("订单处理结束");
            System.out.println();
        }
    }
    

    业务层转调短信处理的服务MessageService

    表现层服务

    @RestController
    @RequestMapping("/orders")
    public class OrderController {
    
        @Autowired
        private OrderService orderService;
    
        @PostMapping("{id}")
        public void order(@PathVariable String id){
            orderService.order(id);
        }
    }
    

    表现层对外开发接口,传入订单id即可(模拟)

    短信处理业务

    业务层接口

    public interface MessageService {
        void sendMessage(String id);
        String doMessage();
    }
    

    短信处理业务层接口提供两个操作,发送要处理的订单id到消息中间件,另一个操作目前暂且设计成处理消息,实际消息的处理过程不应该是手动执行,应该是自动执行,到具体实现时再进行设计

    业务层实现

    @Service
    public class MessageServiceImpl implements MessageService {
        private ArrayList msgList = new ArrayList();
    
        @Override
        public void sendMessage(String id) {
            System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
            msgList.add(id);
        }
    
        @Override
        public String doMessage() {
            String id = msgList.remove(0);
            System.out.println("已完成短信发送业务,id:"+id);
            return id;
        }
    }
    

    短信处理业务层实现中使用集合先模拟消息队列,观察效果

    表现层服务

    @RestController
    @RequestMapping("/msgs")
    public class MessageController {
    
        @Autowired
        private MessageService messageService;
    
        @GetMapping
        public String doMessage(){
            String id = messageService.doMessage();
            return id;
        }
    }
    

    短信处理表现层接口暂且开发出一个处理消息的入口,但是此业务是对应业务层中设计的模拟接口,实际业务不需要设计此接口。

    下面开启springboot整合各种各样的消息中间件,从严格满足JMS规范的ActiveMQ开始

    SpringBoot整合ActiveMQ

    ActiveMQ是MQ产品中的元老级产品,早期标准MQ产品之一,在AMQP协议没有出现之前,占据了消息中间件市场的绝大部分份额,后期因为AMQP系列产品的出现,迅速走弱,目前仅在一些线上运行的产品中出现,新产品开发较少采用。

    安装

    windows版安装包下载地址:activemq.apache.org/components/…/

    下载的安装包是解压缩就能使用的zip文件,解压缩完毕后会得到如下文件

    image-20220228160001620.png

    启动服务器

    activemq.bat
    

    运行bin目录下的win32或win64目录下的activemq.bat命令即可,根据自己的操作系统选择即可,默认对外服务端口61616。

    访问web管理服务

    ActiveMQ启动后会启动一个Web控制台服务,可以通过该服务管理ActiveMQ。

    http://127.0.0.1:8161/
    

    web管理服务默认端口8161,访问后可以打开ActiveMQ的管理界面,如下:

    image-20220228160844972.png

    首先输入访问用户名和密码,初始化用户名和密码相同,均为:admin,成功登录后进入管理后台界面,如下:

    image-20220228161010401.png

    看到上述界面视为启动ActiveMQ服务成功。

    启动失败

    在ActiveMQ启动时要占用多个端口,以下为正常启动信息:

    wrapper  | --> Wrapper Started as Console
    wrapper  | Launching a JVM...
    jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
    jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
    jvm 1    |
    jvm 1    | Java Runtime: Oracle Corporation 1.8.0_172 D:softjdk1.8.0_172jre
    jvm 1    |   Heap sizes: current=249344k  free=235037k  max=932352k
    jvm 1    |     JVM args: -Dactivemq.home=../.. -Dactivemq.base=../.. -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=../../conf/broker.ks -Djavax.net.ssl.trustStore=../../conf/broker.ts -Dcom.sun.management.jmxremote -Dorg.apache.activemq.UseDedicatedTaskRunner=true -Djava.util.logging.config.file=logging.properties -Dactivemq.conf=../../conf -Dactivemq.data=../../data -Djava.security.auth.login.config=../../conf/login.config -Xmx1024m -Djava.library.path=../../bin/win64 -Dwrapper.key=7ySrCD75XhLCpLjd -Dwrapper.port=32000 -Dwrapper.jvm.port.min=31000 -Dwrapper.jvm.port.max=31999 -Dwrapper.pid=9364 -Dwrapper.version=3.2.3 -Dwrapper.native_library=wrapper -Dwrapper.cpu.timeout=10 -Dwrapper.jvmid=1
    jvm 1    | Extensions classpath:
    jvm 1    |   [....lib,....libcamel,....liboptional,....libweb,....libextra]
    jvm 1    | ACTIVEMQ_HOME: ....
    jvm 1    | ACTIVEMQ_BASE: ....
    jvm 1    | ACTIVEMQ_CONF: ....conf
    jvm 1    | ACTIVEMQ_DATA: ....data
    jvm 1    | Loading message broker from: xbean:activemq.xml
    jvm 1    |  INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory$1@5f3ebfe0: startup date [Mon Feb 28 16:07:48 CST 2022]; root of context hierarchy
    jvm 1    |  INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[D:softactivemqbinwin64....datakahadb]
    jvm 1    |  INFO | KahaDB is version 7
    jvm 1    |  INFO | PListStore:[D:softactivemqbinwin64....datalocalhosttmp_storage] started
    jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10434-1646035669595-0:1) is starting
    jvm 1    |  INFO | Listening for connections at: tcp://CZBK-20210302VL:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600
    jvm 1    |  INFO | Connector openwire started
    jvm 1    |  INFO | Listening for connections at: amqp://CZBK-20210302VL:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600
    jvm 1    |  INFO | Connector amqp started
    jvm 1    |  INFO | Listening for connections at: stomp://CZBK-20210302VL:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600
    jvm 1    |  INFO | Connector stomp started
    jvm 1    |  INFO | Listening for connections at: mqtt://CZBK-20210302VL:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600
    jvm 1    |  INFO | Connector mqtt started
    jvm 1    |  INFO | Starting Jetty server
    jvm 1    |  INFO | Creating Jetty connector
    jvm 1    |  WARN | ServletContext@o.e.j.s.ServletContextHandler@7350746f{/,null,STARTING} has uncovered http methods for path: /
    jvm 1    |  INFO | Listening for connections at ws://CZBK-20210302VL:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600
    jvm 1    |  INFO | Connector ws started
    jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10434-1646035669595-0:1) started
    jvm 1    |  INFO | For help or more information please see: http://activemq.apache.org
    jvm 1    |  WARN | Store limit is 102400 mb (current store usage is 0 mb). The data directory: D:softactivemqbinwin64....datakahadb only has 68936 mb of usable space. - resetting to maximum available disk space: 68936 mb
    jvm 1    |  INFO | ActiveMQ WebConsole available at http://127.0.0.1:8161/
    jvm 1    |  INFO | ActiveMQ Jolokia REST API available at http://127.0.0.1:8161/api/jolokia/
    

    其中占用的端口有:61616、5672、61613、1883、61614,如果启动失败,请先管理对应端口即可。以下就是某个端口占用的报错信息,可以从抛出异常的位置看出,启动5672端口时端口被占用,显示java.net.BindException: Address already in use: JVM_Bind。Windows系统中终止端口运行的操作参看【命令行启动常见问题及解决方案】

    wrapper  | --> Wrapper Started as Console
    wrapper  | Launching a JVM...
    jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
    jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
    jvm 1    |
    jvm 1    | Java Runtime: Oracle Corporation 1.8.0_172 D:softjdk1.8.0_172jre
    jvm 1    |   Heap sizes: current=249344k  free=235038k  max=932352k
    jvm 1    |     JVM args: -Dactivemq.home=../.. -Dactivemq.base=../.. -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=../../conf/broker.ks -Djavax.net.ssl.trustStore=../../conf/broker.ts -Dcom.sun.management.jmxremote -Dorg.apache.activemq.UseDedicatedTaskRunner=true -Djava.util.logging.config.file=logging.properties -Dactivemq.conf=../../conf -Dactivemq.data=../../data -Djava.security.auth.login.config=../../conf/login.config -Xmx1024m -Djava.library.path=../../bin/win64 -Dwrapper.key=QPJoy9ZoXeWmmwTS -Dwrapper.port=32000 -Dwrapper.jvm.port.min=31000 -Dwrapper.jvm.port.max=31999 -Dwrapper.pid=14836 -Dwrapper.version=3.2.3 -Dwrapper.native_library=wrapper -Dwrapper.cpu.timeout=10 -Dwrapper.jvmid=1
    jvm 1    | Extensions classpath:
    jvm 1    |   [....lib,....libcamel,....liboptional,....libweb,....libextra]
    jvm 1    | ACTIVEMQ_HOME: ....
    jvm 1    | ACTIVEMQ_BASE: ....
    jvm 1    | ACTIVEMQ_CONF: ....conf
    jvm 1    | ACTIVEMQ_DATA: ....data
    jvm 1    | Loading message broker from: xbean:activemq.xml
    jvm 1    |  INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory$1@2c9392f5: startup date [Mon Feb 28 16:06:16 CST 2022]; root of context hierarchy
    jvm 1    |  INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[D:softactivemqbinwin64....datakahadb]
    jvm 1    |  INFO | KahaDB is version 7
    jvm 1    |  INFO | PListStore:[D:softactivemqbinwin64....datalocalhosttmp_storage] started
    jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is starting
    jvm 1    |  INFO | Listening for connections at: tcp://CZBK-20210302VL:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600
    jvm 1    |  INFO | Connector openwire started
    jvm 1    | ERROR | Failed to start Apache ActiveMQ (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1)
    jvm 1    | java.io.IOException: Transport Connector could not be registered in JMX: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
    jvm 1    |      at org.apache.activemq.util.IOExceptionSupport.create(IOExceptionSupport.java:28)
    jvm 1    |      at org.apache.activemq.broker.BrokerService.registerConnectorMBean(BrokerService.java:2288)
    jvm 1    |      at org.apache.activemq.broker.BrokerService.startTransportConnector(BrokerService.java:2769)
    jvm 1    |      at org.apache.activemq.broker.BrokerService.startAllConnectors(BrokerService.java:2665)
    jvm 1    |      at org.apache.activemq.broker.BrokerService.doStartBroker(BrokerService.java:780)
    jvm 1    |      at org.apache.activemq.broker.BrokerService.startBroker(BrokerService.java:742)
    jvm 1    |      at org.apache.activemq.broker.BrokerService.start(BrokerService.java:645)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerService.afterPropertiesSet(XBeanBrokerService.java:73)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1748)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1685)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1615)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:481)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:312)
    jvm 1    |      at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:308)
    jvm 1    |      at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    jvm 1    |      at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:756)
    jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
    jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542)
    jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:64)
    jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:52)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory$1.(XBeanBrokerFactory.java:104)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
    jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
    jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
    jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
    jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
    jvm 1    |      at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
    jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
    jvm 1    |      at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
    jvm 1    |      at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
    jvm 1    |      at org.apache.activemq.console.Main.main(Main.java:115)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
    jvm 1    |      at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
    jvm 1    |      at java.lang.Thread.run(Thread.java:748)
    jvm 1    | Caused by: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
    jvm 1    |      at org.apache.activemq.util.IOExceptionSupport.create(IOExceptionSupport.java:34)
    jvm 1    |      at org.apache.activemq.transport.tcp.TcpTransportServer.bind(TcpTransportServer.java:146)
    jvm 1    |      at org.apache.activemq.transport.tcp.TcpTransportFactory.doBind(TcpTransportFactory.java:62)
    jvm 1    |      at org.apache.activemq.transport.TransportFactorySupport.bind(TransportFactorySupport.java:40)
    jvm 1    |      at org.apache.activemq.broker.TransportConnector.createTransportServer(TransportConnector.java:335)
    jvm 1    |      at org.apache.activemq.broker.TransportConnector.getServer(TransportConnector.java:145)
    jvm 1    |      at org.apache.activemq.broker.TransportConnector.asManagedConnector(TransportConnector.java:110)
    jvm 1    |      at org.apache.activemq.broker.BrokerService.registerConnectorMBean(BrokerService.java:2283)
    jvm 1    |      ... 46 more
    jvm 1    | Caused by: java.net.BindException: Address already in use: JVM_Bind
    jvm 1    |      at java.net.DualStackPlainSocketImpl.bind0(Native Method)
    jvm 1    |      at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
    jvm 1    |      at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
    jvm 1    |      at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
    jvm 1    |      at java.net.ServerSocket.bind(ServerSocket.java:375)
    jvm 1    |      at java.net.ServerSocket.(ServerSocket.java:237)
    jvm 1    |      at javax.net.DefaultServerSocketFactory.createServerSocket(ServerSocketFactory.java:231)
    jvm 1    |      at org.apache.activemq.transport.tcp.TcpTransportServer.bind(TcpTransportServer.java:143)
    jvm 1    |      ... 52 more
    jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is shutting down
    jvm 1    |  INFO | socketQueue interrupted - stopping
    jvm 1    |  INFO | Connector openwire stopped
    jvm 1    |  INFO | Could not accept connection during shutdown  : null (null)
    jvm 1    |  INFO | Connector amqp stopped
    jvm 1    |  INFO | Connector stomp stopped
    jvm 1    |  INFO | Connector mqtt stopped
    jvm 1    |  INFO | Connector ws stopped
    jvm 1    |  INFO | PListStore:[D:softactivemqbinwin64....datalocalhosttmp_storage] stopped
    jvm 1    |  INFO | Stopping async queue tasks
    jvm 1    |  INFO | Stopping async topic tasks
    jvm 1    |  INFO | Stopped KahaDB
    jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) uptime 0.426 seconds
    jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is shutdown
    jvm 1    |  INFO | Closing org.apache.activemq.xbean.XBeanBrokerFactory$1@2c9392f5: startup date [Mon Feb 28 16:06:16 CST 2022]; root of context hierarchy
    jvm 1    |  WARN | Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.apache.activemq.xbean.XBeanBrokerService#0' defined in class path resource [activemq.xml]: Invocation of init method failed; nested exception is java.io.IOException: Transport Connector could not be registered in JMX: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
    jvm 1    | ERROR: java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
    jvm 1    | java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
    jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:91)
    jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
    jvm 1    |      at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
    jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
    jvm 1    |      at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
    jvm 1    |      at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
    jvm 1    |      at org.apache.activemq.console.Main.main(Main.java:115)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
    jvm 1    |      at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
    jvm 1    |      at java.lang.Thread.run(Thread.java:748)
    jvm 1    | Caused by: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
    jvm 1    |      at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:164)
    jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1034)
    jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555)
    jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:64)
    jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:52)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory$1.(XBeanBrokerFactory.java:104)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
    jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
    jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
    jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
    jvm 1    |      ... 16 more
    jvm 1    | ERROR: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
    jvm 1    | java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
    jvm 1    |      at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:164)
    jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1034)
    jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555)
    jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:64)
    jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:52)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory$1.(XBeanBrokerFactory.java:104)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
    jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
    jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
    jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
    jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
    jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
    jvm 1    |      at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
    jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
    jvm 1    |      at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
    jvm 1    |      at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
    jvm 1    |      at org.apache.activemq.console.Main.main(Main.java:115)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
    jvm 1    |      at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
    jvm 1    |      at java.lang.Thread.run(Thread.java:748)
    wrapper  |  0;
        }
    }
    

    在性能指标中就出现了自定义的性能指标监控项

    image-20220301175101812.png

    自定义端点

    可以根据业务需要自定义端点,方便业务监控

    @Component
    @Endpoint(id="pay",enableByDefault = true)
    public class PayEndpoint {
        @ReadOperation
        public Object getPay(){
            Map payMap = new HashMap();
            payMap.put("level 1","300");
            payMap.put("level 2","291");
            payMap.put("level 3","666");
            return payMap;
        }
    }
    

    由于此端点数据spirng boot admin无法预知该如何展示,所以通过界面无法看到此数据,通过HTTP请求路径可以获取到当前端点的信息,但是需要先开启当前端点对外功能,或者设置当前端点为默认开发的端点。

    image-20220301175355482.png

    总结

  • 端点的指标可以自定义,但是每种不同的指标根据其功能不同,自定义方式不同
  • info端点通过配置和编程的方式都可以添加端点指标
  • health端点通过编程的方式添加端点指标,需要注意要为对应指标添加启动状态的逻辑设定
  • metrics指标通过在业务中添加监控操作设置指标
  • 可以自定义端点添加更多的指标
  • 开发实用篇完结

    开发实用篇到这里就暂时完结了,在开发实用篇中我们讲解了大量的第三方技术的整合方案,选择的方案都是市面上比较流行的常用方案,还有一些国内流行度较低的方案目前还没讲,留到番外篇中慢慢讲吧。

    整体开发实用篇中讲解的内容可以分为两大类知识:实用性知识与经验性知识。

    实用性知识就是新知识了,springboot整合各种技术,每种技术整合中都有一些特殊操作,整体来说其实就是三句话。加坐标做配置调接口。经验性知识是对前面两篇中出现的一些知识的补充,在学习基础篇时如果将精力放在这些东西上就有点学偏了,容易钻牛角尖,放到实用开发篇中结合实际开发说一些不常见的但是对系统功能又危害的操作解决方案,提升理解。

    开发实用篇做到这里就告一段落,下面就要着手准备原理篇了。市面上很多课程原理篇讲的过于高深莫测,在新手还没明白123的时候就开始讲微积分了,着实让人看了着急。至于原理篇我讲成什么样子?一起期待吧。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论