SpringBoot系列教程之RedisTemplate Jedis配置说明文档

更多Spring文章,欢迎点击 一灰灰Blog-Spring专题SpringBoot2之后,默认采用Lettuce作为redis的连接客户端,当然我们还是可以强制捡回来,使用我们熟悉的Jedis的,本篇简单介绍下使用Jedis的相关配置原文链接: 181101-SpringBoot高级篇Redis之Jedis配置I. 基本配置1. 依赖使用Jedis与Lettuce不同的是,需要额外的引入Jedis包的依赖<dependencies> <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> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency></dependencies>2. 配置redis的相关配置,和前面的差不多,只是线程池的参数稍稍有点区别spring: redis: host: 127.0.0.1 port: 6379 password: database: 0 jedis: pool: max-idle: 6 max-active: 32 max-wait: 100 min-idle: 43. AutoConfig与前面不同的是,我们需要定义一个RedisConnectionFactory的bean作为默认的连接工厂,以此来确定底层的连接采用的是Jedis客户端@Configurationpublic class RedisAutoConfig { @Bean public RedisConnectionFactory redisConnectionFactory(JedisPoolConfig jedisPool, RedisStandaloneConfiguration jedisConfig) { JedisConnectionFactory connectionFactory = new JedisConnectionFactory(jedisConfig); connectionFactory.setPoolConfig(jedisPool); return connectionFactory; } @Configuration public static class JedisConf { @Value("${spring.redis.host:127.0.0.1}") private String host; @Value("${spring.redis.port:6379}") private Integer port; @Value("${spring.redis.password:}") private String password; @Value("${spring.redis.database:0}") private Integer database; @Value("${spring.redis.jedis.pool.max-active:8}") private Integer maxActive; @Value("${spring.redis.jedis.pool.max-idle:8}") private Integer maxIdle; @Value("${spring.redis.jedis.pool.max-wait:-1}") private Long maxWait; @Value("${spring.redis.jedis.pool.min-idle:0}") private Integer minIdle; @Bean public JedisPoolConfig jedisPool() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMaxWaitMillis(maxWait); jedisPoolConfig.setMaxTotal(maxActive); jedisPoolConfig.setMinIdle(minIdle); return jedisPoolConfig; } @Bean public RedisStandaloneConfiguration jedisConfig() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); config.setHostName(host); config.setPort(port); config.setDatabase(database); config.setPassword(RedisPassword.of(password)); return config; } }}4. 测试测试主要就是查看下RedisTemplate的连接工厂类,到底是啥,简单的是截图如下II. 其他0. 项目工程:spring-boot-demomodule: 121-redis-jedis-config1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

December 28, 2018 · 1 min · jiezi

SpringBoot之RedisTemplate Set数据结构使用教程

更多Spring文章,欢迎点击 一灰灰Blog-Spring专题Redis的五大数据结构,前面讲述了String和List,Hash的使用姿势,接下来就是Set集合,与list最大的区别就是里面不允许存在重复的数据<!– more –>I. 基本使用在开始之前,序列化的指定需要额外处理,上一篇已经提及,相关内容可以参考:181109-SpringBoot高级篇Redis之List数据结构使用姿势1. 新增元素新增元素时,可以根据返回值来判断是否添加成功, 如下面的单个插入时,如果集合中之前就已经有数据了,那么返回0,否则返回1/** * 新增一个 sadd * * @param key * @param value /public void add(String key, String value) { redisTemplate.opsForSet().add(key, value);}2. 删除元素因为list是有序的,所以在list的删除需要指定位置;而set则不需要/* * 删除集合中的值 srem * * @param key * @param value /public void remove(String key, String value) { redisTemplate.opsForSet().remove(key, value);}3. 判断是否存在set一个最大的应用场景就是判断某个元素是否有了,从而决定怎么执行后续的操作, 用的是 isMember方法,来判断集合中是否存在某个value/* * 判断是否包含 sismember * * @param key * @param value /public void contains(String key, String value) { redisTemplate.opsForSet().isMember(key, value);}4. 获取所有的valueset无序,因此像list一样获取某个范围的数据,不太容易,更常见的方式就是全部获取出来/* * 获取集合中所有的值 smembers * * @param key * @return /public Set<String> values(String key) { return redisTemplate.opsForSet().members(key);}5. 集合运算set还提供了另外几个高级一点的功能,就是集合的运算,如求并集,交集等操作,虽然在我有限的业务应用中,并没有使用到这些高级功能,下面依然个给出使用的姿势/* * 返回多个集合的并集 sunion * * @param key1 * @param key2 * @return /public Set<String> union(String key1, String key2) { return redisTemplate.opsForSet().union(key1, key2);}/* * 返回多个集合的交集 sinter * * @param key1 * @param key2 * @return /public Set<String> intersect(String key1, String key2) { return redisTemplate.opsForSet().intersect(key1, key2);}/* * 返回集合key1中存在,但是key2中不存在的数据集合 sdiff * * @param key1 * @param key2 * @return */public Set<String> diff(String key1, String key2) { return redisTemplate.opsForSet().difference(key1, key2);}II. 其他0. 项目工程:spring-boot-demo1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

December 28, 2018 · 1 min · jiezi

SpringBoot之RedisTemplate ZSet数据结构使用教程

更多Spring文章,欢迎点击 一灰灰Blog-Spring专题Redis的五大数据结构,目前就剩下最后的ZSET,可以简单的理解为带权重的集合;与前面的set最大的区别,就是每个元素可以设置一个score,从而可以实现各种排行榜的功能原文地址: 181212-SpringBoot高级篇Redis之ZSet数据结构使用姿势I. 基本使用在开始之前,序列化的指定需要额外处理,前面List这一篇已经提及,相关内容可以参考:181109-SpringBoot高级篇Redis之List数据结构使用姿势1. 新增元素新增元素时,用起来和set差不多,无非是多一个score的参数指定而已如果元素存在,会用新的score来替换原来的,返回0;如果元素不存在,则会会新增一个/** * 添加一个元素, zset与set最大的区别就是每个元素都有一个score,因此有个排序的辅助功能; zadd * * @param key * @param value * @param score /public void add(String key, String value, double score) { redisTemplate.opsForZSet().add(key, value, score);}2. 删除元素删除就和普通的set没啥区别了/* * 删除元素 zrem * * @param key * @param value /public void remove(String key, String value) { redisTemplate.opsForZSet().remove(key, value);}3. 修改scorezset中的元素塞入之后,可以修改其score的值,通过 zincrby 来对score进行加/减;当元素不存在时,则会新插入一个从上面的描述来看,zincrby 与 zadd 最大的区别是前者是增量修改;后者是覆盖score方式/* * score的增加or减少 zincrby * * @param key * @param value * @param score /public Double incrScore(String key, String value, double score) { return redisTemplate.opsForZSet().incrementScore(key, value, score);}4. 获取value对应的score这个需要注意的是,当value在集合中时,返回其score;如果不在,则返回null/* * 查询value对应的score zscore * * @param key * @param value * @return /public Double score(String key, String value) { return redisTemplate.opsForZSet().score(key, value);}5. 获取value在集合中排名前面是获取value对应的score;这里则是获取排名;这里score越小排名越高;从这个使用也可以看出结合4、5, 用zset来做排行榜可以很简单的获取某个用户在所有人中的排名与积分/* * 判断value在zset中的排名 zrank * * @param key * @param value * @return /public Long rank(String key, String value) { return redisTemplate.opsForZSet().rank(key, value);}6. 集合大小/* * 返回集合的长度 * * @param key * @return /public Long size(String key) { return redisTemplate.opsForZSet().zCard(key);}7. 获取集合中数据因为是有序,所以就可以获取指定范围的数据,下面有两种方式根据排序位置获取数据根据score区间获取排序位置/* * 查询集合中指定顺序的值, 0 -1 表示获取全部的集合内容 zrange * * 返回有序的集合,score小的在前面 * * @param key * @param start * @param end * @return /public Set<String> range(String key, int start, int end) { return redisTemplate.opsForZSet().range(key, start, end);}/* * 查询集合中指定顺序的值和score,0, -1 表示获取全部的集合内容 * * @param key * @param start * @param end * @return /public Set<ZSetOperations.TypedTuple<String>> rangeWithScore(String key, int start, int end) { return redisTemplate.opsForZSet().rangeWithScores(key, start, end);}/* * 查询集合中指定顺序的值 zrevrange * * 返回有序的集合中,score大的在前面 * * @param key * @param start * @param end * @return /public Set<String> revRange(String key, int start, int end) { return redisTemplate.opsForZSet().reverseRange(key, start, end);}/* * 根据score的值,来获取满足条件的集合 zrangebyscore * * @param key * @param min * @param max * @return */public Set<String> sortRange(String key, int min, int max) { return redisTemplate.opsForZSet().rangeByScore(key, min, max);}II. 其他0. 项目工程:spring-boot-demo1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

December 28, 2018 · 2 min · jiezi

SpringBoot之RedisTemplate Hash数据结构使用教程

更多Spring文章,欢迎点击 一灰灰Blog-Spring专题Redis的五大数据结构,前面讲述了String和List的使用姿势,而Hash数据结构,也是比较常用的,接下来看下hash数据结构的读取,删除,塞入的基本使用姿势原文连接: 181202-SpringBoot高级篇Redis之Hash数据结构使用姿势I. 基本使用在开始之前,序列化的指定需要额外处理,上一篇已经提及,相关内容可以参考:181109-SpringBoot高级篇Redis之List数据结构使用姿势1. 查询元素hash数据结构和我们理解jdk中的hash差不多,使用的姿势也没什么区别,需要注意的是需要,定位一个元素,需要由缓存的key + hash的key-field/** * 获取hash中field对应的值 * * @param key * @param field * @return /public String hget(String key, String field) { Object val = redisTemplate.opsForHash().get(key, field); return val == null ? null : val.toString();}2. 添加元素/* * 添加or更新hash的值 * * @param key * @param field * @param value /public void hset(String key, String field, String value) { redisTemplate.opsForHash().put(key, field, value);}3. 删除hash最好的一个地方,我个人感觉就是在删除时特别方便,比如将同类的数据聚集在一个hash中,删除key就可以实现全部都删除,清理数据就比较方便了;除此之外,另外一种就是删除hash中的部分key/* * 删除hash中field这一对kv * * @param key * @param field /public void hdel(String key, String field) { redisTemplate.opsForHash().delete(key, field);}4. 批量查询批量查询有两种,一个是全部捞出来,一个是捞出指定key的相关数据public Map<String, String> hgetall(String key) { return redisTemplate.execute((RedisCallback<Map<String, String>>) con -> { Map<byte[], byte[]> result = con.hGetAll(key.getBytes()); if (CollectionUtils.isEmpty(result)) { return new HashMap<>(0); } Map<String, String> ans = new HashMap<>(result.size()); for (Map.Entry<byte[], byte[]> entry : result.entrySet()) { ans.put(new String(entry.getKey()), new String(entry.getValue())); } return ans; });}public Map<String, String> hmget(String key, List<String> fields) { List<String> result = redisTemplate.<String, String>opsForHash().multiGet(key, fields); Map<String, String> ans = new HashMap<>(fields.size()); int index = 0; for (String field : fields) { if (result.get(index) == null) { continue; } ans.put(field, result.get(index)); } return ans;}5. 自增hash的value如果是数字,提供了一个自增的方式,和String中的incr/decr差不多的效果// hash 结构的计数public long hincr(String key, String field, long value) { return redisTemplate.opsForHash().increment(key, field, value);}6. hash + listhash的value如果另外一种场景就是数组,目前没有找到特别友好的操作方式,只能在业务层进行兼容/* * value为列表的场景 * * @param key * @param field * @return */public <T> List<T> hGetList(String key, String field, Class<T> obj) { Object value = redisTemplate.opsForHash().get(key, field); if (value != null) { return JSONObject.parseArray(value.toString(), obj); } else { return new ArrayList<>(); }}public <T> void hSetList(String key, String field, List<T> values) { String v = JSONObject.toJSONString(values); redisTemplate.opsForHash().put(key, field, v);}II. 其他0. 项目工程:spring-boot-demo1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

December 28, 2018 · 2 min · jiezi

SpringBoot之RedisTemplate List数据结构使用教程

更多Spring文章,欢迎点击 一灰灰Blog-Spring专题前面一篇博文介绍redis五种数据结构中String的使用姿势,这一篇则将介绍另外一个用的比较多的List,对于列表而言,用的最多的场景可以说是当做队列或者堆栈来使用了<!– more –>原文连接: 181109-SpringBoot高级篇Redis之List数据结构使用姿势I. 基本使用1. 序列化指定前面一篇的操作都是直接使用的execute配合回调方法来说明的,其实还有一种更加方便的方式,即 opsForValue, opsForList,本文则以这种方式演示list数据结构的操作所以在正式开始之前,有必要指定一下key和value的序列化方式,当不现实指定时,采用默认的序列化(即jdk的对象序列化方式),直接导致的就是通过redis-cli获取存储数据时,会发现和你预期的不一样首先实现序列化类public class DefaultSerializer implements RedisSerializer<Object> { private final Charset charset; public DefaultSerializer() { this(Charset.forName(“UTF8”)); } public DefaultSerializer(Charset charset) { Assert.notNull(charset, “Charset must not be null!”); this.charset = charset; } @Override public byte[] serialize(Object o) throws SerializationException { return o == null ? null : String.valueOf(o).getBytes(charset); } @Override public Object deserialize(byte[] bytes) throws SerializationException { return bytes == null ? null : new String(bytes, charset); }}其次定义RedisTemplate的序列化方式@Configurationpublic class AutoConfig { @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, String> redis = new RedisTemplate<>(); redis.setConnectionFactory(redisConnectionFactory); // 设置redis的String/Value的默认序列化方式 DefaultSerializer stringRedisSerializer = new DefaultSerializer(); redis.setKeySerializer(stringRedisSerializer); redis.setValueSerializer(stringRedisSerializer); redis.setHashKeySerializer(stringRedisSerializer); redis.setHashValueSerializer(stringRedisSerializer); redis.afterPropertiesSet(); return redis; }}2. 添加元素对于list而言,添加元素常见的有两种,从左边加和从右边加,以lpush为例/** * 在列表的最左边塞入一个value * * @param key * @param value /public void lpush(String key, String value) { redisTemplate.opsForList().leftPush(key, value);}3. 获取元素既然是list,就是有序的,因此完全是可以向jdk的list容器一样,获取指定索引的值/* * 获取指定索引位置的值, index为-1时,表示返回的是最后一个;当index大于实际的列表长度时,返回null * * @param key * @param index * @return /public String index(String key, int index) { return redisTemplate.opsForList().index(key, index);}与jdk中的List获取某个索引value不同的是,这里的index可以为负数,-1表示最右边的一个,-2则表示最右边的第二个,依次4. 范围查询这个查询就类似JDK容器中的List#subList了,查询指定范围的列表/* * 获取范围值,闭区间,start和end这两个下标的值都会返回; end为-1时,表示获取的是最后一个; * * 如果希望返回最后两个元素,可以传入 -2, -1 * * @param key * @param start * @param end * @return /public List<String> range(String key, int start, int end) { return redisTemplate.opsForList().range(key, start, end);}5. 列表长度/* * 返回列表的长度 * * @param key * @return /public Long size(String key) { return redisTemplate.opsForList().size(key);}6. 修改更新List中某个下标的value,也属于比较常见的case了,/* * 设置list中指定下标的值,采用干的是替换规则, 最左边的下标为0;-1表示最右边的一个 * * @param key * @param index * @param value /public void set(String key, Integer index, String value) { redisTemplate.opsForList().set(key, index, value);}7. 删除在接口中没有看到删除指定小标的元素,倒是看到可以根据value进行删除,以及控制列表长度的方法/* * 删除列表中值为value的元素,总共删除count次; * * 如原来列表为 【1, 2, 3, 4, 5, 2, 1, 2, 5】 * 传入参数 value=2, count=1 表示删除一个列表中value为2的元素 * 则执行后,列表为 【1, 3, 4, 5, 2, 1, 2, 5】 * * @param key * @param value * @param count /public void remove(String key, String value, int count) { redisTemplate.opsForList().remove(key, count, value);}/* * 删除list首尾,只保留 [start, end] 之间的值 * * @param key * @param start * @param end */public void trim(String key, Integer start, Integer end) { redisTemplate.opsForList().trim(key, start, end);}个人感觉在实际的使用中remove这个方法用得不太多;但是trim方法则比较有用了,特别是在控制list的长度,避免出现非常大的列表时,很有效果,传入的start/end参数,采用的是闭区间的原则II. 其他0. 项目工程:spring-boot-demo1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

December 28, 2018 · 2 min · jiezi

Spring AOP 增强框架 Nepxion Matrix 详解

概述在《深入聊一聊 Spring AOP 实现机制》一文中,介绍了 Spring AOP 的多种实现机制,原理大同小异。本篇来继续介绍一款开源的 AOP 框架:Nepxion Matrix,该框架致力于对 Spring AOP 的扩展和增强,灵活而且易用。Matrix 框架主要对 Spring 做了三个模块的扩展:Spring AutoProxy,Spring Registrar,Spring Selectror。本篇主要分析 AOP相关的功能,也就是 AutoProxy 模块。主要围绕以下几个方面:Nepxion Matrix AutoProxy框架有什么特性?Nepxion Matrix AutoProxyAOP 增强框架的扩展点什么?如何扩展?源码分析。该框架和Spring AOP异同点。一:Nepxion Matrix AutoProxy特性大多数的项目中,使用Spring AOP的方式都是采用注解形式,方法上面加个自定义注解即可实现,这种方式注解只能加在类方法上,不能加在接口或接口方法上。Nepxion Matrix AutoProxy主要针对这些问题,特性如下:支持通用代理和额外代理支持接口代理和类代理支持接口方法代理这里要介绍一下上面提到了两种代理方式:通用代理是指通过AbstractAutoProxyCreator中的变量interceptorNames,来设置具体的通知名称。 额外代理是指通过实现自定义逻辑,来选择性设置通知(这里的通知也就是拦截方法)。二:Nepxion Matrix AutoProxy扩展点要理解该框架的实现方式,首先要知道该框架的扩展点是什么。先来看一下代理机制相关的 UML 图:AbstractAutoProxyCreator抽象类为Spring AOP暴露的抽象扩展类,其每一个实现类都代表着一种实现机制。Nepxion Matrix也正是基于此类做为扩展点,分别来看一下涉及到核心类:AbstractAutoScanProxy:Nepxion Matrix提供的核心抽象类,封装了获取顾问advisor的方法,并暴露了一些抽象方法,如获取通知,注解等方法。该类同 Spring 内置的代理机制AbstractAdvisorAutoProxyCreator平级,默认先执行Spring AOP内置代理机制。DefaultAutoScanProxy:提供了一些默认为空的实现,不能直接使用。MyAutoScanProxyForClass:类代理机制,提供通用代理实现。MyAutoScanProxyForMethod:方法代理机制,提供额外代理。MyAutoScanProxy:混合代理,提供通用代理和额外代理。三:源码分析这里就针对类代理的方式,进行源码分析。先来看源码中的使用示例:@MyAnnotation1(name = “MyAnnotation1”, label = “MyAnnotation1”,description = “MyAnnotation1”)public interface MyService1 {void doA(String id);void doB(String id);}@Servicepublic class MyService1Impl implements MyService1 {@Overridepublic void doA(String id) {System.out.println(“doA”);}@Overridepublic void doB(String id) {System.out.println(“doB”);}}示例中只需在接口上添加一个自定义注解@MyAnnotation1,即可满足两个实现方法都会走代理方法。源码中还有其他几种使用示例,这里就不列举了,具体可以参考项目wiki。首先来看一下AbstractAutoScanProxy的构造方法:public AbstractAutoScanProxy(String[] scanPackages, ProxyModeproxyMode, ScanMode scanMode, boolean exposeProxy) {//设置代理目录,非指定目录直接返回this.scanPackages = scanPackages;//Spring提供的是否暴露代理对象标识。this.setExposeProxy(exposeProxy);//代理模式,类代理或是方法代理。this.proxyMode = proxyMode;this.scanMode = scanMode;//……// 设定全局拦截器,通过名称指定。// 如果同时设置了全局和额外的拦截器,那么它们都同时工作,全局拦截器先运行,额外拦截器后运行Class<? extends MethodInterceptor>[] commonInterceptorClasses =getCommonInterceptors();String[] commonInterceptorNames = getCommonInterceptorNames();String[] interceptorNames = ArrayUtils.addAll(commonInterceptorNames,convertInterceptorNames(commonInterceptorClasses));if (ArrayUtils.isNotEmpty(interceptorNames)) {setInterceptorNames(interceptorNames);}}构造方法中有两个变量比较重要:exposeProxy:默认为false,这里设置为 true,支持在同一个类中,一个方法调用另一个方法走代理拦截方法。 比如,类中方法1调用方法2,开启该变量,则不会直接调用方法2,而是从 threadLocal 中取出提前存入的代理类发起调用。interceptorNames:通知名称,也就是通用代理,通过构造方法设置。在后面生成代理类的方法中会根据该变量取出所有拦截器实例。我们来看一下代理执行入口。因为该类继承beanPostProcessor,所以最终会执行扩展接口postProcessAfterInitialization,在该方法中调用模板方法getAdvicesAndAdvisorsForBean,来看一下Nepxion Matrix对该方法的实现:protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass,String beanName, TargetSource targetSource) {boolean scanPackagesEnabled = scanPackagesEnabled();// scanPackagesEnabled=false,表示“只扫描指定目录”的方式未开启,则不会对扫描到的bean进行代理预先判断if (scanPackagesEnabled) {boolean scanPackagesContained = scanPackagesContained(beanClass);// 如果beanClass的类路径,未包含在扫描目录中,返回DO_NOT_PROXYif (!scanPackagesContained) {return DO_NOT_PROXY;}}// 根据Bean名称获取Bean对象Object bean = beanMap.get(beanName);// 获取最终目标类,Class<?> targetClass = null;if (bean != null / && AopUtils.isCglibProxy(bean) /) {targetClass = AopProxyUtils.ultimateTargetClass(bean);} else {targetClass = beanClass;}// Spring容器扫描实现类if (!targetClass.isInterface()) {// 扫描接口(从实现类找到它的所有接口)if (targetClass.getInterfaces() != null) {for (Class<?> targetInterface : targetClass.getInterfaces()) {Object[] proxyInterceptors = scanAndProxyForTarget(targetInterface,beanName, false);if (proxyInterceptors != DO_NOT_PROXY) {return proxyInterceptors;}}}// 扫描实现类(如果接口上没找到注解, 就找实现类的注解)Object[] proxyInterceptors = scanAndProxyForTarget(targetClass,beanName, true);if (proxyInterceptors != DO_NOT_PROXY) {return proxyInterceptors;}}return DO_NOT_PROXY;}上面逻辑中调用了AopProxyUtils.ultimateTargetClass(bean)来获取对应的 class 对象,而不是使用参数中的beanClass。因为方法传进来的 class 对象有可能是被代理过的 class,所以这里要获取最初的 class 对象。继续跟进scanAndProxyForTarget方法:protected Object[] scanAndProxyForTarget(Class<?> targetClass, StringbeanName, boolean proxyTargetClass) {String targetClassName = targetClass.getCanonicalName();//这里获取额外代理Object[] interceptors = getInterceptors(targetClass);// 排除java开头的接口,例如java.io.Serializable,java.io.Closeable等,执行不被代理if (StringUtils.isNotEmpty(targetClassName) &&!targetClassName.startsWith(“java.”)) {// 避免对同一个接口或者类扫描多次Boolean proxied = proxyMap.get(targetClassName);if (proxied != null) {if (proxied) {return interceptors;}} else {Object[] proxyInterceptors = null;switch (proxyMode) {// 只通过扫描到接口名或者类名上的注解后,来确定是否要代理case BY_CLASS_ANNOTATION_ONLY:proxyInterceptors = scanAndProxyForClass(targetClass, targetClassName,beanName, interceptors, proxyTargetClass);break;// 只通过扫描到接口或者类方法上的注解后,来确定是否要代理case BY_METHOD_ANNOTATION_ONLY:proxyInterceptors = scanAndProxyForMethod(targetClass,targetClassName, beanName, interceptors, proxyTargetClass);break;// 上述两者都可以case BY_CLASS_OR_METHOD_ANNOTATION:Object[] classProxyInterceptors = scanAndProxyForClass(targetClass,targetClassName, beanName, interceptors, proxyTargetClass);// 没有接口或者类名上扫描到目标注解,那么扫描接口或者类的方法上的目标注解Object[] methodProxyInterceptors = scanAndProxyForMethod(targetClass,targetClassName, beanName, interceptors, proxyTargetClass);if (classProxyInterceptors != DO_NOT_PROXY || methodProxyInterceptors!= DO_NOT_PROXY) {proxyInterceptors = interceptors;} else {proxyInterceptors = DO_NOT_PROXY;}break;}// 是否需要代理proxyMap.put(targetClassName, Boolean.valueOf(proxyInterceptors !=DO_NOT_PROXY));return proxyInterceptors;}}return DO_NOT_PROXY;}大致的思路:根据MyService1Impl获取到接口MyService1,然后判断接口上是否有指定的注解@MyAnnotation1,判断条件符合,然后调用getInterceptors方法获取拦截器,传递到父类AbstractAutoProxyCreator中的方法createProxy中,完成代理。推荐一个Java进阶架构学习交流:952124565,群内有分布式架构、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Netty、Jvm等视频资料提供学习参考。跟进getInterceptors方法来看一下:protected Object[] getInterceptors(Class<?> targetClass) {Object[] interceptors = getAdditionalInterceptors(targetClass);if (ArrayUtils.isNotEmpty(interceptors)) {return interceptors;}Class<? extends MethodInterceptor>[] commonInterceptorClasses =getCommonInterceptors();String[] commonInterceptorNames = getCommonInterceptorNames();if (ArrayUtils.isNotEmpty(commonInterceptorClasses) ||ArrayUtils.isNotEmpty(commonInterceptorNames)) {return PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS;}return DO_NOT_PROXY;}这里先获取所有的额外代理拦截器,如果有直接返回。如果为空,则返回一个是否有通用代理拦截器的标识,具体拦截器的名称上面已经通过构造方法传入。再来看一下在createProxy方法:protected Object createProxy( Class<?> beanClass, String beanName,Object[] specificInterceptors, TargetSource targetSource) { if(this.beanFactory instanceof ConfigurableListableBeanFactory) { AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)this.beanFactory, beanName, beanClass); } ProxyFactory proxyFactory= new ProxyFactory(); proxyFactory.copyFrom(this); //判断代理生成方式 if (!proxyFactory.isProxyTargetClass()) { if(shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { evaluateProxyInterfaces(beanClass, proxyFactory); } } //获取拦截器,包括通用代理和额外代理 Advisor[] advisors = buildAdvisors(beanName,specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); proxyFactory.setFrozen(this.freezeProxy); if (advisorsPreFiltered()){ proxyFactory.setPreFiltered(true); } returnproxyFactory.getProxy(getProxyClassLoader()); }Nepxion Matrix重写了上面的shouldProxyTargetClass(beanClass, beanName)方法,重写逻辑如下:protected boolean shouldProxyTargetClass(Class<?> beanClass, StringbeanName) { // 设置不同场景下的接口代理,还是类代理 Boolean proxyTargetClass =proxyTargetClassMap.get(beanName); if (proxyTargetClass != null) { return proxyTargetClass; } returnsuper.shouldProxyTargetClass(beanClass, beanName); }需要注意的是,上述重写方式只在SpringBoot 1.x中生效,因为在 2.x版本中,proxyFactory.isProxyTargetClass()默认为 true,默认走 cglib 代理,所以默认情况下上述重写的方法不会执行。继续跟进获取拦截器的方法buildAdvisors:protected Advisor[] buildAdvisors(String beanName, Object[]specificInterceptors) { //解析通用代理拦截器 Advisor[] commonInterceptors =resolveInterceptorNames(); List<Object> allInterceptors = newArrayList<Object>(); //判断是否需要额外代理,是否有指定拦截器 if (specificInterceptors!= null) { allInterceptors.addAll(Arrays.asList(specificInterceptors)); if(commonInterceptors.length > 0) { if(this.applyCommonInterceptorsFirst) { allInterceptors.addAll(0,Arrays.asList(commonInterceptors)); } else { allInterceptors.addAll(Arrays.asList(commonInterceptors)); } } } Advisor[] advisors = new Advisor[allInterceptors.size()]; for (int i= 0; i < allInterceptors.size(); i++) { advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i)); } returnadvisors; }通过调用resolveInterceptorNames,根据interceptorNames中设置的拦截器名称,从Spring容器中取出所有的通用代理拦截器,结合指定拦截器specificInterceptors,一起织入代理类。Nepxion Matrix AutoProxy中的方法代理这里就不展开了,原理类似。四:Nepxion Matrix AutoProxy 和 Spring AOP 异同点1)代理机制原理一样,都是AbstractAutoScanProxy的实现类,只是代理功能点不同。2)两种代理机制可同时使用。如果同时使用,一定保证Spring AOP先代理,Nepxion Matrix AutoProxy后代理。这也是默认的代理顺序。尽量不要通过重写Ordered接口的方式改变先后顺序。原因是采用Spring AOP注解形式时需要获取代理类最初的 Class 对象,如果Nepxion Matrix AutoProxy先执行,那么在执行Spring AOP代理逻辑时获取到的当前 Class 对象就是被代理过重新生成的 Class 对象,这时就无法获取自定义的切面注解了。 ...

December 28, 2018 · 3 min · jiezi

Spring Bean注入/单例理解/循环依赖

理解循环依赖问题,首先明白spring有四种注入方式。第一种,SET注入a类中持有b类的引用,并且a类有b的set方法。在bean中添加<property>标签即可注入。实质上是将b实例化,然后调用set方法注入。 <bean id=“a” class=“com.qunar.pojo.StudentA” scope=“singleton”> <property name=“studentB” ref=“b”></property> </bean>第二种,构造器注入a类中持有b类的引用,并且a的构造函数参数中有b。实质上就是通过构造函数注入,创建a对象时要把b对象传进去。 <bean id=“a” class=“com.qunar.pojo.StudentA”> <constructor-arg index=“0” ref=“b”></constructor-arg> </bean>第三种,静态工厂如果有需要静态工厂实例化的类,不能通过静态工厂.方法实现。在bean属性中对应类指向静态工厂,对应方法指向返回实例的方法图片描述第四种,实例工厂如果工厂不是静态,需要实例化,就实例化对应工厂,设定factory-bean和factory-method进行方法调用。图片描述设定三个实体类,StudentA,B,C代码如下,A持有B,B持有C,C持有Apublic class StudentA { private StudentB studentB ; public void setStudentB(StudentB studentB) { this.studentB = studentB; } public StudentA() { } public StudentA(StudentB studentB) { this.studentB = studentB; }}当我通过构造器注入时,会产生BeanCurrentlyInCreationException异常。为什么会出现这种异常,spring如何加载实体?图片描述Spring容器会将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖;而对于创建完毕的Bean将从“当前创建Bean池”中清除掉。Spring容器先创建单例StudentA,StudentA依赖StudentB,然后将A放在“当前创建Bean池”中,此时创建StudentB,StudentB依赖StudentC ,然后将B放在“当前创建Bean池”中,此时创建StudentC,StudentC又依赖StudentA, 但是,此时Student已经在池中,所以会报错,因为在池中的Bean都是未初始化完的,所以会依赖错误.解决这个问题,可以用setter注入的方式。图片描述Spring是先将Bean对象实例化之后,再设置对象属性。所以会先调用他的无参构造函数实例化。每个对象存在一个map中。当遇到依赖,就去map中调用对应的单例对象。图片描述一部分源码另外: 对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。Spring装配Bean的过程实例化;设置属性值;如果实现了BeanNameAware接口,调用setBeanName设置Bean的ID或者Name;如果实现BeanFactoryAware接口,调用setBeanFactory 设置BeanFactory;如果实现ApplicationContextAware,调用setApplicationContext设置ApplicationContext调用BeanPostProcessor的预先初始化方法;调用InitializingBean的afterPropertiesSet()方法;调用定制init-method方法;调用BeanPostProcessor的后初始化方法;Spring容器关闭过程调用DisposableBean的destroy();调用定制的destroy-method方法;图片描述了解了bean默认是单例模式,不由想spring的单例和设计模式单例同一种吗?其实不一样。单例模式是指在一个JVM进程中仅有一个实例,而Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。如果有多个Spring容器,可能有多个Bean对象。spring单例是一种类似注册表实现的方式。利用hashmap,向map中注册和取值,思路类似下面代码public class Singleton { private static Map<String,Singleton> map = new HashMap<String,Singleton>(); static{ Singleton single = new Singleton(); map.put(single.getClass().getName(), single); } //保护的默认构造子 protected Singleton(){} //静态工厂方法,返还此类惟一的实例 public static Singleton getInstance(String name) { if(name == null) { name = Singleton.class.getName(); } if(map.get(name) == null) { try { map.put(name, (Singleton) Class.forName(name).newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return map.get(name); }} ...

December 28, 2018 · 1 min · jiezi

Spring Boot 静态资源文件配置 A卷

Spring Boot 静态资源文件配置说在前面的话:创建SpringBoot应用,选中我们需要的模块SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来自己编写业务代码由于 Spring Boot 采用了”约定优于配置”这种规范,所以在使用静态资源的时候也很简单。SpringBoot本质上是为微服务而生的,以JAR的形式启动运行,但是有时候静态资源的访问是必不可少的,比如:image、js、css 等资源的访问1.webjars配置静态路径简单了解即可,感觉实用性不大,public class WebMvcAutoConfiguration { public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug(“Default resource handling disabled”); } else { Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); if (!registry.hasMappingForPattern("/webjars/")) { this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/"}).addResourceLocations(new String[]{“classpath:/META-INF/resources/webjars/”}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl)); } String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl)); } } }}代码分析: 所有 /webjars/** ,都去 classpath:/META-INF/resources/webjars/ 找资源;webjars:以jar包的方式引入静态资源; webjars提供的依赖官网<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>1.12.4</version></dependency>启动服务,测试访问静态地址http://127.0.0.1:8001/hp/webjars/jquery/1.12.4/jquery.js2.默认静态资源路径@ConfigurationProperties( prefix = “spring.resources”, ignoreUnknownFields = false)public class ResourceProperties { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{“classpath:/META-INF/resources/”, “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”}; private String[] staticLocations; private boolean addMappings; private final ResourceProperties.Chain chain; private final ResourceProperties.Cache cache; public ResourceProperties() { this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; this.addMappings = true; this.chain = new ResourceProperties.Chain(); this.cache = new ResourceProperties.Cache(); } public String[] getStaticLocations() { return this.staticLocations; }}摘抄了部分源码,算是为了增加篇幅,从上述代码中我们可以看到,提供了几种默认的配置方式classpath:/staticclasspath:/publicclasspath:/resourcesclasspath:/META-INF/resources备注说明: “/"=>当前项目的根路径我们在src/main/resources目录下新建 public、resources、static 、META-INF等目录目录,并分别放入 1.jpg 2.jpg 3.jpg 4.jpg 5.jpg 五张图片。注意:需要排除webjars的形式,将pom.xml中的代码去掉,在进行测试结果结果如下3.新增静态资源路径我们在spring.resources.static-locations后面追加一个配置classpath:/os/:# 静态文件请求匹配方式spring.mvc.static-path-pattern=/# 修改默认的静态寻址资源目录 多个使用逗号分隔spring.resources.static-locations = classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/os/4.自定义静态资源映射在实际开发中,我们可能需要自定义静态资源访问以及上传路径,特别是文件上传,不可能上传的运行的JAR服务中,那么可以通过继承WebMvcConfigurerAdapter来实现自定义路径映射。application.properties 文件配置:# 图片音频上传路径配置(win系统自行变更本地路径)web.upload.path=D:/upload/attr/Demo05BootApplication.java 启动配置:package com.hanpang;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@SpringBootApplicationpublic class Demo05BootApplication implements WebMvcConfigurer { private final static Logger LOGGER = LoggerFactory.getLogger(Demo05BootApplication.class); @Value("${web.upload.path}”) private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/").addResourceLocations( “file:” + uploadPath); LOGGER.info(“自定义静态资源目录、此处功能用于文件映射”); } public static void main(String[] args) { SpringApplication.run(Demo05BootApplication.class, args); }}5.设置欢迎界面依然从源码出手来解决这个问题public class WebMvcAutoConfiguration { private Optional<Resource> getWelcomePage() { String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations()); return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst(); } private Resource getIndexHtml(String location) { return this.resourceLoader.getResource(location + “index.html”); }}5.1 直接设置静态默认页面欢迎页; 静态资源文件夹下的所有index.html页面,被"/“映射;5.2 增加控制器的方式新增模版引擎的支持<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>配置核心文件application.propertiesserver.port=8001server.servlet.context-path=/hpspring.mvc.view.prefix=classpath:/templates/没有去设置后缀名设置增加路由@Controllerpublic class IndexController { @GetMapping({”/","/index"}) public String index(){ return “default”; }}访问http://127.0.0.1:8001/hp/5.3 设置默认的View跳转页面新增模版引擎的支持<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>配置核心文件application.propertiesserver.port=8001server.servlet.context-path=/hpspring.mvc.view.prefix=classpath:/templates/没有去设置后缀名启动文件的修改如下@SpringBootApplicationpublic class Demo05BootApplication implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName(“default”); registry.addViewController("/index1").setViewName(“default”); registry.setOrder(Ordered.HIGHEST_PRECEDENCE); } public static void main(String[] args) { SpringApplication.run(Demo05BootApplication.class, args); }}6. Favicon设置 @Configuration @ConditionalOnProperty( value = {“spring.mvc.favicon.enabled”}, matchIfMissing = true ) public static class FaviconConfiguration implements ResourceLoaderAware { private final ResourceProperties resourceProperties; private ResourceLoader resourceLoader; public FaviconConfiguration(ResourceProperties resourceProperties) { this.resourceProperties = resourceProperties; } public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Bean public SimpleUrlHandlerMapping faviconHandlerMapping() { SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setOrder(-2147483647); mapping.setUrlMap(Collections.singletonMap("/favicon.ico", this.faviconRequestHandler())); return mapping; } @Bean public ResourceHttpRequestHandler faviconRequestHandler() { ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); requestHandler.setLocations(this.resolveFaviconLocations()); return requestHandler; } private List<Resource> resolveFaviconLocations() { String[] staticLocations = WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.getResourceLocations(this.resourceProperties.getStaticLocations()); List<Resource> locations = new ArrayList(staticLocations.length + 1); Stream var10000 = Arrays.stream(staticLocations); ResourceLoader var10001 = this.resourceLoader; var10001.getClass(); var10000.map(var10001::getResource).forEach(locations::add); locations.add(new ClassPathResource("/")); return Collections.unmodifiableList(locations); } }SpringBoot 默认是开启Favicon,并且提供了一个默认的Favicon,如果想关闭Favicon,只需要在application.properties中添加spring.mvc.favicon.enabled=false如果想更改Favicon,只需要将自己的Favicon.ico(文件名不能改动),放置到类路径根目录、类路径META_INF/resources/下、类路径resources/下、类路径static/下或者类路径public/下。5.附录A.WebMvcConfigurerAdapter过时在Springboot中配置WebMvcConfigurerAdapter的时候发现这个类过时了。所以看了下源码,发现官方在spring5弃用了WebMvcConfigurerAdapter,因为springboot2.0使用的spring5,所以会出现过时。WebMvcConfigurerAdapter已经过时,在新版本中被废弃,以下是比较常用的重写接口:/** 解决跨域问题 /public void addCorsMappings(CorsRegistry registry) ;/ 添加拦截器 /void addInterceptors(InterceptorRegistry registry);/ 这里配置视图解析器 /void configureViewResolvers(ViewResolverRegistry registry);/ 配置内容裁决的一些选项 /void configureContentNegotiation(ContentNegotiationConfigurer configurer);/ 视图跳转控制器 /void addViewControllers(ViewControllerRegistry registry);/ 静态资源处理 /void addResourceHandlers(ResourceHandlerRegistry registry);/ 默认静态资源处理器 /void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);方案1:直接实现WebMvcConfigurer@Configurationpublic class WebMvcConfg implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName(“index”); }}方案2:直接继承WebMvcConfigurationSupport@Configurationpublic class WebMvcConfg extends WebMvcConfigurationSupport { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName(“index”); }}其实,源码下WebMvcConfigurerAdapter是实现WebMvcConfigurer接口,所以直接实现WebMvcConfigurer接口也可以;WebMvcConfigurationSupport与WebMvcConfigurerAdapter、接口WebMvcConfigurer处于同一个目录下WebMvcConfigurationSupport包含WebMvcConfigurer里面的方法,由此看来版本中应该是推荐使用WebMvcConfigurationSupport类的,WebMvcConfigurationSupport应该是新版本中对WebMvcConfigurerAdapter的替换和扩展B.关于加载目录问题// 可以直接使用addResourceLocations 指定磁盘绝对路径,同样可以配置多个位置,注意路径写法需要加上file:registry.addResourceHandler("/myimgs/").addResourceLocations(“file:H:/myimgs/”);// 访问myres根目录下的fengjing.jpg 的URL为 http://localhost:8080/fengjing.jpg (/** 会覆盖系统默认的配置)registry.addResourceHandler("/**").addResourceLocations(“classpath:/myres/”).addResourceLocations(“classpath:/static/”); ...

December 28, 2018 · 2 min · jiezi

阿里专家杜万:Java响应式编程,一文全面解读

本篇文章来自于2018年12月22日举办的《阿里云栖开发者沙龙—Java技术专场》,杜万专家是该专场第四位演讲的嘉宾,本篇文章是根据杜万专家在《阿里云栖开发者沙龙—Java技术专场》的演讲视频以及PPT整理而成。摘要:响应式宣言如何解读,Java中如何进行响应式编程,Reactor Streams又该如何使用?热衷于整合框架与开发工具的阿里云技术专家杜万,为大家全面解读响应式编程,分享Spring Webflux的实践。从响应式理解,到Reactor项目示例,再到Spring Webflux框架解读,本文带你进入Java响应式编程。演讲嘉宾简介:杜万(倚贤),阿里云技术专家,全栈工程师,从事了12年 Java 语言为主的软件开发工作,热衷于整合框架与开发工具,Linux拥趸,问题终结者。合作翻译《Elixir 程序设计》。目前负责阿里云函数计算的工具链开发,正在实践 WebFlux 和 Reactor 开发新的 Web 应用。本次直播视频精彩回顾,戳这里!https://yq.aliyun.com/live/721PPT下载地址:https://yq.aliyun.com/download/3187以下内容根据演讲嘉宾视频分享以及PPT整理而成。本文围绕以下三部分进行介绍:1.Reactive2.Project Reactor3.Spring Webflux一.Reactive1.Reactive Manifesto下图是Reactive Manifesto官方网站上的介绍,这篇文章非常短但也非常精悍,非常值得大家去认真阅读。响应式宣言是一份构建现代云扩展架构的处方。这个框架主要使用消息驱动的方法来构建系统,在形式上可以达到弹性和韧性,最后可以产生响应性的价值。所谓弹性和韧性,通俗来说就像是橡皮筋,弹性是指橡皮筋可以拉长,而韧性指在拉长后可以缩回原样。这里为大家一一解读其中的关键词:1)响应性:快速/一致的响应时间。假设在有500个并发操作时,响应时间为1s,那么并发操作增长至5万时,响应时间也应控制在1s左右。快速一致的响应时间才能给予用户信心,是系统设计的追求。2)韧性:复制/遏制/隔绝/委托。当某个模块出现问题时,需要将这个问题控制在一定范围内,这便需要使用隔绝的技术,避免连锁性问题的发生。或是将出现故障部分的任务委托给其他模块。韧性主要是系统对错误的容忍。3)弹性:无竞争点或中心瓶颈/分片/扩展。如果没有状态的话,就进行水平扩展,如果存在状态,就使用分片技术,将数据分至不同的机器上。4)消息驱动:异步/松耦合/隔绝/地址透明/错误作为消息/背压/无阻塞。消息驱动是实现上述三项的技术支撑。其中,地址透明有很多方法。例如DNS提供的一串人类能读懂的地址,而不是IP,这是一种不依赖于实现,而依赖于声明的设计。再例如k8s每个service后会有多个Pod,依赖一个虚拟的服务而不是某一个真实的实例,从何实现调用1 个或调用n个服务实例对于对调用方无感知,这是为分片或扩展做了准备。错误作为消息,这在Java中是不太常见的,Java中通常将错误直接作为异常抛出,而在响应式中,错误也是一种消息,和普通消息地位一致,这和JavaScript中的Promise类似。背压是指当上游向下游推送数据时,可能下游承受能力不足导致问题,一个经典的比喻是就像用消防水龙头解渴。因此下游需要向上游声明每次只能接受大约多少量的数据,当接受完毕再次向上游申请数据传输。这便转换成是下游向上游申请数据,而不是上游向下游推送数据。无阻塞是通过no-blocking IO提供更高的多线程切换效率。2.Reactive Programming响应式编程是一种声明式编程范型。下图中左侧显示了一个命令式编程,相信大家都比较熟悉。先声明两个变量,然后进行赋值,让两个变量相加,得到相加的结果。但接着当修改了最早声明的两个变量的值后,sum的值不会因此产生变化。而在Java 9 Flow中,按相同的思路实现上述处理流程,当初始变量的值变化,最后结果的值也同步发生变化,这就是响应式编程。这相当于声明了一个公式,输出值会随着输入值而同步变化。响应式编程也是一种非阻塞的异步编程。下图是用reactor.ipc.netty实现的TCP通信。常见的server中会用循环发数据后,在循环外取出,但在下图的实现中没有,因为这不是使用阻塞模型实现,是基于非阻塞的异步编程实现。响应式编程是一种数据流编程,关注于数据流而不是控制流。下图中,首先当页面出现点击操作时产生一个click stream,然后页面会将250ms内的clickStream缓存,如此实现了一个归组过程。然后再进行map操作,得到每个list的长度,筛选出长度大于2的,这便可以得出多次点击操作的流。这种方法应用非常广泛,例如可以筛选出双击操作。由此可见,这种编程方式是一种数据流编程,而不是if else的控制流编程。之前有提及消息驱动,那么消息驱动(Message-driven)和事件驱动(Event-driven)有什么区别呢。1)消息驱动有确定的目标,一定会有消息的接受者,而事件驱动是一件事情希望被观察到,观察者是谁无关紧要。消息驱动系统关注消息的接受者,事件驱动系统关注事件源。2)在一个使用响应式编程实现的响应式系统中,消息擅长于通讯,事件擅长于反应事实。3.Reactive StreamsReactive Streams提供了一套非阻塞背压的异步流处理标准,主要应用在JVM、JavaScript和网络协议工作中。通俗来说,它定义了一套响应式编程的标准。在Java中,有4个Reactive Streams API,如下图所示:这个API中定义了Publisher,即事件的发生源,它只有一个subscribe方法。其中的Subscriber就是订阅消息的对象。作为订阅者,有四个方法。onSubscribe会在每次接收消息时调用,得到的数据都会经过onNext方法。onError方法会在出现问题时调用,Throwable即是出现的错误消息。在结束时调用onComplete方法。Subscription接口用来描述每个订阅的消息。request方法用来向上游索要指定个数的消息,cancel方法用于取消上游的数据推送,不再接受消息。Processor接口继承了Subscriber和Publisher,它既是消息的发生者也是消息的订阅者。这是发生者和订阅者间的过渡桥梁,负责一些中间转换的处理。Reactor Library从开始到现在已经历经多代。第0代就是java包Observable 接口,也就是观察者模式。具体的发展见下图:第四代虽然仍然是RxJava2,但是相比第三代的RxJava2,其中的小版本有了不一样的改进,出现了新特性。Reactor Library主要有两点特性。一是基于回调(callback-based),在事件源附加回调函数,并在事件通过数据流链时被调用;二是声明式编程(Declarative),很多函数处理业务类似,例如map/filter/fold等,这些操作被类库固化后便可以使用声明式方法,以在程序中快速便捷使用。在生产者、订阅者都定义后,声明式方法便可以用来实现中间处理者。二.Project ReactorProject Reactor,实现了完全非阻塞,并且基于网络HTTP/TCP/UDP等的背压,即数据传输上游为网络层协议时,通过远程调用也可以实现背压。同时,它还实现了Reactive Streams API和Reactive Extensions,以及支持Java 8 functional API/Completable Future/Stream /Duration等各新特性。下图所示为Reactor的一个示例:首先定义了一个words的数组,然后使用flatMap做映射,再将每个词和s做连接,得出的结果和另一个等长的序列进行一个zipWith操作,最后打印结果。这和Java 8 Stream非常类似,但仍存在一些区别:1)Stream是pull-based,下游从上游拉数据的过程,它会有中间操作例如map和reduce,和终止操作例如collect等,只有在终止操作时才会真正的拉取数据。Reactive是push-based,可以先将整个处理数据量构造完成,然后向其中填充数据,在出口处可以取出转换结果。2)Stream只能使用一次,因为它是pull-based操作,拉取一次之后源头不能更改。但Reactive可以使用多次,因为push-based操作像是一个数据加工厂,只要填充数据就可以一直产出。3)Stream#parallel()使用fork-join并发,就是将每一个大任务一直拆分至指定大小颗粒的小任务,每个小任务可以在不同的线程中执行,这种多线程模型符合了它的多核特性。Reactive使用Event loop,用一个单线程不停的做循环,每个循环处理有限的数据直至处理完成。在上例中,大家可以看到很多Reactive的操作符,例如flatMap/concatWith/zipWith等,这样的操作符有300多个,这可能是学习这个框架最大的压力。如何理解如此繁多的操作符,可能一个归类会有所帮助:1)新序列创建,例如创建数组类序列等;2)现有序列转换,将其转换为新的序列,例如常见的map操作;3)从现有的序列取出某些元素;4)序列过滤;5)序列异常处理。6)与时间相关的操作,例如某个序列是由时间触发器定期发起事件;7)序列分割;8)序列拉至同步世界,不是所有的框架都支持异步,再需要和同步操作进行交互时就需要这种处理。上述300+操作符都有如下所示的弹珠图(Marble Diagrams),用表意的方式解释其作用。例如下图的操作符是指,随着时间推移,逐个产生了6个元素的序列,黑色竖线表示新元素产生终止。在这个操作符的作用下,下方只取了前三个元素,到第四个元素就不取了。这些弹珠图大家可以自行了解。三.Spring Webflux1.Spring Webflux框架Spring Boot 2.0相较之前的版本,在基于Spring Framework 5的构建添加了新模块Webflux,将默认的web服务器改为Netty,支持Reactive应用,并且Webflux默认运行在Netty上。而Spring Framework 5也有了一些变化。Java版本最低依赖Java 8,支持Java 9和Java 10,提供许多支持Reactive的基础设施,提供面向Netty等运行时环境的适配器,新增Webflux模块(集成的是Reactor 3.x)。下图所示为Webflux的框架:左侧是通常使用的框架,通过Servlet API的规范和Container进行交互,上一层是Spring-Webmvc,再上一层则是经常使用的一些注解。右侧为对应的Webflux层级,只要是支持NIO的Container,例如Tomcat,Jetty,Netty或Undertow都可以实现。在协议层的是HTTP/Reactive Streams。再上一层是Spring-Webflux,为了保持兼容性,它支持这些常用的注解,同时也有一套新的语法规则Router Functions。下图显示了一个调用的实例:在Client端,首先创建一个WebClient,调用其get方法,写入URL,接收格式为APPLICATION_STREAM_JSON的数据,retrieve获得数据,取得数据后用bodyToFlux将数据转换为Car类型的对象,在doOnNext中打印构造好的Car对象,block方法意思是直到回调函数被执行才可以结束。在Server端,在指定的path中进行get操作,produces和以前不同,这里是application/stream+json,然后返回Flux范型的Car对象。传统意义上,如果数据中有一万条数据,那么便直接返回一万条数据,但在这个示例返回的Flux范型中,是不包含数据的,但在数据库也支持Reactive的情况下,request可以一直往下传递,响应式的批量返回。传统方式这样的查询很有可能是一个全表遍历,这会需要较多资源和时间,甚至影响其他任务的执行。而响应式的方法除了可以避免这种情况,还可以让用户在第一时间看到数据而不是等待数据采集完毕,这在架构体验的完整性上有了很大的提升。application/stream+json也是可以让前端识别出,这些数据是分批响应式传递,而不会等待传完才显示。现在的Java web应用可以使用Servlet栈或Reactive栈。Servlet栈已经有很久的使用历史了,而现在又增加了更有优势的Reactive栈,大家可以尝试实现更好的用户体验。2.Reactive编程模型下图中是Spring实现的一个向后兼容模型,可以使用annotation来标注Container。这是一个非常清晰、支持非常细节化的模型,也非常利于同事间的交流沟通。下图是一个Functional编程模型,通过写函数的方式构造。例如下图中传入一个Request,返回Response,通过函数的方法重点关注输入输出,不需要区分状态。然后将这些函数注册至Route。这个模型和Node.js非常接近,也利于使用。3.Spring Data框架Spring Data框架支持多种数据库,如下图所示,最常用的是JPA和JDBC。在实践中,不同的语言访问不同的数据库时,访问接口是不一样的,这对编程人员来说是个很大的工作量。Spring Data便是做了另一层抽象,使你无论使用哪种数据库,都可以使用同一个接口。具体特性这里不做详谈。下图展示了一个Spring Data的使用示例。只需要写一个方法签名,然后注解为Query,这个方法不需要实现,因为框架后台已经采用一些技术,直接根据findByFirstnameAndLastname就可以查询到。这种一致的调用方式无疑提供了巨大的方便。现在Reactive对Spring Data的支持还是不完整的,只支持了MongoDB/Redis/Cassandra和Couchbase,对JPA/LDAP/Elasticsearch/Neo4j/Solr等还不兼容。但也不是不能使用,例如对JDBC数据库,将其转为同步即可使用,重点在于findAll和async两个函数,这里不再展开详述,具体代码如下图所示:Reactive不支持JDBC最根本的原因是,JDBC不是non-blocking设计。但是现在JavaOne已经在2016年9月宣布了Non-blocking JDBC API的草案,虽然还未得到Java 10的支持,但可见这已经成为一种趋势。四.总结Spring MVC框架是一个命令式逻辑,方便编写和调试。Spring WebFlux也具有众多优势,但调试却不太容易,因为它经常需要切换线程执行,出现错误的栈可能已经销毁。当然这也是现今Java的编译工具对WebFlux不太友好,相信以后会改善。下图中列出了Spring MVC和Spring WebFlux各自的特性及交叉的部分。最后也附上一些参考资料。本文作者:李博bluemind阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 28, 2018 · 1 min · jiezi

Spring、Spring Boot和TestNG测试指南 - 集成测试中用Docker创建数据库

原文地址在测试关系型数据库一篇里我们使用的是H2数据库,这是为了让你免去你去安装/配置一个数据库的工作,能够尽快的了解到集成测试的过程。在文章里也说了:在真实的开发环境中,集成测试用数据库应该和最终的生产数据库保持一致那么很容易就能想到两种解决方案:开发团队使用共用同一个数据库。这样做的问题在于:当有多个集成测试同时在跑时,会产生错误的测试结果。每个人使用自己的数据库。这样做的问题在于让开发人员维护MySQL数据库挺麻烦的。那么做到能否这样呢?测试启动前,创建一个MySQL数据库测试过程中连接到这个数据库测试结束后,删除这个MySQL数据库So, Docker comes to the rescue。我们还是会以测试关系型数据库里的FooRepositoryImpl来做集成测试(代码在这里)。下面来讲解具体步骤:安装Docker请查阅官方文档。并且掌握Docker的基本概念。配置fabric8 docker-maven-pluginfarbic8 docker-maven-plugin顾名思义就是一个能够使用docker的maven plugin。它主要功能有二:创建Docker image启动Docker container我们这里使用启动Docker container的功能。大致配置如下 <plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.28.0</version> <configuration> <images> <image> <!– 使用mysql:8 docker image –> <name>mysql:8</name> <!– 定义docker run mysql:8 时的参数 –> <run> <ports> <!– host port到container port的映射 这里随机选择一个host port,并将值存到property docker-mysql.port里 –> <port>docker-mysql.port:3306</port> </ports> <!– 启动时给的环境变量,参阅文档:https://hub.docker.com/_/mysql –> <env> <MYSQL_ROOT_PASSWORD>123456</MYSQL_ROOT_PASSWORD> <MYSQL_DATABASE>test</MYSQL_DATABASE> <MYSQL_USER>foo</MYSQL_USER> <MYSQL_PASSWORD>bar</MYSQL_PASSWORD> </env> <!– 设置判定container启动成功的的条件及timeout –> <wait> <!– 如果container打出了这行日志,则说明容器启动成功 –> <log>MySQL init process done. Ready for start up.</log> <time>120000</time> </wait> </run> </image> </images> </configuration> <executions> <execution> <!– 在集成测试开始前启动容器 –> <id>start</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <!– 在集成测试结束后停止并删除容器 –> <id>stop</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> </plugin>配置maven-failsafe-plugin<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> <execution> <id>verify</id> <goals> <goal>verify</goal> </goals> </execution> </executions> <configuration> <!– 我们被测的是一个Spring Boot项目,因此可以通过System Properties把MySQL container的相关信息传递给程序 详见文档:https://docs.spring.io/spring-boot/docs/1.5.4.RELEASE/reference/html/boot-features-external-config.html –> <systemPropertyVariables> <spring.datasource.url>jdbc:mysql://localhost:${docker-mysql.port}/test</spring.datasource.url> <spring.datasource.username>foo</spring.datasource.username> <spring.datasource.password>bar</spring.datasource.password> </systemPropertyVariables> </configuration></plugin>执行三种常见用法:mvn clean integration-test,会启动docker container、运行集成测试。这个很有用,如果集成测试失败,那么你还可以连接到MySQL数据库查看情况。mvn clean verify,会执行mvn integration-test、删除docker container。mvn clean install,会执mvn verify,并将包安装到本地maven 仓库。下面是mvn clean verify的日志:…[INFO] — docker-maven-plugin:0.28.0:start (start) @ spring-test-examples-rdbs-docker —[INFO] DOCKER> [mysql:8]: Start container f683aadfe8ba[INFO] DOCKER> Pattern ‘MySQL init process done. Ready for start up.’ matched for container f683aadfe8ba[INFO] DOCKER> [mysql:8]: Waited on log out ‘MySQL init process done. Ready for start up.’ 13717 ms[INFO][INFO] — maven-failsafe-plugin:2.22.1:integration-test (integration-test) @ spring-test-examples-rdbs-docker —[INFO][INFO] ——————————————————-[INFO] T E S T S[INFO] ——————————————————-…[INFO][INFO] Results:[INFO][INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0[INFO][INFO][INFO] — docker-maven-plugin:0.28.0:stop (stop) @ spring-test-examples-rdbs-docker —[INFO] DOCKER> [mysql:8]: Stop and removed container f683aadfe8ba after 0 ms[INFO][INFO] — maven-failsafe-plugin:2.22.1:verify (verify) @ spring-test-examples-rdbs-docker —[INFO] ————————————————————————[INFO] BUILD SUCCESS[INFO] ————————————————————————…可以看到fabric8 dmp在集成测试前后start和stop容器的相关日志,且测试成功。如何找到MySQL的端口开在哪一个呢?运行docker ps查看端口(注意下面的0.0.0.0:32798->3306/tcp):CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESa1f4b51d7c75 mysql:8 … … Up 19… 33060/tcp, 0.0.0.0:32798->3306/tcp mysql-1参考文档Fabric8 dmpSpring boot - Externalized Configuration ...

December 28, 2018 · 2 min · jiezi

Spring Cloud Stream同一通道根据消息内容分发不同的消费逻辑

应用场景有的时候,我们对于同一通道中的消息处理,会通过判断头信息或者消息内容来做一些差异化处理,比如:可能在消息头信息中带入消息版本号,然后通过if判断来执行不同的处理逻辑,其代码结构可能是这样的:@StreamListener(value = TestTopic.INPUT)public void receiveV1(String payload, @Header(“version”) String version) { if(“1.0”.equals(version)) { // Version 1.0 } if(“2.0”.equals(version)) { // Version 2.0 }}那么当消息处理逻辑复杂的时候,这段逻辑就会变得特别复杂。针对这个问题,在@StreamListener注解中提供了一个不错的属性condition,可以用来优化这样的处理结构。动手试试下面通过编写一个简单的例子来具体体会一下这个属性的用法:@EnableBinding(TestApplication.TestTopic.class)@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @RestController static class TestController { @Autowired private TestTopic testTopic; /** * 消息生产接口 * * @param message * @return / @GetMapping("/sendMessage") public String messageWithMQ(@RequestParam String message) { testTopic.output().send(MessageBuilder.withPayload(message).setHeader(“version”, “1.0”).build()); testTopic.output().send(MessageBuilder.withPayload(message).setHeader(“version”, “2.0”).build()); return “ok”; } } /* * 消息消费逻辑 */ @Slf4j @Component static class TestListener { @StreamListener(value = TestTopic.INPUT, condition = “headers[‘version’]==‘1.0’”) public void receiveV1(String payload, @Header(“version”) String version) { log.info(“Received v1 : " + payload + “, " + version); } @StreamListener(value = TestTopic.INPUT, condition = “headers[‘version’]==‘2.0’”) public void receiveV2(String payload, @Header(“version”) String version) { log.info(“Received v2 : " + payload + “, " + version); } } interface TestTopic { String OUTPUT = “example-topic-output”; String INPUT = “example-topic-input”; @Output(OUTPUT) MessageChannel output(); @Input(INPUT) SubscribableChannel input(); }}内容很简单,既包含了消息的生产,也包含了消息消费。在/sendMessage接口的定义中,发送了两条消息,一条消息的头信息中包含version=1.0,另外一条消息的头信息中包含version=2.0。在消息监听类TestListener中,对TestTopic.INPUT通道定义了两个@StreamListener,这两个监听逻辑有不同的condition,这里的表达式表示会根据消息头信息中的version值来做不同的处理逻辑分发。在启动应用之前,还要记得配置一下输入输出通道对应的物理目标(exchange或topic名),比如:spring.cloud.stream.bindings.example-topic-input.destination=test-topicspring.cloud.stream.bindings.example-topic-input.group=stream-content-routespring.cloud.stream.bindings.example-topic-output.destination=test-topic完成了上面配置之后,就可以启动应用,并尝试访问localhost:8080/sendMessage?message=hello接口来发送一个消息到MQ中了。此时可以看到类似下面的日志:2018-12-24 15:50:33.361 INFO 17683 — [content-route-1] c.d.stream.TestApplication$TestListener : Received v1 : hello, 1.02018-12-24 15:50:33.363 INFO 17683 — [content-route-1] c.d.stream.TestApplication$TestListener : Received v2 : hello, 2.0从日志中可以看到,两条带有不同头信息的消息,分别通过不同的监听处理逻辑输出了对应的日志打印。本文首发:http://blog.didispace.com/spr… ...

December 27, 2018 · 1 min · jiezi

Spring-AntPathMatcher

介绍AntPathMatcher是路径匹配规则工具类规则? 匹配一个字符(除过操作系统默认的文件分隔符)* 匹配0个或多个字符**匹配0个或多个目录{spring:[a-z]+} 将正则表达式[a-z]+匹配到的值,赋值给名为 spring 的路径变量.案例AntPathMatcher antPathMatcher = new AntPathMatcher();System.out.println(antPathMatcher.match("/get/{id}.json","/get/1.json"));

December 27, 2018 · 1 min · jiezi

从 Spring Boot到 Spring MVC(注解方式)

概述在前文《从SpringBoot到SpringMVC(非注解方式)》之中,我们远离了 Spring Boot的开箱即用与自动配置的便利性后,回归到了淳朴的 Spring MVC开发时代,但是以非注解的方式来给出的,而本文则以注解方式再度讲述一遍。注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站Spring MVC架构模式一个典型的Spring MVC请求流程如图所示,详细分为12个步骤:用户发起请求,由前端控制器DispatcherServlet处理前端控制器通过处理器映射器查找hander,可以根据XML或者注解去找处理器映射器返回执行链前端控制器请求处理器适配器来执行hander处理器适配器来执行handler处理业务完成后,会给处理器适配器返回ModeAndView对象,其中有视图名称,模型数据处理器适配器将视图名称和模型数据返回到前端控制器前端控制器通过视图解析器来对视图进行解析视图解析器返回真正的视图给前端控制器前端控制器通过返回的视图和数据进行渲染返回渲染完成的视图将最终的视图返回给用户,产生响应整个过程清晰明了,下面我们将结合实际实验来理解这整个过程。Spring MVC项目搭建实验环境如下:IntelliJ IDEA 2018.1 (Ultimate Edition)SpringMVC 4.3.9.RELEASEMaven 3.3.9这里我是用IDEA来搭建的基于Maven的SpringMVC项目,搭建过程不再赘述,各种点击并且下一步,最终创建好的项目架构如下:添加前端控制器配置使用了SpringMVC,则所有的请求都应该交由SpingMVC来管理,即要将所有符合条件的请求拦截到SpringMVC的专有Servlet上。为此我们需要在 web.xml 中添加SpringMVC的前端控制器DispatcherServlet: <!–springmvc前端控制器–> <servlet> <servlet-name>mvc-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:mvc-dispatcher.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>mvc-dispatcher</servlet-name> <url-pattern>*.action</url-pattern> </servlet-mapping>该配置说明所有符合.action的url,都交由mvc-dispatcher这个Servlet来进行处理编写Spring MVC核心XML配置文件从上一步的配置可以看到,我们定义的mvc-dispatcher Servlet依赖于配置文件 mvc-dispatcher.xml,在本步骤中我们需要在其中添加如下的配置添加注解的处理器适配器和处理器映射器方式一:<bean class=“org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping” /><bean class=“org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter” />方式二:<mvc:annotation-driven></mvc:annotation-driven>编写控制器由于使用了注解的处理器映射器和处理器适配器,所以不需要在XML中配置任何信息,也不需要实现任何接口,只需要添加相应注解即可。@Controllerpublic class TestController { private StudentService studentService = new StudentService(); @RequestMapping("/queryStudentsList") public ModelAndView handleRequest( ) throws Exception { List<Student> studentList = studentService.queryStudents(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject(“studentList”,studentList); modelAndView.setViewName("/WEB-INF/views/studentList.jsp"); return modelAndView; }}class StudentService { public List<Student> queryStudents() { List<Student> studentList = new ArrayList<Student>(); Student hansonwang = new Student(); hansonwang.setName(“hansonwang99”); hansonwang.setID(“123456”); Student codesheep = new Student(); codesheep.setName(“codesheep”); codesheep.setID(“654321”); studentList.add(hansonwang); studentList.add(codesheep); return studentList; }}为了让注解的处理器映射器和处理器适配器找到注解的Controllor,有两种配置方式:方式一:在xml中声明Controllor对应的bean<bean class=“cn.codesheep.controller.TestController” />方式二:使用扫描配置,对某一个包下的所有类进行扫描,找出所有使用@Controllor注解的Handler控制器类<context:component-scan base-package=“cn.codesheep.controller”></context:component-scan>编写视图文件这里的视图文件是一个jsp文件,路径为:/WEB-INF/views/studentList.jsp<%@ page contentType=“text/html; charset=UTF-8” pageEncoding=“UTF-8” %><%@ taglib uri=“http://java.sun.com/jsp/jstl/core" prefix=“c” %><html><head> <title>学生名单</title></head><body> <h3>学生列表</h3> <table width=“300px;” border=1> <tr> <td>姓名</td> <td>学号</td> </tr> <c:forEach items="${studentList}” var=“student” > <tr> <td>${student.name}</td> <td>${student.ID}</td> </tr> </c:forEach> </table></body></html>实验测试启动Tomcat服务器,然后浏览器输入:http://localhost:8080/queryStudentsList.action数据渲染OK。后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊我的半年技术博客之路 ...

December 27, 2018 · 1 min · jiezi

Springboot 前后端参数交互方式

springboot 前后端参数交互方式Get 方式:1. localhost:8080/index?id=1@RequestParam(value = “grade”, defaultValue = “”) String grade)2.localhost:8080/index/{reportType}localhost:8080/index-{reportType} @RequestMapping("/commonReportForm/{reportType}") @ResponseBody public ModelAndView index(@PathVariable(name = “reportType”, required = false) Integer reportType, ModelAndView mv) { ….} @RequestMapping("/commonReportForm-{reportType}") @ResponseBody public ModelAndView index(@PathVariable(name = “reportType”, required = false) Integer reportType, ModelAndView mv) { ….} Post 方式:直接以一个bean接收public Object save( User user) {….}前端传参方式:form 表单 - * input 中 name 要与 bean中的属性名相同ajax : $.ajax({ type: “POST”, url: dbrwListpath, data: { “id”: id, “name”: name, }, dataType: “json”, success: function (data) { //do something.. }, error: function (data) { //do something.. } });json 传参方式:public Object save(@RequestBody User user) {….}前端ajax: $.ajax({ type: “POST”, headers: { ‘Accept’: ‘application/json’, ‘Content-Type’: ‘application/json’ }, url: dbrwListpath, data: { “id”: id, “name”: name, }, dataType: “json”, success: function (data) { //do something.. }, error: function (data) { //do something.. } }); Dto 封装方式:public Object save(@RequestBody DataDto dto) {….}public class DataDto{ //学校id private Integer schoolId; //幼儿信息 private List<User> users; //get set …}前端ajax数据格式://示例 var user = []; for (var i = 0; i < clen; i++) { var c = {}; c.id = 1; c.name = tom; user.push(c); }//datas属性名需与Dto实体的属性名相同var datas = { schoolId:schoolId, user:user }; $.ajax({ type: “POST”, headers: { ‘Accept’: ‘application/json’, ‘Content-Type’: ‘application/json’ }, url: “/comprehensiveDeclaration/comprehensive-” + comprehensiveId + “/step1-save”, data: JSON.stringify(datas), dataType: “json”, success: function (response) { //do something.. }, error: function (data) { //do something.. } }); ...

December 26, 2018 · 2 min · jiezi

一对一直播系统搭建方法展示:Java 实现阿里云直播

准备步骤创建 阿里云账号根据 流程 完成实名认证,以确保可以使用阿里云相应服务在密钥管理页面获取阿里云访问密钥,AccessKeyId 和 AccessKeySecret开通阿里云直播服务关键点阿里云直播服务端提供了 一系列 API ,但如果只是单纯的直播[推流和拉流] ,实际不需要使用这些 API推流准备推流即直播人员进行视频播放的操作,这需要使用推流客户端 第三方推流工具 OBS在推流工具中需要指定推流地址、流名称、鉴权密钥如果上述信息阿里云验证合法,既可以开始直播,在阿里云后端可以看到正在直播的流信息拉流准备拉流即直播观众通过视频播放器在线获取直播信息,播放器使用 阿里云播放器 即可,该播放器目前只是阿里云的点播和直播服务获取拉流地址后传入播放器,即可开始观看直播Java 开发注意点在阿里云直播的文档中有提供 Java SDK目前 SDK 中推荐引入的版本号是 2.3.0 ,但其实所有 API 参照的都是最新版 SDK ,最新的版本号可在 阿里云SDK频道 找到但如果只是单纯的直播[推流和拉流] ,则不需要进行以上操作推流的关键点在于 直播鉴权此处介绍的直播鉴权只是说的 auth_key 的拼接和验证规则完整的推流和拉流地址并不知这些,需要依旧案例参考获取推流地址此处获取的只是推流地址的房间号及其他请求参数完整的推流地址需要加上阿里云直播中心地址和用户的产品名称直播中心地址 http://video-center.alivecdn.com产品名称[支持自定义] /appName/vhost 用于接收拉流地址,即申请阿里云直播时准备的直播域名此处使用 Java MD5加密 实现字串加密,加密后长度需要是 32 位加密串中的 Constants.ALI_LIVE_PRIVATE_KEY 可在阿里云后端的直播鉴权处获取1// 获取推流地址public String getPushUri(String roomName, Long endTime) {return getRoomName(roomName) + “vhost=” + Constants.ALI_LIVE_PULL_URL + “&” + generateAuthKey(roomName, endTime);}// 房间号private String getRoomName(String roomName) {return roomName + “?”;}// 完整验签串private String generateAuthKey(String roomName, Long endTime) {return “auth_key=” + endTime + generateUuid() + generateEncryptStr(roomName, endTime);}// 唯一标识private String generateUuid() {String uuid = “0”;String uid = “0”;return “-” + uuid + “-” + uid + “-”;}// 验签密钥private String generateEncryptStr(String roomName, Long endTime) {String uri = Constants.ALI_LIVE_APP_NAME + roomName;return md5(uri + “-” + endTime + generateUuid() + Constants.ALI_LIVE_PRIVATE_KEY);}获取拉流地址此处获取的拉流地址是完整的,因为拉流地址是直接获取后传入前端的阿里云播放器中注意房间名后面加的后缀 .m3u8 用于表示接受的直播视频类型,阿里云官方还提供其他几种类型,可在文档中查看拉流地址和推流地址最大的区别在于请求地址的不同,拉流是请求自己提供给阿里云的直播域名,而拉流是请求阿里云的直播中心而且推流时需要指定 vhost 告知阿里云直播域名,但拉流时不需要获取到拉流地址后可直接参照 Java + jQuery 实现阿里云播放器接口 实现播放器的对接在播放器的的配置中指明 isLive: true 表名是直播操作上述笔记中实现的是点播接口,利用的通过 vid 获取 playAuth 的方式,这不适用于直播直播需要直接指定 source: url 即可public String getPullUrl(String roomName, Long endTime) {roomName += “.m3u8”;return “http://” + Constants.ALI_LIVE_PULL_URL + generateUri(roomName) + generateAuthKey(roomName, endTime);}// 获取请求参数private String generateUri(String roomName) {return Constants.ALI_LIVE_APP_NAME + getRoomName(roomName);}// 房间号private String getRoomName(String roomName) {return roomName + “?”;}// 完整验签串private String generateAuthKey(String roomName, Long endTime) {return “auth_key=” + endTime + generateUuid() + generateEncryptStr(roomName, endTime);}// 唯一标识private String generateUuid() {String uuid = “0”;String uid = “0”;return “-” + uuid + “-” + uid + “-”;}// 验签密钥private String generateEncryptStr(String roomName, Long endTime) {String uri = Constants.ALI_LIVE_APP_NAME + roomName;return md5(uri + “-” + endTime + generateUuid() + Constants.ALI_LIVE_PRIVATE_KEY); ...

December 25, 2018 · 2 min · jiezi

Spring详解1.概述

博客地址:https://spiderlucas.github.io备用地址:http://spiderlucas.coding.me1 Spring是什么Spring是由Rod Johnson缔造的一个分层的Java SE/EE应用一站式的轻量级开源框架,以IoC(Inverse of Control,反转控制)和AOP(Aspect Oriented Programming,面向切面编程)为内核,提供了展现层Spring MVC、持久层Spring JDBC及业务层事务管理等一站式的企业级应用技术。2 Spring的优点方便解耦——Spring提供的IoC容器实现了对象依赖关系的管理,避免了硬编码导致的耦合。支持AOP——Spring提供的AOP功能,方便进行面向切面编程。声明式事物——Spring提供了通过声明的方式灵活的进行事务管理。方便程序测试——可以用非容器以来的编程方式进行几乎所有的测试工作。集成了多种优秀框架——Spring提供了对各种优秀框架(如Struts、Hibernate、Hessian、Quartz等)的直接支持。降低Java EE API的使用难度——Spring对很多难用的Java EE API(如JDBC、JavaMail、远程调用等)提供了一个薄薄的封装层,使得这些Java EE API的使用难度大为降低。Java源码是经典学习范例——Spring的源码设计精妙、结构清晰,是Java技术的最佳实践的范例。3 Spring体系结构Spring框架按照所属功能可以划分为5个主要模块,如下所示:IOCSpring的核心模块实现了IoC的功能,它将类和类之间的依赖从代码中脱离出来,用配置的方式进行依赖关系描述,由IoC容器负责类的创建,管理,获取等工作。BeanFactory接口是Spring框架的核心接口,实现了容器很多核心的功能。Context模块构建于核心模块之上,扩展了BeanFactory的功能,包括国际化、Bean生命周期控制、框架事件体系、资源加载透明化等功能;还提供了众多企业级服务的支持,如邮件服务、任务调度、JNDI、EJB、远程访问等。ApplicationContext是Context模块的核心接口。表达式语言(Expression Language)是统一表达式语言的一个扩展,用于查询和管理运行期的对象,支持设置和获取对象属性,调用对象方法,操作数组、集合等。使用它可以很方便的通过表达式和Spring IoC容器进行交互。AOPSpring提供了满足AOP Alliance规范的实现,还整合了AspectJ这种语言级的框架。Java 5.0引入了java.lang.instrument,允许在JVM启动时启用一个代理类,通过该代理类在运行期修改类的字节码,改变一个类的功能,从而实现AOP的功能。数据访问和集成Spring站在DAO的抽象层面,建立了一套面向DAO层的统一的异常体系,同时将各种访问数据的检查异常转换成非检查型异常,为整合各种持久层框架提供基础。Spring通过模版化技术对各种高数据访问技术进行了薄层封装,将模式化的代码隐藏起来,使数据访问的程序得到大幅简化。借助AOP技术,Spring提供了声明式事务的功能。Web及远程操作该模块建立在Application Context模块之上,提供了Web应用的各种工具类和多项面向Web的功能。Spring提供了一个完整的MVC框架——Spring MVC,还整合Structs、WebWork等MVC框架。WebSocket提供了一个在Web应用中高效、双向的通信,实现了客户端和服务器之间的高频和低延时消息交换。测试框架Spring可以用非容器依赖的编程方式进行几乎所有的测试工作,支持JUnit和TestNG等测试框架。4 Spring4.0的新特性完全支持Java 8核心容器的增强:支持范型依赖注入;对CgLib类代理不要求必须有空参构造器;提供了@Description、@Conditional、@Lazy、@Order等新的注解支持用Groovy定义BeanWeb的增强:开始基于Servlet 3.0;为了方便REST开发,引入了@RestController控制器注解;添加了一个AsyncRestTemplate支持Rest客户端的异步无阻塞请求。支持WebSocket测试的增强:Spring-test模块里的所有注解都可以用作meta-annotation,这样就可以自定义组合注解来减少测试时的重复配置;提供了@Sql注解支持多数据源的测试。提供了对JCache注解的支持,并对Cache抽象部分进行了增强。添加了动态语言支持,对动态脚本语言计算表达式进行了抽象封装。添加了多线程并发处理支持增强了持久化处理5 Spring的子项目Spring IO Platform : Spring IO是可集成的、构建现代化应用的版本平台。Spring IO是模块化的、企业级的分布式系统,包括一系列依赖,是的开发者仅能对自己所需的部分进行完全的部署控制。Spring Boot:Spring应用快速开发工具,用来简化Spring应用开发过程。Spring XD:Spring XD(eXtreme Date,极限数据)是Pivotal的大数据产品。它结合了Spring Boot和Grails,组成Spring IO平台的执行部分。Spring Data:Spring Data是为了简化构建基于Spring框架应用的数据访问实现,包括非关系数据库、Map-Reduce框架、云数据服务等;另外,也包含对关系数据库的访问支持。Spring Integration:Spring Integration为企业数据集成提供了各种适配器,可以通过这些适配器来转换各种消息格式,并帮助Spring应用完成与企业应用系统的集成。Spring Batch:Spring Batch是一个轻量级的完整批处理框架,皆在帮助应用开发者构建一个健壮、高效的企业级批处理应用(这些应用的特点是不需要与用户交互,重复的操作量大,对于大容量的批量数据处理而言,这些操作往往要求较高的可靠性)Spring Security:Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文配置的bean,充分利用Ioc和AOP功能,为应用系统提供声明式的安全访问控制功能。Spring Hateoas:Spring Hateoas是一个用户支持实现超文本驱动的REST Web服务的开发库,是Hateoas的实现。Hateoas(Hypermedia as the engine of application state)是REST架构风格中最复杂的约束,也是构建成熟REST服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约,是的客户端可以更加智能和自适应。Spring Social:Spring Social是Spring框架的扩展,用来方便开发Web社交应用程序,可通过该项目来创建与各种社交网站的交互,如Facebook,LinkedIn、Twitter等。Spring AMQP:Spring AMQP是基于Spring框架的AMQP消息解决方案,提供模版化的发送和接收消息的抽象层,提供基于消息驱动的POJO。这个项目支持Java和.NET连个版本。Spring Source旗下的Rabbit MQ就是一个开源的基于AMQP的消息服务器。Spring for Android:Spring for Android为Android终端开发应用提供Spring的支持,它提供了一个在Android应用环境中工作、基于Java的REST客户端。Spring Mobile:Spring Mobile是基于Spring MVC构建的,为移动端的服务器应用开发提供支持。Spring Web Flow:Spring Web Flow(SWF)一个建立在Spring MVC基础上的Web页面流引擎。Spring Web Service:Spring Web Service是基于Spring框架的Web服务框架,主要侧重于基于文档驱动的Web服务,提供SOAP服务开发,允许通过多种方式创建Web服务。Spring LDAP:Spring LDAP是一个用户操作LDAP的Java框架,类似Spring JDBC提供了JdbcTemplate方式来操作数据库。这个框架提供了一个LdapTemplate操作模版,可帮助开发人员简化looking up、closing contexts、encoding/decoding、filters等操作。Spring Session: Spring Session致力于提供一个公共基础设施会话,支持从任意环境中访问一个会话,在Web环境下支持独立于容器的集群会话,支持可插拔策略来确定Session ID,WebSocket活跃的时候可以简单地保持HttpSession。Spring Shell: Spring Shell提供交互式的Shell,用户可以简单的基于Spring的编程模型来开发命令。6 Spring相关资料Spring 官方文档Spring 参考文档Spring 项目地址Spring Boot 项目地址Spring Boot 参考文档Spring Cloud 参考文档Spring maven仓库 ...

December 25, 2018 · 1 min · jiezi

微服务配置中心实战:Spring + MyBatis + Druid + Nacos

在结合场景谈服务发现和配置中我们讲述了 Nacos 配置中心的三个典型的应用场景,包括如何在 Spring Boot 中使用 Nacos 配置中心将数据库连接信息管控起来,而在“原生”的 Spring 中可以怎么使用 Nacos 配置中心呢?很多基于 Spring MVC 框架的 Web 开发中,Spring + MyBatis + Druid 是一个黄金组合,在此基础上融入 Nacos 配置中心,将会发生什么特别的变化呢?本文将通过一个用户信息查询示例,演示在 Spring Web 项目中如何将数据库连接池的配置存放到 Nacos 中,统一运维管控,达到配置治理与降低数据泄露风险的目的。数据表在测试数据库中新建 user 表,其中包含用户名称等字段,与接下来的 User model 类相对应。CREATE TABLE user ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, name varchar(10) NOT NULL DEFAULT ’’ COMMENT ‘名字’, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’, update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘用户表’;添加一行测试数据:INSERT INTO user (name, create_time, update_time) VALUES (‘Nacos’, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);Spring该示例是 Spring 常规的 Web 项目,项目结构如下:pom.xml引入 Nacos Spring 的依赖包 nacos-spring-context:<dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-spring-context</artifactId> <version>${latest.version}</version></dependency>笔者在撰写本文时,nacos-spring-context 的最新版本为:0.2.2-RC1dispatcher-servlet.xmldispatcher-servlet.xml 为示例中 Spring MVC 的入口配置,在其中通过 import 引入了 Nacos、Druid、MyBatis 的配置,其内容如下:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:context=“http://www.springframework.org/schema/context" xmlns:mvc=“http://www.springframework.org/schema/mvc" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <mvc:annotation-driven/> <context:annotation-config/> <context:component-scan base-package=“com.alibaba.nacos.example.spring”/> <import resource=“nacos.xml”/> <import resource=“datasource.xml”/> <import resource=“spring-config-mybatis.xml”/></beans>nacos.xml关键看 nacos.xml ,nacos-spring-context 的扩展了 Spring 的 XML Schema 机制,自定义了 <nacos:property-source/> 等元素,详细配置内容如下:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xmlns:nacos=“http://nacos.io/schema/nacos" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://nacos.io/schema/nacos http://nacos.io/schema/nacos.xsd"> <nacos:global-properties server-addr=“127.0.0.1:8848” /> <nacos:property-source data-id=“datasource.properties”/></beans>其中通过 <nacos:global-properties /> 设置 Nacos Server 的连接地址,通过 <nacos:property-source /> 从 Nacos 配置中心加载了 dataId 为 datasource.properties 的配置,nacos-spring-context 会解析获取到的配置内容并添加到 Spring Environment 的 PropertySources 中,使得后续初始化 Druid 连接池的时候能获取到数据库连接地址、账号密码、初始连接池大小等信息。这就是 Nacos 配置中心与 Spring 结合的关键点。datasource.xml这是数据库连接池的配置,初始化了 DruidDataSource 的 Spring Bean,并将其通过 DataSourceTransactionManager 设置为 Spring 的数据库连接池。<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns=“http://www.springframework.org/schema/beans" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:tx=“http://www.springframework.org/schema/tx" xsi:schemaLocation=“http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id=“dataSource” class=“com.alibaba.druid.pool.DruidDataSource” init-method=“init” destroy-method=“close”> <property name=“url” value="${datasource.url}”/> <property name=“username” value="${datasource.username}”/> <property name=“password” value="${datasource.password}”/> <property name=“initialSize” value="${datasource.initial-size}”/> <property name=“maxActive” value="${datasource.max-active}”/> </bean> <bean id=“txManager” class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”> <property name=“dataSource” ref=“dataSource”/> </bean> <tx:annotation-driven transaction-manager=“txManager”/></beans>从上面的配置内容看一看到,数据库连接池不需要因为引入 Nacos 配置中做任何特殊的改变。其他User 的 Model、Service 等也跟不使用 Nacos 配置中心时完全一致,这里就不一一贴出,完整示例代码可以在 nacos-examples 获取:https://github.com/nacos-group/nacos-examples/tree/master/nacos-spring-example/nacos-spring-config-datasource-exampleNacos参照 Nacos 官网的快速开始:https://nacos.io/zh-cn/docs/quick-start.html从 Nacos 的 github 上下载最新稳定版本的 nacos-server:https://github.com/alibaba/nacos/releases解压后到 Nacos 的 bin 目录下,执行 startup 启动脚本以单机模式启动 Nacos Server, Windows 请使用 cmd startup.cmd ,Linux/Unix/Mac 环境请使用 sh startup.sh -m standalone 。启动后,浏览器访问:http://localhost:8848/nacos/index.html 就可以来到 Nacos 的控制台,新增 dataId 为 datasource.properties 的配置,对应上面 Spring 的 nacos.xml 中的 dataId。配置内容则与 Spring 的 datasource.xml 中的连接池属性一一对应,示例如下:datasource.url=jdbc:mysql://localhost:3306/testdatasource.username=rootdatasource.password=rootdatasource.initial-size=1datasource.max-active=20运行示例中是 UserController#get() 通过 UserServce 调用 Mybatis 的 Mapper 类(UserMapper)从数据库中查询指定 ID 的用户信息,假设该示例是运行在端口为 8080 的 Tomcat 上,访问:http://localhost:8080/users?id=1 地址将返回:{ “id”: 1, “name”: “Nacos”}总结本文通过一个示例演示在“原生” Spring 中如何使用 Nacos 配置中心,从示例可以看出,对原有的 Spring 项目基本没有任何侵入,只需在 pom.xml 中添加 nacos-spring-context 的依赖,然后再定义并引入 nacos.xml 配置,就可以将数据库连接池信息管控起来,做到统一运维,并降低数据泄露的风险。试想,如果你有多个项目连接同一个数据库或一个项目部署很多实例,当数据库密码修改时,你不需要去修改每个项目的 datasource.properties 文件,再走一次应用的部署发布流程,而是到 Nacos 的控制台上修改一个配置项,再去重启应用实例即可。当然,如果你是自己管理数据库连接池,则可以做到连“重启应用实例”都不需要了,只需在监听到 Nacos 配置变化时重新初始化数据库连接池即可。将 Spring 配置放置到 Nacos 配置中,还能用上“动态推送”、“版本管理”、“快速回滚”、“监听查询”,以及后续的 “灰度发布”、“配置加密”、“权限管控”等功能,为 Spring + MyBatis + Druid 插上“飞翔”的翅膀。作为 Nacos 配置中心的阿里云免费产品 ACM:https://www.aliyun.com/product/acm,已经提供了上面所有的功能,如果您不想自己部署运维 Nacos Server 或者想使用“推送轨迹”、“细粒度权限控制”、“ECS 实例 RAM 角色”等高级特性,不妨尝试下免费的 ACM。完整示例代码:Nacos: https://github.com/nacos-group/nacos-examples/tree/master/nacos-spring-example/nacos-spring-config-datasource-exampleACM: https://github.com/nacos-group/nacos-examples/tree/acm/nacos-spring-example/nacos-spring-config-datasource-example本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 24, 2018 · 2 min · jiezi

一起来读Spring源码吧(三)ApplicationContext初始化过程

上篇讲到BeanFactory的初始化过程,Spring还提供了另外一种拥有更多扩展功能的容器ApplicationContext。ApplicationContext比简单BeanFactory多出的功能:1、支持不同的信息源。通过继承MessageSource接口2、访问资源。通过继承ResourceLoader,可以从不同地方得到Bean定义资源3、应用事件。通过继承ApplicationEventPublisher,引入事件机制,方便管理Bean的生命周期。。我们以ClassPathXmlApplicationContext为例分析这类容器的初始化过程。第一步是设置配置文件路径,保存在configLocations属性中。重点是后面的refresh(),其中逻辑清晰地展示了所有的流程:1、准备工作对系统属性和环境属性进行准备和验证2、初始化BeanFactory,解析容器中的bean3、对BeanFactory进行各种功能填充3.1、增加对SPEl语言的支持;3.2、增加对属性编辑器的支持,如字符串转Date;3.3、添加ApplicationContextAwareProcessor为了在实现了EnvironmentAware等接口的bean实例化时取得相应的资源3.4、设置忽略依赖将ApplicationContextAwareProcessor涉及到的Aware类在依赖注入的时候忽略3.5、注册固定依赖为bean中BeanFactory等属性的注入注册固定的实例3.6、增加对AspectJ的支持(后续会细说)3.7、将相关环境变量及属性以单例模式注册4、允许子类添加自己的BeanPostProcessors5、激活BeanFactoryPostProcessor在容器所有bead实例化前对bean配置的元数据进行处理,如PropertyPlaceholderConfigurer实现bean定义中${key}和配置文件的映射6、注册BeanPostProcessor7、初始化消息资源,国际化处理8、初始化应用事件广播器用于在context.publishEvent时根据事件类型遍历对应的监听器9、允许子类初始化其他的bean10、查找并注册事件监听器11、初始化非延迟加载的单例12、通知生命周期处理器容器启动完毕,发出ContextRefreshedEvent事件

December 22, 2018 · 1 min · jiezi

一起来读Spring源码吧(二)容器getBean过程详解

上一篇分析了XmlBeanFactory的初始化过程,此时配置的bean已注册到容器中,但也仅仅只是保存了bean的信息,并没有产生bean实例。下面我们以BeanFactory.getBean(String name)为出发点探索下bean的加载过程。AbstractBeanFactory实现了getBean方法,调用doGetBean真正开始获取bean。首先是将参数name转化为beanName,如果是“&”开头的代表是FactoryBean(是特殊的bean,后面会讲到)需要把“&”去除;如果是别名就去寻找它最终指向的beanName。然后从容器的单例注册中心尝试获取bean实例。顺序如下:从单例缓存中(容器的singletonObjects属性)尝试获取;如果没有就从提早曝光单例对象(容器的earlySingletonObjects属性)尝试获取;如果还没有,就从singletonFactories中尝试获取这个bean的ObjectFactory,再调用ObjectFactory.getObject()获取到bean,放入earlySingletonObjects,把ObjectFactory从singletonFactories中移除(是不是很混乱?别急,这是Spring为了避免循环依赖做出的策略,后面我们会解释)。如果没有获取到,也就是说要新建一个,先检查bean是否已经在原型创建中(避免原型的循环依赖),再在容器中查找是否存在beanName对应的BeanDefinition,找不到的话就递归从父容器查找;然后如果BeanDefinition有依赖(也就是bean定义时有depends-n属性)就先把依赖关系注册到容器的dependentBeanMap,再递归加载依赖的bean;然后根据BeanDefinition设置的scope选择创建单例、原型还是其他scope的bean实例。单例的话调用getSingleton创建单例;原型的需要先把beanName注册到容器的prototypesCurrentlyInCreation中(为了防止循环依赖)然后调用createBean创建bean实例,随后从prototypesCurrentlyInCreation找中移除beanName。创建出的实例跟前面在缓存中获取到的一样并不会直接返回,而是调用getObjectForBeanInstance,如果不是FactoryBean或者想要的就是FactoryBean本身(name是&开头)就直接返回;否则先从工厂创建bean缓存factoryBeanObjectCache中尝试获取,没有就调用getObjectFromFactoryBean方法创建一个bean实例(是单例还会存入factoryBeanObjectCache)返回。一大堆流程看下来是不是有点晕,让我们再来简述下步骤:1、name转化为beanName2、尝试从缓冲中获取单例2.1、单例缓存singletonObjects中查找(这个getSingleton只查找不创建)2.2、提早曝光单例earlySingletonObjects中查找2.3、singletonFactories中查找ObjectFactory并在生产bean放入earlySingletonObjects后移除3、缓冲中获取不到,需要新建3.1、当前容器中查找BeanDefinition3.2、没有的话去父容器中查找BeanDefinition3.3、递归加载设置过的依赖bean3.4、根据scope创建bean实例4、如果是工厂bean,返回生产的bean4.1、尝试从缓存factoryBeanObjectCache中获取4.2、调用BeanFactory.getObject()生产一个bean实例4.3、如果不是应用程序本身的bean,调用后置处理器(这个后面会重点说)4.4、是单例的话存入factoryBeanObjectCache现在我们大致了解了向容器获取一个bean的主要步骤,但是我们还是不太清楚具体是怎么创建bean实例的,所以我们还需要继续深入:前面说到如果是单例的话,调用getSingleton(String beanName, ObjectFactory<?> singletonFactory)(区别于2.1的getSingleton),先从singletonObjects查找,没有的话将beanName放入singletonsCurrentlyInCreation表示该bean正在加载;然后用参数singletonFactory的函数方法createBean(对,就是和原型一样的那个)创建一个bean;将beanName从singletonsCurrentlyInCreation中移除;最后将新建立的beanName和bean的映射关系保存到缓存singletonObjects和已注册registeredSingletons中,移除singletonFactories和earlySingletonObjects中的记录。步骤简述如下:3.4.1、从singletonObjects查找3.4.2、设置正在加载状态3.4.3、createBean创建bean3.4.4、移除正在加载状态3.4.5、保存缓存,删除中间辅助状态我们再来看createBean。先从BeanDefinition中获取bean的Class,再对override属性(通过lookup-method和replace-method配置,后面实例化时会用到)进行标记和验证,然后给后置处理器一个机会直接返回bean代理,即如果容器中有注册InstantiationAwareBeanPostProcessor这类后置处理器的话,就分别执行它的postProcessBeforeInstantiation和所有后置处理器的postProcessAfterInstantiation,如果返回不为空就直接返回代理bean(AOP功能基于这里判断的);最后调用doCreateBean实例化bean。步骤简述如下:3.4.3.1、从BeanDefinition中获取bean的Class对象3.4.3.2、对override属性进行标记和验证3.4.3.3、初始化前的短路判断3.4.3.4、实例化bean继续深入doCreateBean,第一步判断如果是单例的话从factoryBeanInstanceCache中获取缓存的BeanWrapper(bean的包装类)并从中移除,如果没有则调用createBeanInstance创建一个BeanWrapper;然后应用MergedBeanDefinitionPostProcessor(AutowiredAnnotationBeanPostProcessor,Autowired);允许循环依赖的并且自身在创建中的单例,把此单例的ObjectFactory(SmartInstantiationAwareBeanPostProcessor,AOP)注册到singletonFactories,同时从earlySingletonObjects移除对应;然后调用populateBean对bean的属性进行填充,递归初始依赖的bean;再调用initializeBean执行初始化方法;对于允许循环依赖的并且自身在创建中的单例做循环依赖检查;最后注册DisposableBean。步骤简述如下:3.4.3.4.1、尝试从factoryBeanInstanceCache中获取缓存BeanWrapper3.4.3.4.2、创建一个bean实例返回BeanWrapper3.4.3.4.3、应用MergedBeanDefinitionPostProcessor3.4.3.4.4、依赖处理3.4.3.4.5、属性填充3.4.3.4.6、执行初始化方法3.4.3.4.7、循环依赖检查3.4.3.4.8、注册DisposableBean首先我们看如何创建一个bean实例。先是解析出Class,如果工厂方法不用空,即设置了factory-method,则使用工厂方法初始化策略;否则需要根据参数锁定构造函数,如果已经解析过了在缓存中可以找到锁定,否则进行解析锁定并加入缓存;没有锁定到有参数的构造函数的话,就使用默认构造器,如果有需要覆盖或动态替换的方法(override属性)需要使用CGLIB进行动态代理,否则就用反射的方式创建实例。再来看一下属性填充。在属性设置前应用InstantiationAwareBeanPostProcessors的postProcessAfterInstantiation(可以控制是否继续填充),然后根据设置的自动注入方式(名称或者类型)获取属性bean(递归getBean)存入PropertyValues中,再应用InstantiationAwareBeanPostProcessors的postProcessProperties对填充前的属性进行处理(如对属性的验证),最后将所有PropertyValues中的属性填充到BeanWrapper中。属性填充完之后就是initializeBean方法了。首先对实现XXAware接口的bean注入相应的资源(如实现了BeanFactoryAware的bean会注入当前BeanFactory的实例),然后就是注册了的BeanPostProcessor的postProcessBeforeInitialization,再是激活自定义的init方法,先后执行实现了InitializingBean的afterPropertiesSet方法和配置init-method方法。最后看下DisposableBean的注册。注册DisposableBean的实现,在注销时执行来源于DestructionAwareBeanPostProcessors、实现的DisposableBean的destroy方法还有自己配置的destroy-method的处理。

December 22, 2018 · 1 min · jiezi

Spring-loaded实现热部署-开发环境

Oracle提供的JDK其实已经自带一定程度的热加载功能,但是如果你修改了类名,方法名,或者添加了新类,新方法的话。Tomcat都需要重新启动来使得刚才的更改生效。而JRebel和spring-loaded都能有效地解决这个问题。其中springloaded是开源软件,可以免费使用。其主页:https://github.com/spring-pro…获取jar包首先我们需要得到spring-loaded的jar包,上面的github链接中可以下载到。这里我用的是最新的springloaded-1.2.7.RELEASE.jar存放位置:D:springloaded-1.2.7.RELEASE.jarIDE中部署打开项目,在启动之前按以下进行配置idea中在启动Tomcat之前配置VM option。填写以下参数: -javaagent:D:/springloaded-1.2.7.RELEASE.jar -noverify其中参数中Springloaded的路径按实际填写配置完成后可以启动项目了eclipse中右击项目->Run as->Run configurations… 在tomcat启动项添加VM参数 -javaagent:D:/springloaded-1.2.7.RELEASE.jar -noverify其中参数中Springloaded的路径按实际填写配置完成后可以启动项目了测试为了解Springloaded 适用于哪些更改,下面来作几个测试我在上述启动的SSM项目中,在一个控制器里添加了以下方法/** * 测试SpringLoaded /@RequestMapping("/hello")@ResponseBody public String test(){ return “Hello Spring Loaded!”;}保存后通过浏览器访问失败,找不到/hello 这个路径。通过重启Tomcat后可以正常访问test方法继续添加test2()方法,不使用注解,为了在浏览器中方便测试,通过test()方法来访问test2()方法。(经过上面重启Tomcat后test方法可以访问)/* * 测试SpringLoaded */@RequestMapping("/hello")@ResponseBodypublic String test(){ return this.test2();}public String test2(){ return “Spring Loaded By Test2”;}浏览器输出 “Spring Loaded By Test2"说明我们添加的第二个方法test2()没有经过重启服务器就可以访问了,热部署生效我们继续 新建一个类,并在test()方法中去调用/**新建类 * Created by JiangWei.Huang * 2017/8/22 0022. */@RestControllerpublic class TestCtrl { @GetMapping("/hello3”) public String test3(){ return “TestCtrl-test3”; }}/**修改test方法调用新建的类TestCtr中的test3方法 * 测试SpringLoaded */@RequestMapping("/hello")@ResponseBodypublic String test(){ TestCtrl testCtrl = new TestCtrl(); return testCtrl.test3();}浏览器输出 “TestCtrl-test3"说明我们新建的类,在不用重启的情况下也能够被调用到了,热部署生效但值得注意的是,我们新建的类中,在类上与方法上都写了Spring注解,但这里是也不生效的。@RestController这个注解没有生效,/hello3这个路径也是访问不了的。需要重启服务器才生效另外在Idea中修改后自动保存但不会自动重新编译,如果在Idea中修改后热部署没有生效,按ctrl+shift+f9重新编译。也可以设置Idea自动编译,设置如下图。总结经过上面的测试我们可以得出一些结论。像官方所说,可以实现以下的热更新Spring Loaded allows you to add/modify/delete methods/fields/constructors. The annotations on types/methods/fields/constructors can also be modified and it is possible to add/remove/change values in enum types.Spring加载允许您添加/修改/删除/字段/方法构造函数。注释类型/方法/字段/构造函数,并且还可以在枚举类型中添加/删除/更改值。但是对于第三方像是Spring注解这些的修改,spring-loaded就无能为力了,必须求助于更加强大的,收费的JRebel了 ...

December 19, 2018 · 1 min · jiezi

Flutter路由管理代码这么长长长长长,阿里工程师怎么高效解决?(实用)

背景:在flutter的业务开发过程中,flutter侧会逐渐丰富自己的路由管理。一个轻量的路由管理本质上是页面标识(或页面路径)与页面实例的映射。本文基于dart注解提供了一个轻量路由管理方案。 不论是在native与flutter的混合工程,还是纯flutter开发的工程,当我们实现一个轻量路由的时候一般会有以下几种方法:较差的实现,if-else的逻辑堆叠: 做映射时较差的实现是通过if-else的逻辑判断把url映射到对应的widget实例上,class Router { Widget route(String url, Map params) { if(url == ‘myapp://apage’) { return PageA(url); } else if(url == ‘myapp://bpage’) { return PageB(url, params); } }}这样做的弊端比较明显: 1)每个映射的维护影响全局映射配置的稳定性,每次维护映射管理时需要脑补所有的逻辑分支. 2)无法做到页面的统一抽象,页面的构造器和构造逻辑被开发者自定义. 3)映射配置无法与页面联动,把页面级的配置进行中心化的维护,导致维护责任人缺失.一般的实现,手动维护的映射表: 稍微好一点的是将映射关系通过一个配置信息和一个工厂方法来表现class Router { Map<String, dynamic> mypages = <String, dynamic> { ‘myapp://apage’: ‘pagea’, ‘myapp://bpage’: ‘pageb’ } Widget route(String url, Map params) { String pageId = mypages[url]; return getPageFromPageId(pageId); } Widget getPageFromPageId(String pageId) { switch(pageId) { case ‘pagea’: return PageA(); case ‘pageb’: return PageB(); } return null; }在flutter侧这种做法仍然比较麻烦,首先是问题3仍然存在,其次是由于flutter目前不支持反射,必须有一个类似工厂方法的方式来创建页面实例。 为了解决以上的问题,我们需要一套能在页面级使用、自动维护映射的方案,注解就是一个值得尝试的方向。我们的路由注解方案annotation_route(github地址:https://github.com/alibaba-flutter/annotation_route)) 应运而生,整个注解方案的运行系统如图所示: 让我们从dart注解开始,了解这套系统的运作。dart注解注解,实际上是代码级的一段配置,它可以作用于编译时或是运行时,由于目前flutter不支持运行时的反射功能,我们需要在编译期就能获取到注解的相关信息,通过这些信息来生成一个自动维护的映射表。那我们要做的,就是在编译时通过分析dart文件的语法结构,找到文件内的注解块和注解的相关内容,对注解内容进行收集,最后生成我们想要的映射表,这套方案的构想如图示: 在调研中发现,dart的部分内置库加速了这套方案的落地。source_gendart提供了build、analyser、source_gen这三个库,其中source_gen利用build库和analyser库,给到了一层比较好的注解拦截的封装。从注解功能的角度来看,这三个库分别给到了如下的功能:build库:整套资源文件的处理analyser库:对dart文件生成完备的语法结构source_gen库:提供注解元素的拦截 这里简要介绍下source_gen和它的上下游,先看看我们捋出来的它注解相关的类图:source_gen的源头是build库提供的Builder基类,该类的作用是让使用者自定义正在处理的资源文件,它负责提供资源文件信息,同时提供生成新资源文件的方法。source_gen从build库提供的Builder类中派生出了一个自己的builder,同时自定义了一套生成器Generator的抽象,派生出来的builder接受Generator类的集合,然后收集Generator的产出,最后生成一份文件,不同的派生builder对generator的处理各异。这样source_gen就把一个文件的构造过程交给了自己定义的多个Generator,同时提供了相对build库而言比较友好的封装。 在抽象的生成器Generator基础上,source_gen提供了注解相关的生成器GeneratorForAnnotation,一个注解生成器实例会接受一个指定的注解类型,由于analyser提供了语法节点的抽象元素Element和其metadata字段,即注解的语法抽象元素ElementAnnotation,注解生成器即可通过检查每个元素的metadata类型是否匹配声明的注解类型,从而筛选出被注解的元素及元素所在上下文的信息,然后将这些信息包装给使用者,我们就可以利用这些信息来完成路由注解。annotation_route在了解了source_gen之后,我们开始着手自己的注解解析方案annotation_route 刚开始介入时,我们遇到了几个问题:只需要生成一个文件:由于一个输入文件对应了一个生成文件后缀,我们需要避免多余的文件生成需要知道在什么时候生成文件:我们需要在所有的备选文件扫描收集完成后再能进行映射表的生成source_gen对一个类只支持了一个注解,但存在多个url映射到一个页面 在一番思索后我们有了如下产出首先将注解分成两类,一类用于注解页面@ARoute,另一类用于注解使用者自己的router@ARouteRoot。routeBuilder拥有RouteGenerator实例,RouteGenerator实例,负责@ARoute注解;routeWriteBuilder拥有RouteWriterGenerator实例,负责@ARouteRoot注解。通过build库支持的配置文件build.yaml,控制两类builder的构造顺序,在routeBuilder执行完成后去执行routeWriteBuilder,这样我们就能准确的在所有页面注解扫描完成后开始生成自己的配置文件。 在注解解析工程中,对于@ARoute注解的页面,通过RouteGenerator将其配置信息交给拥有静态存储空间的Collector处理,同时将其输出内容设为null,即不会生成对应的文件。在@ARoute注解的所有页面扫描完成后,RouteWriteGenerator则会调用Writer,它从Collector中提取信息,并生成最后的配置文件。对于使用者,我们提供了一层友好的封装,在使用annotation_route配置到工程后,我们的路由代码发生了这样的变化: 使用前: class Router { Widget pageFromUrlAndQuery(String urlString, Map<String, dynamic> query) { if(urlString == ‘myapp://testa’) { return TestA(urlString, query); } else if(urlString == ‘myapp://testb’) { String absoluteUrl = Util.join(urlString, query); return TestB(url: absoluteUrl); } else if(urlString == ‘myapp://testc’) { String absoluteUrl = Util.join(urlString, query); return TestC(config: absoluteUrl); } else if(urlString == ‘myapp://testd’) { return TestD(PageDOption(urlString, query)); } else if(urlString == ‘myapp://teste’) { return TestE(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testf’) { return TestF(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testg’) { return TestG(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testh’) { return TestH(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testi’) { return TestI(PageDOption(urlString, query)); } return DefaultWidget; } }使用后:import ‘package:annotation_route/route.dart’; class MyPageOption { String url; Map<String, dynamic> query; MyPageOption(this.url, this.query); } class Router { ARouteInternal internal = ARouteInternalImpl(); Widget pageFromUrlAndQuery(String urlString, Map<String, dynamic> query) { ARouteResult routeResult = internal.findPage(ARouteOption(url: urlString, params: query), MyPageOption(urlString, query)); if(routeResult.state == ARouteResultState.FOUND) { return routeResult.widget; } return DefaultWidget; } }目前该方案已在闲鱼app内稳定运行,我们提供了基础的路由参数,随着flutter业务场景越来越复杂,我们也会在注解的自由度上进行更深的探索。关于annotation_route更加详细的安装和使用说明参见github地址:https://github.com/alibaba-flutter/annotation_route ,在使用中遇到任何问题,欢迎向我们反馈。本文作者:闲鱼技术-兴往阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 18, 2018 · 2 min · jiezi

追踪解析Spring ioc启动源码(2)

接上篇3 reader 注册配置类该 part 的起点:public AnnotationConfigApplicationContext(Class<?>… annotatedClasses) { this(); register(annotatedClasses); // 3 reader 注册配置类 refresh();}该行代码会将 iocConfig bean 注册到 reader 中AnnotationConfigApplicationContext 的 register 方法://AnnotationConfigApplicationContext.classpublic void register(Class<?>… annotatedClasses) { //参数非空效验 Assert.notEmpty(annotatedClasses, “At least one annotated class must be specified”); //调用 AnnotatedBeanDefinitionReader 的 register 方法 this.reader.register(annotatedClasses); }上述方法主要是调用了 AnnotatedBeanDefinitionReader 的 register 方法://AnnotatedBeanDefinitionReader.classpublic void register(Class<?>… annotatedClasses) { for (Class<?> annotatedClass : annotatedClasses) { registerBean(annotatedClass); }}上述方法循环调用了 AnnotatedBeanDefinitionReader 的 registerBean 方法://AnnotatedBeanDefinitionReader.classpublic void registerBean(Class<?> annotatedClass) { doRegisterBean(annotatedClass, null, null, null);}上述方法调用了 AnnotatedBeanDefinitionReader 的 doRegisterBean 方法,这个方法比较长,需要重点关注://AnnotatedBeanDefinitionReader.class<T> void doRegisterBean(Class<T> annotatedClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer… definitionCustomizers) { //用 BeanDefinition 包装 iocConfig AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass); //此段代码用于处理 Conditional 注解,在特定条件下阻断 bean 的注册 //本例中此处不会 return //3.1 if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } //用来创建 bean 的 supplier,会替代掉 bean 本身的创建方法 //instanceSupplier 一般情况下为 null abd.setInstanceSupplier(instanceSupplier); //此行代码处理 scope 注解,本例中 scope 是默认值 singleton //3.2 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); //bean name 在本例中为自动生成的 iocConfig //3.3 String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); //特定注解解析,本例中均不做操作 //3.4 AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); //本例中 qualifiers 传入的是 null //3.5 if (qualifiers != null) { for (Class<? extends Annotation> qualifier : qualifiers) { if (Primary.class == qualifier) { abd.setPrimary(true); } else if (Lazy.class == qualifier) { abd.setLazyInit(true); } else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } } } //本例中 definitionCustomizers 传入的是 null //3.6 for (BeanDefinitionCustomizer customizer : definitionCustomizers) { customizer.customize(abd); } //用 BeanDefinitionHolder 包装 BeanDefinition BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd,beanName); //此行代码与动态代理和 scope 注解有关,但是在本案例中没有做任何操作,只是返回了传入的 definitionHolder //3.7 definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); //iocConfig 注册 // 3.8 BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); }3.1看一下上述方法的片段:if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return;}首先需要了解到 abd 的 getMetadata() 方法会获取到 abd 中的 metadata 对象。该对象是一个 StandardAnnotationMetadata 的实例化对象,在创建的时候会利用 java.Class 中的 api 获取 bean 中所有的注解,并保存为一个数组://StandardAnnotationMetadata.classpublic StandardAnnotationMetadata(Class<?> introspectedClass, boolean nestedAnnotationsAsMap) { //此处的 introspectedClass 即为 bean 的 class //父类的构造器用于内部保存 bean 的 class super(introspectedClass); //获取所有的注解 this.annotations = introspectedClass.getAnnotations(); //nestedAnnotationsAsMap 暂时用不上,按下不表 //nestedAnnotationsAsMap = true this.nestedAnnotationsAsMap = nestedAnnotationsAsMap;}conditionEvaluator 是一个注解解析器,在 AnnotatedBeanDefinitionReader 创建的时候在其构造方法内被创建:this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);追踪 conditionEvaluator 的 shouldSkip(…) 方法://ConditionEvaluator.classpublic boolean shouldSkip(AnnotatedTypeMetadata metadata) { return shouldSkip(metadata, null); //调用自身的重载方法}//ConditionEvaluator.classpublic boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { //metadata 在此处不为 null //判断 bean 是否使用了 Conditional 注解 if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { //如果 metadata为空或者 bean 没有使用 Conditional 注解,就会返回 false return false; } //第一次调用该方法的时候,phase 为 null if (phase == null) { //下列源码规整一下,其实是四个条件: //1 bean.metadata 是 AnnotationMetadata 或其子类 //2 bean 使用了 Configuration 注解 //3 bean 不是一个接口 //4 bean 使用了 Component、ComponentScan、Import、ImportResource 这四个注解之一,或者使用了 Bean 注解 //这四个条件中满足 1、2 或者 1、3、4 就会进入 if 语句中 //请注意,对于 config bean 来说,只要使用了 Conditional 注解,必然会进入到语句中 if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } List<Condition> conditions = new ArrayList<>(); //getConditionClasses(metadata) 会获取到 Conditional 注解中的 value 数组 for (String[] conditionClasses : getConditionClasses(metadata)) { //遍历数组 for (String conditionClass : conditionClasses) { //利用反射获取实例化数组内的 class Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); //获取所有的 canditionClass 并以次存入到列表中 } } //利用了 List 自带的排序 api 进行排序 AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } //对于 Conditional 内的 value 并非是实现 ConfigurationCondition 接口的 class,requiredPhase == null 必然为 true;对于实现了该接口的 class,requiredPhase == phase 必然为 true //所以要注意,如果 value class 的 matches(…) 方法返回 false,则会在此处阻断 bean 的注册 if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } //正常情况下,做完所有检查工作之后还是会返回 false return false;}可以看到这个方法其实是 Conditional 注解的解析器,对于未使用这个注解的 bean,直接就返回了,不会继续往下走。先来看一下 Conditional 的源码:@Target({ElementType.TYPE, ElementType.METHOD}) //可以标注在类和方法上方@Retention(RetentionPolicy.RUNTIME) //注解生命周期@Documented //javadoc 相关public @interface Conditional { //class 数组 //这个数组里的值必须要是实现了 Condition 接口的类 //注意这个 value 没有默认值,如果要使用该注解就必须要填入 Class<? extends Condition>[] value();}顺便来看一下 Condition 接口:public interface Condition { //这个方法会返回一个 boolean 值,如果为 true,则将继承该接口的类注入到 Conditional 修饰的 bean 中 boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);}conditional 的具体内容有待研究,不展开。3.2看下方代码片段:ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);abd.setScope(scopeMetadata.getScopeName()); scopeMetadataResolver 是一个定义在 AnnotatedBeanDefinitionReader 里的 AnnotationScopeMetadataResolver 对象。顾名思义,其主要作用是解析 scope 标签。先来看一下 Scope 注解的定义源码:@Target({ElementType.TYPE, ElementType.METHOD}) //可以标注在类和方法上方@Retention(RetentionPolicy.RUNTIME) //注解生命周期@Documented //javadoc 相关public @interface Scope { //value 是平时开发中最常用到的 scope 属性,用来设置是否是单例模式 //在处理注解的时候 value 属性会被转化成 scopeName 属性来看待 //所以两个属性其实是一样的 String value() default “”; String scopeName() default “”; //代理模式设置,默认为无代理 ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;}ScopedProxyMode 是一个枚举类,没有任何处理业务逻辑的代码,一同放在这里:public enum ScopedProxyMode { DEFAULT, //不使用代理,default 和 no 是等价的 NO, INTERFACES, //使用 jdk 自带的动态代理 api 进行创建 TARGET_CLASS; //target-class 模式,需要使用 cglib 进行 bean 的创建 }AnnotationScopeMetadataResolver 的 resolveScopeMetadata(…) 方法具体实现如下://AnnotationScopeMetadataResolver.classpublic ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { //创建一个 metadata 对象用于返回 ScopeMetadata metadata = new ScopeMetadata(); if (definition instanceof AnnotatedBeanDefinition) { AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; //从 bean 的注解里寻找 scope 这个注解 AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor( annDef.getMetadata(), this.scopeAnnotationType); //如果 bean 确实是用了 scope 注解 if (attributes != null) { metadata.setScopeName(attributes.getString(“value”)); //存入 scope 的 value 属性值 //获取 proxyMode 属性值 ScopedProxyMode proxyMode = attributes.getEnum(“proxyMode”); //default 和 no 是等同的,默认会转化成 no 进行处理 if (proxyMode == ScopedProxyMode.DEFAULT) { //this.defaultProxyMode = ScopedProxyMode.NO proxyMode = this.defaultProxyMode; } metadata.setScopedProxyMode(proxyMode); //存入 scope 的 proxyMode 属性值 } } //没有使用 scope 的情况下会返回一个新建的 metadata return metadata;}annDef.getMetadata() 会获取到一个 AnnotationMetadata 对象,里面包含了 bean 的所有注解信息。scopeAnnotationType 是一个定义在 AnnotationScopeMetadataResolver 里的 Class 对象:protected Class<? extends Annotation> scopeAnnotationType = Scope.class;可见 AnnotationConfigUtils 的 attributesFor(…) 就是去注解集里查找 scope 注解,并且封装成一个 AnnotationAttributes 返回。AnnotationAttributes 是 Spring 用来存储注解所定义的一种数据结构,本质上是一个 LinkedHashMap。再回到本小节最上方的代码:abd.setScope(scopeMetadata.getScopeName()); 最后其实 BeanDefinition 只接收了 scopeName,而没有接收 proxyMode。proxyMode 属性会在后面代码中用到。3.3看下方代码片段:String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); beanNameGenerator 是一个定义在 AnnotatedBeanDefinitionReader 里的 AnnotationBeanNameGenerator 对象,顾名思义用来生成 bean 的名称:private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();追踪一下 generateBeanName(…) 方法://AnnotationBeanNameGenerator.classpublic String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { if (definition instanceof AnnotatedBeanDefinition) { //determineBeanNameFromAnnotation(…) 方法会从 bean 的所有注解里去遍历搜寻 bean 名称 String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition); //如果此处的 beanName 非空,则表明在注解里找到了定义的 bean 名称 if (StringUtils.hasText(beanName)) { // Explicit bean name found. return beanName; } } //没有在前面 return,证明 bean 没有被设置名称,则在此处默认生成一个名称 return buildDefaultBeanName(definition, registry);}看一眼 buildDefaultBeanName(…) 方法://AnnotationBeanNameGenerator.classprotected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { return buildDefaultBeanName(definition);}其实这个方法只用到了 definition,而没有使用到传入的 registry。继续追踪代码实现://AnnotationBeanNameGenerator.classprotected String buildDefaultBeanName(BeanDefinition definition) { //该处返回的是 bean 的整个 class 路径和名称 String beanClassName = definition.getBeanClassName(); //beanClassName 非空判断 Assert.state(beanClassName != null, “No bean class name set”); //截掉 class 的路径,只取 class 名称 String shortClassName = ClassUtils.getShortName(beanClassName); //将首字母小写并返回 return Introspector.decapitalize(shortClassName);}3.4看下方代码实现:AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); 追踪这行代码://AnnotationConfigUtils.classpublic static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd) { //调用重载方法 processCommonDefinitionAnnotations(abd, abd.getMetadata());}//AnnotationConfigUtils.classstatic void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) { //检查 lazy 注解 AnnotationAttributes lazy = attributesFor(metadata, Lazy.class); if (lazy != null) { abd.setLazyInit(lazy.getBoolean(“value”)); }else if (abd.getMetadata() != metadata) { //这里还有一个补充检查,如果传入的 metadata 不是 abd 内部的 metadata的话,还会继续进来判断一次 //在本例中没什么必要 lazy = attributesFor(abd.getMetadata(), Lazy.class); if (lazy != null) { abd.setLazyInit(lazy.getBoolean(“value”)); } } //检查 primary 注解 if (metadata.isAnnotated(Primary.class.getName())) { abd.setPrimary(true); } //检查 dependsOn 注解 AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class); if (dependsOn != null) { abd.setDependsOn(dependsOn.getStringArray(“value”)); } //检查 role 注解 AnnotationAttributes role = attributesFor(metadata, Role.class); if (role != null) { abd.setRole(role.getNumber(“value”).intValue()); } //检查 description 注解 AnnotationAttributes description = attributesFor(metadata, Description.class); if (description != null) { abd.setDescription(description.getString(“value”)); }}其实上述代码的主体都是类似的,步骤都是尝试从 metadata 中获取特定注解,如果获取到了就将其作为一个属性值 set 进 abd 中。这里需要强调 abd 就是要注册的 bean 的 BeanDefinition 包装对象。本例中没有用到上述的注解,所以均为 null。3.5看下方代码:if (qualifiers != null) { //在 qualifiers 不为 null 的情况下会遍历该集合,并将当中的所有的元素解析出来,进行业务操作 for (Class<? extends Annotation> qualifier : qualifiers) { if (Primary.class == qualifier) { abd.setPrimary(true); }else if (Lazy.class == qualifier) { abd.setLazyInit(true); }else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } }}上述代码是针对 qualifier 注解的解析,和 3.4 很类似。AutowireCandidateQualifier 是注解的包装类,储存了一个特定注解的名字和 value。abd 的 addQualifier(…) 方法会将这个创建出来的包装类存储到一个 map 对象里。3.6看下方代码:for (BeanDefinitionCustomizer customizer : definitionCustomizers) { customizer.customize(abd);}这段代码是 Spring5 中新加入的。根据注释,官方应该是留下这个接口用以让开发者通过 lambda 表达式去定义 bean。3.7看下方代码:definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); 先来追踪 applyScopedProxyMode(…) 方法://AnnotationConfigUtils.classstatic BeanDefinitionHolder applyScopedProxyMode( ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { //先判断 scope 注解的使用 ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode(); if (scopedProxyMode.equals(ScopedProxyMode.NO)) { return definition; } //判断具体使用哪种模式 boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS); //此行代码会连向 spring-aop 包下的类来处理 return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);}注意,此处传入的 metadata 是上述 3.2 小节中新建出来并返回的对象:ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);对于一般没有使用 scope 注解或者 scope 注解为默认的 bean,此时 scopedProxyMode 是等于 ScopedProxyMode.NO 的。对于 scopedProxyMode 不为 NO 的 bean,均为需要使用动态代理进行创建的对象,区别只是使用 jdk 自带的 api 还是使用 cglib 包。追踪一下 ScopedProxyCreator 的 createScopedProxy(…) 方法://ScopedProxyCreator.classpublic static BeanDefinitionHolder createScopedProxy( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) { return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass);}继续追踪://ScopedProxyUtils.classpublic static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, BeanDefinitionRegistry registry, boolean proxyTargetClass) { //bean 的名称 String originalBeanName = definition.getBeanName(); //bean 的 BeanDefinition 包装类 BeanDefinition targetDefinition = definition.getBeanDefinition(); //在 bean 的名称前面加上字符串 “scopedTarget.” ,拼成 targetBeanName //比如 scopedTarget.iocConfig String targetBeanName = getTargetBeanName(originalBeanName); //以下代码用来组装一个动态代理的工厂 bean,这个 bean 是用来动态代理的主体 RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class); proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName)); proxyDefinition.setOriginatingBeanDefinition(targetDefinition); proxyDefinition.setSource(definition.getSource()); proxyDefinition.setRole(targetDefinition.getRole()); proxyDefinition.getPropertyValues().add(“targetBeanName”, targetBeanName); if (proxyTargetClass) { targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); }else { proxyDefinition.getPropertyValues().add(“proxyTargetClass”, Boolean.FALSE); } proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); if (targetDefinition instanceof AbstractBeanDefinition) { proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition); } targetDefinition.setAutowireCandidate(false); targetDefinition.setPrimary(false); //此处的 targetDefinition 是传入的 bean 的包装类 //这一步会提前将该 bean 进行注册 //注册过程见 2.2 registry.registerBeanDefinition(targetBeanName, targetDefinition); //返回的其实是动态代理所需要的工厂 bean return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());}scope 的具体内容有待研究,不展开。3.8在上例代码中的这一行中:BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);传入的 definitionHolder 就是 iocConfig bean 的包装对象;而传入的 registry 就是在 ApplicationContext 中实例化的 BeanFactory,此处具体而言就是DefaultListableBeanFactory。继续追踪这行代码的内部实现://BeanDefinitionReaderUtils.classpublic static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { //获取bean的名称 String beanName = definitionHolder.getBeanName(); //调用 AnnotationConfigApplicationContext 的 registerBeanDefinition 方法 //注册过程见 2.2 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); //处理bean的别名,本例中没有别名,不进入循环 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } }}alias 的具体内容有待研究,不展开。到此为止,iocConfig bean 已经被注册到 bean factory 中。To Be Continued … ...

December 18, 2018 · 7 min · jiezi

Spring源码一(容器的基本实现2)

前言:继续前一章。一、porfile 属性的使用如果你使用过SpringBoot, 你一定会知道porfile配置所带来的方便, 通过配置开发环境还是生产环境, 我们可以十分方便的切换开发环境,部署环境,更换不同的数据库。 可能为了让Java开发者转向SpringBoot开发, Spring在5.x之后停止了对这个属性的支持。所以本文也就不再继续描述这一属性。二、bean标签的解析及注册Spring中的标签分为默认标签和自定义标签两种,而这两种标签的用法及解析方式存在着很大的不同,默认标签是在parseDefaultElement中进行的,函数中的功能一目了然,分别对4种标签(import, alias、bean、beans)做了不同的处理。private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { importBeanDefinitionResource(ele); } else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { processAliasRegistration(ele); } else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { processBeanDefinition(ele, delegate); } else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // recurse doRegisterBeanDefinitions(ele); }}我们不得不承认,Spring5.x提倡我们更少的使用xml文件,而是更多的使用注解进行配置,而且如果你经常使用Springboot的话,那么你肯定知道习惯优于约定,并且springboot中只需要一个配置文件,虽然这有时根本无法满足需求,这里不做关于springboot的更多的说明。不过这并不影响Spring内部的实现,现在主要还是从xml文件分析一下bean标签的解析及注册。在4中标签的解析中,对bean标签的解析最为复杂也最为重要, 所以我们从这个标签进行深入的分析。不过在这之前我还是要将之前怎么加载这个文件的部分进行一下回忆还记得上一部分,有一个这样的方法:/** * This implementation parses bean definitions according to the “spring-beans” XSD * (or DTD, historically). * <p>Opens a DOM Document; then initializes the default settings * specified at the {@code <beans/>} level; then parses the contained bean definitions. /@Overridepublic void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; doRegisterBeanDefinitions(doc.getDocumentElement());}如果确实对一步感兴趣可以追溯下去,这样就可以发现下面这段代码:/* * Parse the elements at the root level in the document: * “import”, “alias”, “bean”. * @param root the DOM root element of the document / protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { if (delegate.isDefaultNamespace(root)) { NodeList nl = root.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { Element ele = (Element) node; if (delegate.isDefaultNamespace(ele)) { parseDefaultElement(ele, delegate); } else { delegate.parseCustomElement(ele); } } } } else { delegate.parseCustomElement(root); } }这段代码可能有点难以理解,不过当知道了if(delegate.isDefaultNamespace(ele)) 这个方法的作用就知道了,这其实就是在对标签进行一次处理而已, 是默认标签的就交给默认的处理方式,是自定义标签的话就换另一种处理方式。这就是这个方法中做的事了。 public boolean isDefaultNamespace(Node node) { return isDefaultNamespace(getNamespaceURI(node));}这里的Node节点定义了所有的Spring提供的默认标签的解析结果。那parseDefaultElement(ele, delegate)这个方法又在做些什么呢?其实不过是对根级节点的标签进行解析分类而已,现在我们先分析一下bean标签, 所以现在只看针对于标签做了些什么。else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { processBeanDefinition(ele, delegate);}进入这个方法/* * Process the given bean element, parsing the bean definition * and registering it with the registry. /protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); if (bdHolder != null) { bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); try { // Register the final decorated instance. BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); } catch (BeanDefinitionStoreException ex) { getReaderContext().error(“Failed to register bean definition with name ‘” + bdHolder.getBeanName() + “’”, ele, ex); } // Send registration event. getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); }}这里使用Spring源码深入解析的一段:三、解析BeanDefiniton这个部分是Spring解析配置文件的最重要的部分, 根本就是解析标签并加载, 在使用Spring进行配置的时候不难发现, 我们有以下几个重要的根级标签,bean, imort, alias, nested-beans, 下面的内容就主要介绍一下bean标签的解析。上一个小结的结尾部分已经涉及了这个的处理,继续上面的内容, 我们会发现,实际上Spring首先是先通过“Bean定义解析委托”来获得了一个BeanDefinitionHolder, 在上面的分析中,我们似乎只注意了Element,而忘记了委托的是什么时候出现的,事实上这个委托是在DefaultBeanDefinitionDocumentReader在这个类中的时候就已经创建了这个委托, 并且一直通过参数的方式保存着这个委托, 知道们希望获得一个BeanDefinitionHolder的时候才真正的发挥作用,那么这个委托具体是什么呢?这个委托的作用是状态的保存, 早在DefaultBeanDefinitionDocumentReader 这个类中使用的时候就通过xml解析的上下文,保存了bean标签中的所有状态,这些状态包括,….等等等……那么BeanDefinitionHolder的作用又是什么呢? 通过这个holder, 可是实现注册的功能这是一个十分重要的功能,后面会具体分析这个功能。现在首先要看的是怎么获得的这个holder呢:/* * Parses the supplied {@code <bean>} element. May return {@code null} * if there were errors during parse. Errors are reported to the * {@link org.springframework.beans.factory.parsing.ProblemReporter}. / @Nullable public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { String id = ele.getAttribute(ID_ATTRIBUTE); String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); List<String> aliases = new ArrayList<>(); if (StringUtils.hasLength(nameAttr)) { String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); aliases.addAll(Arrays.asList(nameArr)); } String beanName = id; if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { beanName = aliases.remove(0); if (logger.isTraceEnabled()) { logger.trace(“No XML ‘id’ specified - using ‘” + beanName + “’ as bean name and " + aliases + " as aliases”); } } if (containingBean == null) { checkNameUniqueness(beanName, aliases, ele); } AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); if (beanDefinition != null) { if (!StringUtils.hasText(beanName)) { try { if (containingBean != null) { beanName = BeanDefinitionReaderUtils.generateBeanName( beanDefinition, this.readerContext.getRegistry(), true); } else { beanName = this.readerContext.generateBeanName(beanDefinition); // Register an alias for the plain bean class name, if still possible, // if the generator returned the class name plus a suffix. // This is expected for Spring 1.2/2.0 backwards compatibility. String beanClassName = beanDefinition.getBeanClassName(); if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { aliases.add(beanClassName); } } if (logger.isTraceEnabled()) { logger.trace(“Neither XML ‘id’ nor ’name’ specified - " + “using generated bean name [” + beanName + “]”); } } catch (Exception ex) { error(ex.getMessage(), ele); return null; } } String[] aliasesArray = StringUtils.toStringArray(aliases); return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); } return null; }此处引用Spring源码解析中的一段内容:以上便是对默认标签解析的全过程了。当然,对Spring的解析犹如洋葱剥皮一样,一层一层的进行,尽管现在只能看到对属性id以及name的解析,但是很庆幸,思路我们已经了解了。在开始对属性进行全面分析之前, Spring在最外层做了一个当前成的功能架构, 在当前成完成的主要工作包括以下的内容。(1)提取元素中的id和name属性。(2)进一步解析其他所有属性并统一封装至GenericBeanDefinition类型的实例中。(3)如果检测到bean没有指定beanName,那么使用默认规则为此Bean生成beanName。(4)将检测到的信息封装到BeanDefintionHolder的实例中。继续跟进代码:/* * Parse the bean definition itself, without regard to name or aliases. May return * {@code null} if problems occurred during the parsing of the bean definition. */@Nullablepublic AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, @Nullable BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); String className = null; if (ele.hasAttribute(CLASS_ATTRIBUTE)) { className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); } String parent = null; if (ele.hasAttribute(PARENT_ATTRIBUTE)) { parent = ele.getAttribute(PARENT_ATTRIBUTE); } try { AbstractBeanDefinition bd = createBeanDefinition(className, parent); parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); parseMetaElements(ele, bd); parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); parseConstructorArgElements(ele, bd); parsePropertyElements(ele, bd); parseQualifierElements(ele, bd); bd.setResource(this.readerContext.getResource()); bd.setSource(extractSource(ele)); return bd; } catch (ClassNotFoundException ex) { error(“Bean class [” + className + “] not found”, ele, ex); } catch (NoClassDefFoundError err) { error(“Class that bean class [” + className + “] depends on not found”, ele, err); } catch (Throwable ex) { error(“Unexpected failure during bean definition parsing”, ele, ex); } finally { this.parseState.pop(); } return null;}通过对代码的跟踪,事实上,我们很容易发现,这里的className就是从上一个方法中的通过解析别名得到beanName,在这里通过beanName又从已存的元素中获取得到的。同样的这个标签的父级元素parent也是这样获取得到。而接下来的操作也就是对各种属性的具体的解析操作了,诸如:me他, lookup-method, replace-method, property, qualifier子元素等。BeanDefinition是一个接口,在Spring中存在三种实现:RootBeanDefinition、ChildBeanDefinition以及GenericBeanDefinition。三种实现均继承了AbstractBeanDefinition,其中BeanDefinition是配置文件<bean>元素标签在容器中的内部表示形式。<bean>元素标签拥有class、scope、lazy-init等配置属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和<bean>中的属性是一一对应的。其中RootBeanDefinition是最常用的实现类,它对应一般性的<bean>元素标签,GenericBeanDefinition是自2.5版本以后新加入的bean文件配置属性定义类,是一站式服务类。在配置文件中可以定义父<bean>和子<bean>,父<bean>用RootBeanDefinition表示,而子<bean>用ChildBeanDefinition表示,而没有父<bean>的<bean>就使用RootBeanDefinition表示。AbstractBeanDefinition对两者共同的类信息进行抽象。Spring通过BeanDefinition将配置文件中<bean>配置信息转换为容器的内部表示,并将这些BeanDefinition注册到BeanDefinitionRegistry中。Spring容器的BeanDefinitionRestry就像是Spring配置信息的内存数据库,主要是以map的形式保存。后续操作直接从BeanDefinitionRegistry中读取配置信息。 BeanDefinition 及其实现类 由此可知,要解析属性首先要创建用于承载属性的实例,也就是创建GenericBeanDefinition类型的实例。而代码createBeanDefinition(className, parent)的作用就是实现此功能。 ...

December 18, 2018 · 4 min · jiezi

Spring Cloud Stream消费失败后的处理策略(四):重新入队(RabbitMQ)

应用场景之前我们已经通过《Spring Cloud Stream消费失败后的处理策略(一):自动重试》一文介绍了Spring Cloud Stream默认的消息重试功能。本文将介绍RabbitMQ的binder提供的另外一种重试功能:重新入队。动手试试准备一个会消费失败的例子,可以直接沿用前文的工程,也可以新建一个,然后创建如下代码的逻辑:@EnableBinding(TestApplication.TestTopic.class)@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @RestController static class TestController { @Autowired private TestTopic testTopic; /** * 消息生产接口 * * @param message * @return / @GetMapping("/sendMessage") public String messageWithMQ(@RequestParam String message) { testTopic.output().send(MessageBuilder.withPayload(message).build()); return “ok”; } } /* * 消息消费逻辑 */ @Slf4j @Component static class TestListener { private int count = 1; @StreamListener(TestTopic.INPUT) public void receive(String payload) { log.info(“Received payload : " + payload + “, " + count); throw new RuntimeException(“Message consumer failed!”); } } interface TestTopic { String OUTPUT = “example-topic-output”; String INPUT = “example-topic-input”; @Output(OUTPUT) MessageChannel output(); @Input(INPUT) SubscribableChannel input(); }}内容很简单,既包含了消息的生产,也包含了消息消费。消息消费的时候主动抛出了一个异常来模拟消息的消费失败。在启动应用之前,还要记得配置一下输入输出通道对应的物理目标(exchange或topic名)、并设置一下分组,比如:spring.cloud.stream.bindings.example-topic-input.destination=test-topicspring.cloud.stream.bindings.example-topic-input.group=stream-exception-handlerspring.cloud.stream.bindings.example-topic-input.consumer.max-attempts=1spring.cloud.stream.rabbit.bindings.example-topic-input.consumer.requeue-rejected=truespring.cloud.stream.bindings.example-topic-output.destination=test-topic完成了上面配置之后,启动应用并访问localhost:8080/sendMessage?message=hello接口来发送一个消息到MQ中了,此时可以看到程序不断的抛出了消息消费异常。这是由于这里我们多加了一个配置:spring.cloud.stream.rabbit.bindings.example-topic-input.consumer.requeue-rejected=true。在该配置作用之下,消息消费失败之后,并不会将该消息抛弃,而是将消息重新放入队列,所以消息的消费逻辑会被重复执行,直到这条消息消费成功为止。深入思考在完成了上面的这个例子之后,可能读者会有下面两个常见问题:问题一:之前介绍的Spring Cloud Stream默认提供的默认功能(spring.cloud.stream.bindings.example-topic-input.consumer.max-attempts)与本文所说的重入队列实现的重试有什么区别?Spring Cloud Stream默认提供的默认功能只是对处理逻辑的重试,它们的处理逻辑是由同一条消息触发的。而本文所介绍的重新入队史通过重新将消息放入队列而触发的,所以实际上是收到了多次消息而实现的重试。问题二:如上面的例子那样,消费一直不成功,这些不成功的消息会被不断堆积起来,如何解决这个问题?对于这个问题,我们可以联合前文介绍的DLQ队列来完善消息的异常处理。我们只需要增加如下配置,自动绑定dlq队列:spring.cloud.stream.rabbit.bindings.example-topic-input.consumer.auto-bind-dlq=true然后改造一下消息处理程序,可以根据业务情况,为进入dlq队列增加一个条件,比如下面的例子:@StreamListener(TestTopic.INPUT)public void receive(String payload) { log.info(“Received payload : " + payload + “, " + count); if (count == 3) { count = 1; throw new AmqpRejectAndDontRequeueException(“tried 3 times failed, send to dlq!”); } else { count ++; throw new RuntimeException(“Message consumer failed!”); }}设定了计数器count,当count为3的时候抛出AmqpRejectAndDontRequeueException这个特定的异常。此时,当只有当抛出这个异常的时候,才会将消息放入DLQ队列,从而不会造成严重的堆积问题。代码示例本文示例读者可以通过查看下面仓库的中的stream-exception-handler-4项目:GithubGitee如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!以下专题教程也许您会有兴趣Spring Boot基础教程Spring Cloud基础教程本文首发:http://blog.didispace.com/spr… ...

December 18, 2018 · 1 min · jiezi

Spring Cloud Stream消费失败后的处理策略(三):使用DLQ队列(RabbitMQ)

应用场景前两天我们已经介绍了两种Spring Cloud Stream对消息失败的处理策略:自动重试:对于一些因环境原因(如:网络抖动等不稳定因素)引发的问题可以起到比较好的作用,提高消息处理的成功率。自定义错误处理逻辑:如果业务上,消息处理失败之后有明确的降级逻辑可以弥补的,可以采用这种方式,但是2.0.x版本有Bug,2.1.x版本修复。那么如果代码本身存在逻辑错误,无论重试多少次都不可能成功,也没有具体的降级业务逻辑,之前在深入思考中讨论过,可以通过日志,或者降级逻辑记录的方式把错误消息保存下来,然后事后分析、修复Bug再重新处理。但是很显然,这样做非常原始,并且太过笨拙,处理复杂度过高。所以,本文将介绍利用中间件特性来便捷地处理该问题的方案:使用RabbitMQ的DLQ队列。动手试试准备一个会消费失败的例子,可以直接沿用前文的工程。也可以新建一个,然后创建如下代码的逻辑:@EnableBinding(TestApplication.TestTopic.class)@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @RestController static class TestController { @Autowired private TestTopic testTopic; /** * 消息生产接口 * * @param message * @return / @GetMapping("/sendMessage") public String messageWithMQ(@RequestParam String message) { testTopic.output().send(MessageBuilder.withPayload(message).build()); return “ok”; } } /* * 消息消费逻辑 */ @Slf4j @Component static class TestListener { @StreamListener(TestTopic.INPUT) public void receive(String payload) { log.info(“Received payload : " + payload); throw new RuntimeException(“Message consumer failed!”); } } interface TestTopic { String OUTPUT = “example-topic-output”; String INPUT = “example-topic-input”; @Output(OUTPUT) MessageChannel output(); @Input(INPUT) SubscribableChannel input(); }}内容很简单,既包含了消息的生产,也包含了消息消费。消息消费的时候主动抛出了一个异常来模拟消息的消费失败。在启动应用之前,还要记得配置一下输入输出通道对应的物理目标(exchange或topic名)、并设置一下分组,比如:spring.cloud.stream.bindings.example-topic-input.destination=test-topicspring.cloud.stream.bindings.example-topic-input.group=stream-exception-handlerspring.cloud.stream.bindings.example-topic-input.consumer.max-attempts=1spring.cloud.stream.rabbit.bindings.example-topic-input.consumer.auto-bind-dlq=truespring.cloud.stream.bindings.example-topic-output.destination=test-topic这里加入了一个重要配置spring.cloud.stream.rabbit.bindings.example-topic-input.consumer.auto-bind-dlq=true,用来开启DLQ(死信队列)。完成了上面配置之后,启动应用并访问localhost:8080/sendMessage?message=hello接口来发送一个消息到MQ中了,此时可以看到消费失败后抛出了异常,消息消费失败,记录了日志。此时,可以查看RabbitMQ的控制台如下:其中,test-topic.stream-exception-handler.dlq队列就是test-topic.stream-exception-handler的dlq(死信)队列,当test-topic.stream-exception-handler队列中的消息消费时候之后,就会将这条消息原封不动的转存到dlq队列中。这样这些没有得到妥善处理的消息就通过简单的配置实现了存储,之后,我们还可以通过简单的操作对这些消息进行重新消费。我们只需要在控制台中点击test-topic.stream-exception-handler.dlq队列的名字进入到详情页面之后,使用Move messages功能,直接将这些消息移动回test-topic.stream-exception-handler队列,这样这些消息就能重新被消费一次。如果Move messages功能中是如下内容:To move messages, the shovel plugin must be enabled, try:$ rabbitmq-plugins enable rabbitmq_shovel rabbitmq_shovel_management那是由于没有安装对应的插件,只需要根据提示的命令安装就能使用该命令了。深入思考先来总结一下在引入了RabbitMQ的DLQ之后,对于消息异常处理更为完整一些的基本思路:瞬时的环境抖动引起的异常,利用重试功能提高处理成功率如果重试依然失败的,日志报错,并进入DLQ队列日志告警通知相关开发人员,分析问题原因解决问题(修复程序Bug、扩容等措施)之后,DLQ队列中的消息移回重新处理在这样的整体思路中,可能还涉及一些微调,这里举几个常见例子,帮助读者进一步了解一些特殊的场景和配置使用!场景一:有些消息在业务上存在时效性,进入死信队列之后,过一段时间再处理已经没有意义,这个时候如何过滤这些消息呢?只需要配置一个参数即可:spring.cloud.stream.rabbit.bindings.example-topic-input.consumer.dlq-ttl=10000该参数可以控制DLQ队列中消息的存活时间,当超过配置时间之后,该消息会自动的从DLQ队列中移除。场景二:可能进入DLQ队列的消息存在各种不同的原因(不同异常造成的),此时如果在做补救措施的时候,还希望根据这些异常做不同的处理时候,我们如何区分这些消息进入DLQ的原因呢?再来看看这个参数:spring.cloud.stream.rabbit.bindings.example-topic-input.consumer.republish-to-dlq=true该参数默认是false,如果设置了死信队列的时候,会将消息原封不动的发送到死信队列(也就是上面例子中的实现),此时大家可以在RabbitMQ控制台中通过Get message(s)功能来看看队列中的消息,应该如下图所示:这是一条原始消息。如果我们该配置设置为true的时候,那么该消息在进入到死信队列的时候,会在headers中加入错误信息,如下图所示:这样,不论我们是通过移回原通道处理还是新增订阅处理这些消息的时候就可以以此作为依据进行分类型处理了。关于RabbitMQ的binder中还有很多关于DLQ的配置,这里不一一介绍了,上面几个是目前笔者使用过的几个,其他一些暂时认为采用默认配置已经够用,除非还有其他定制要求,或者是存量内容,需要去适配才会去配置。读者可以查看官方文档了解更多详情!代码示例本文示例读者可以通过查看下面仓库的中的stream-exception-handler-3项目:GithubGitee如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!以下专题教程也许您会有兴趣Spring Boot基础教程Spring Cloud基础教程本文首发:http://blog.didispace.com/spr… ...

December 18, 2018 · 1 min · jiezi

Spring思维导图,让Spring不再难懂(AOP 篇)

转载自:1、微信公众号【Java思维导图】2、若谷先生一、什么是 AOP ?AOP(Aspect-Oriented Programming,面向方面编程),对 OOP(Object-Oriented Programming,面向对象编程)【OOP与AOP】概念AOP(Aspect-Oriented Programming,面向方面编程)OOP(Object-Oriented Programming,面向对象编程)方向OOP 定义从上到下的关系AOP 定义从左到右的关系【两个部分】核心关注点业务处理的主要流程横切关注点与业务主要流程关系不大的部分经常发生在核心关注点的多处,而各处都是基本相似的功能如权限认证、日志、事务处理二、AOP 使用场景1、AOP框架种类AspectJJBoss AOPSpring AOP2、使用 AOP 场景性能监控:在方法调用前后记录调用事件,方法执行太长或超时报警。缓存代理:缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。软件破解:使用AOP修改软件的验证类的判断逻辑。记录日志:在方法执行前后记录系统日志。工作流系统:工作流系统需要将业务代码和流畅引擎代码混合在一起执行,那么可以使用 AOP 将其分离,并动态挂载业务。权限验证:方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕获。3、传统编码与AOP区别三、核心概念【术语】:切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是 J2EE 应用中一个关于横切关注点的很好列子。在 Spring AOP 中,切面可以使用基于模式或者基于 @Aspect 注解的方式来实现。连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在 Spring AOP 中,一个连接点总是表示一个方法的执行。切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如:当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是 AOP 的核心:Spring 缺省使用 Aspect 切入点语法。引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))Spring 允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,可以使用引入来使一个 bean 实现 isModified 接口,以便简化缓存机制。目标对象(Target Object):被一个或者多个切面所通知的对象,也被称为通知(advised)对象。既然 Spring AOP 是通过运行时代理实现的。这个对象永远是一个被代理(proxied)对象。AOP 代理(AOP Proxy):AOP 框架创建的对象,用来实现切面契约(例如,通知方法执行等等)。在 Spring 中,AOP 代理可以是 JDK 动态代理或者 CGLIB 代理。织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用 AspectJ 编译器),类加载时和运行时完成。Spring 和其他纯 Java AOP 框架一样,在运行时完成织入。通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括“around”、“before” 和 “after” 等不同类型的通知。许多 AOP 框架(包括 Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。【通知类型】前置通知(Before advice):@Before在某连接点之前执行的通知,但这个通知不能阻止了连接点之前的执行流程(除非它抛出一个异常)。后置通知(After returning advice):@After在某连接点正常完成后执行的通知,例如,一个方法么有抛出任何异常,正常返回。异常通知(After throwing advice):@After-returning在方法抛出异常退出时执行的通知。最终通知(After(finally)advice):@After-throwing当某连接点退出的时候执行的通知(不论是正常退出还是异常退出);环绕通知(Around advice):@Around包围一个连接点的通知,如方法调用,这是最强大的一种通知类型。三、简单例子1、基于注解的方式@Aspectj public class TransactionDemo { @Pointcut(value=“execution(* com.yangxin.core.service...(..))”) public void point() { //… } @Before(value=“point()”) public void before() { System.out.println(“transaction begin”); } @AfterReturning(value = “point()”) public void after() { System.out.println(“transaction commit”); } @Around(“point()”) public void around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println(“transaction begin”); joinPoint.proceed(); System.out.println(“transaction commit”); }} 在 applicationContext.xml 中配置:<aop:aspectj-autoproxy /> <bean id=“transactionDemo” class=“com.yangxin.core.transaction.TransactionDemo” /> 2、基于 XML 的方式<aop:config> <aop:aspect ref=“log”> <aop:pointcut expression="(execution( spring.ch3.topic1.Chief.(..)))" id=“chiefPointCut” /> <aop:before method=“washOven” pointcut-ref=“chiefPointCut” /> <aop:before method=“prepare” pointcut-ref=“chiefPointCut” /> <aop:after method=“after” pointcut-ref=“chiefPointCut” /> </aop:aspect></aop:config> 四、Spring AOP 原理AOP 代理其实是由 AOP 框架动态生成的一个对象,该对象可作为目标对象使用。AOP 代理包含了目标对象的全部方法,但 AOP 代理中的方法与目标对象的方法存在差异:AOP 方法在特定切入点添加了增强处理,并回调了目标对象的方法。Spring 的 AOP 代理由 Spring 的 IoC 容器负责生成、管理,其依赖关系也由 IoC 容器负责管理。因此,AOP 代理可以直接使用容器中的其他 Bean 实例作为目标,这种关系可由 IoC 容器的依赖注入提供。aop开发时,其中需要参与开发的只有 3 个部分:定义普通业务组件。定义切入点,一个切入点可能横切多个业务组件。定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作。五、两种动态代理方式Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。1、JDK 动态代理JDK动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。InvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。2、CGLib动态代理CGLib全称为Code Generation Library,是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展Java类与实现Java接口,CGLib封装了asm,可以再运行期动态生成新的class。和JDK动态代理相比较:JDK创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过CGLib创建动态代理。六、补充实例package springMVCmybatis.com.my.aop;import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.springframework.core.annotation.Order;@Aspect@Order(3)public class MyAopTest { @Pointcut(“execution( springMVCmybatis.addController.addEmp(..))”) private void pointCutMethod() { //… } @Pointcut(“execution(* springMVCmybatis.com.my.aop.UserServiceImp.*(..))”) private void testAOP() { //… } /** * 声明前置通知 ,JoinPont是srpring提供的静态变量, * 通过joinPoint参数可以获得目标方法的类名,方法参数,方法名等信息,这个参数可有可无。 / @Before(“pointCutMethod() || testAOP()”) public void doBefore(JoinPoint joinPoint) { System.out.println("@Before:开始添加–order=3"); } /* * 声明后置通知 ,如果result的类型与proceed执行的方法返回的参数类型不匹配那么就不会执行这个方法 / @AfterReturning(pointcut = “pointCutMethod() || testAOP()”, returning = “result”) public void doAfterReturning(String result) { System.out.println("@AfterReturning:后置通知–order=3"); System.out.println("—" + result + “—”); } /* * 声明例外通知 / @AfterThrowing(pointcut = “pointCutMethod() || testAOP()”, throwing = “e”) public void doAfterThrowing(Exception e) { System.out.println("@AfterThrowing:例外通知–order=3"); System.out.println(e.getMessage()); } /* * 声明最终通知 / @After(“pointCutMethod() || testAOP()”) public void doAfter() { System.out.println("@After:最终通知–order=3"); } /* * 声明环绕通知 * 参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数, * proceed()的返回值就是环绕通知的返回值,proceedingJoinPoint是个接口, * implement JoinPoint,所以也可以获得目标函数的类名,方法名等参数。 */ @Around(“pointCutMethod() || testAOP()”) public Object doAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("@Around:进入方法—环绕通知–order=3"); Object o = pjp.proceed(); System.out.println("@Around:退出方法—环绕通知–order=3"); return o; }}七、切点表达式1、方法签名表达式execution(<修饰符模式>?<返回类型模式><方法所在类的完全限定名称模式>(<参数模式>)<异常模式>?)execution(modifiers-pattern? ret-type-pattern fully-qualified-class-name (param-pattern) throws-pattern?) 对比方法的定义来记忆,一个java方法的全部定义方式可以表示成下面的方式:public String springMVCmybatic.com.my.aop.UserServiceImp(String a, int b) throw Exception{}modifier-pattern?:表示方法的修饰符,可有可无;对应的就是 publicret-type-pattern:表示方法的返回值;对应的就是 Stringfully-qualified-class-name 方法所在类的完全限定名称;对应的就是 springMVCmybatic.com.my.aop.UserServiceImpparam-pattern:表示方法的参数;对应的就是 String a, int bthrows-pattern:表示方法抛出的异常,可有可无;对应的就是 throw Exception2、&&,||,!(表达式之间可以采用与,或,非的方式来过滤。)@Around(“pointCutMethod() || testAOP()”) public Object doAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("@Around:进入方法—环绕通知"); Object o = pjp.proceed(); System.out.println("@Around:退出方法—环绕通知"); return o; }八、多个切点的执行顺序上面的例子中,定义了order=3,重新创建一个切面,定义order=6,执行的结果是:@Around:进入方法—环绕通知–order=3@Before:开始添加–order=3@Around:进入方法—环绕通知–order=6@Before:开始添加–order=6============执行业务方法findUser,查找的用户是:张三=============@Around:退出方法—环绕通知–order=6@After:最终通知–order=6@AfterReturning:后置通知–order=6—张三—@Around:退出方法—环绕通知–order=3@After:最终通知–order=3@AfterReturning:后置通知–order=3—张三— @Around:进入方法—环绕通知–order=3@Before:开始添加–order=3@Around:进入方法—环绕通知–order=6@Before:开始添加–order=6============执行业务方法addUser=============@After:最终通知–order=6@AfterThrowing:例外通知–order=6null@After:最终通知–order=3@AfterThrowing:例外通知–order=3null 从结果中可以看出order越小越先执行,执行完了之后就order越小就越后推出。总结为下面的图: ...

December 17, 2018 · 2 min · jiezi

Spring Cloud Config 规范

Spring Cloud Config 规范首先Spring Cloud 是基于 Spring 来扩展的,Spring 本身就提供当创建一个Bean时可从Environment 中将一些属性值通过@Value的形式注入到业务代码中的能力。那Spring Cloud Config 要解决的问题就是:如何将配置加载到 Environment 。配置变更时,如何控制 Bean 是否需要 create,重新触发一次 Bean 的初始化,才能将 @Value 注解指定的字段从 Environment 中重新注入。配置变更时,如何控制新的配置会更新到 Environment 中,才能保证配置变更时可注入最新的值。要解决以上三个问题:Spring Cloud Config 规范中刚好定义了核心的三个接口:PropertySourceLocator:抽象出这个接口,就是让用户可定制化的将一些配置加载到 Environment。这部分的配置获取遵循了 Spring Cloud Config 的理念,即希望能从外部储存介质中来 loacte。RefreshScope: Spring Cloud 定义这个注解,是扩展了 Spring 原有的 Scope 类型。用来标识当前这个 Bean 是一个refresh 类型的 Scope。其主要作用就是可以控制 Bean 的整个生命周期。ContextRefresher:抽象出这个 Class,是让用户自己按需来刷新上下文(比如当有配置刷新时,希望可以刷新上下文,将最新的配置更新到 Environment,重新创建 Bean 时,就可以从 Environment 中注入最新的配置)。Spring Cloud Config 原理Spring Cloud Config 的启动过程1、如何将配置加载到Environment:PropertySourceLocator在整个 Spring Boot 启动的生命周期过程中,有一个阶段是 prepare environment。在这个阶段,会publish 一个 ApplicationEnvironmentPreparedEvent,通知所有对这个事件感兴趣的 Listener,提供对 Environment 做更多的定制化的操作。Spring Cloud 定义了一个BootstrapApplicationListener,在 BootstrapApplicationListener 的处理过程中有一步非常关键的操作如下所示:private ConfigurableApplicationContext bootstrapServiceContext( ConfigurableEnvironment environment, final SpringApplication application, String configName) { //省略 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // Use names and ensure unique to protect against duplicates List<String> names = new ArrayList<>(SpringFactoriesLoader .loadFactoryNames(BootstrapConfiguration.class, classLoader)); //省略 }这是 Spring 的工厂加载机制,可通过在 META-INF/spring.factories 文件中配置一些程序中预定义的一些扩展点。比如 Spring Cloud 这里的实现,可以看到 BootstrapConfiguration 不是一个具体的接口,而是一个注解。通过这种方式配置的扩展点好处是不局限于某一种接口的实现,而是同一类别的实现。可以查看 spring-cloud-context 包中的 spring.factories 文件关于BootstrapConfiguration的配置,有一个比较核心入口的配置就是:org.springframework.cloud.bootstrap.BootstrapConfiguration=\org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration可以发现 PropertySourceBootstrapConfiguration 实现了 ApplicationContextInitializer 接口,其目的就是在应用程序上下文初始化的时候做一些额外的操作。在 Bootstrap 阶段,会通过 Spring Ioc 的整个生命周期来初始化所有通过key为_org.springframework.cloud.bootstrap.BootstrapConfiguration_ 在 spring.factories 中配置的 Bean。Spring Cloud Alibaba Nacos Config 的实现就是通过该key来自定义一些在Bootstrap 阶段需要初始化的一些Bean。在该模块的 spring.factories 配置文件中可以看到如下配置:org.springframework.cloud.bootstrap.BootstrapConfiguration=\org.springframework.cloud.alibaba.nacos.NacosConfigBootstrapConfiguration在 Bootstrap 阶段初始化的过程中,会获取所有 ApplicationContextInitializer 类型的 Bean,并设置回SpringApplication主流程当中。如下 BootstrapApplicationListener 类中的部分代码所示:private void apply(ConfigurableApplicationContext context, SpringApplication application, ConfigurableEnvironment environment) { @SuppressWarnings(“rawtypes”) //这里的 context 是一个 bootstrap 级别的 ApplicationContext,这里已经含有了在 bootstrap阶段所有需要初始化的 Bean。 //因此可以获取 ApplicationContextInitializer.class 类型的所有实例 List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context, ApplicationContextInitializer.class); //设置回 SpringApplication 主流程当中 application.addInitializers(initializers .toArray(new ApplicationContextInitializer[initializers.size()])); //省略…}这样一来,就可以通过在 SpringApplication 的主流程中来回调这些ApplicationContextInitializer 的实例,做一些初始化的操作。如下 SpringApplication 类中的部分代码所示:private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment); postProcessApplicationContext(context); //回调在BootstrapApplicationListener中设置的ApplicationContextInitializer实例 applyInitializers(context); listeners.contextPrepared(context); //省略…}protected void applyInitializers(ConfigurableApplicationContext context) { for (ApplicationContextInitializer initializer : getInitializers()) { Class<?> requiredType = GenericTypeResolver.resolveTypeArgument( initializer.getClass(), ApplicationContextInitializer.class); Assert.isInstanceOf(requiredType, context, “Unable to call initializer.”); initializer.initialize(context); }}在 applyInitializers 方法中,会触发 PropertySourceBootstrapConfiguration 中的 initialize 方法。如下所示:@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) { CompositePropertySource composite = new CompositePropertySource( BOOTSTRAP_PROPERTY_SOURCE_NAME); AnnotationAwareOrderComparator.sort(this.propertySourceLocators); boolean empty = true; ConfigurableEnvironment environment = applicationContext.getEnvironment(); for (PropertySourceLocator locator : this.propertySourceLocators) { PropertySource<?> source = null; //回调所有实现PropertySourceLocator接口实例的locate方法, source = locator.locate(environment); if (source == null) { continue; } composite.addPropertySource(source); empty = false; } if (!empty) { //从当前Enviroment中获取 propertySources MutablePropertySources propertySources = environment.getPropertySources(); //省略… //将composite中的PropertySource添加到当前应用上下文的propertySources中 insertPropertySources(propertySources, composite); //省略… }在这个方法中会回调所有实现 PropertySourceLocator 接口实例的locate方法,locate 方法返回一个 PropertySource 的实例,统一add到CompositePropertySource实例中。如果 composite 中有新加的PropertySource,最后将composite中的PropertySource添加到当前应用上下文的propertySources中。Spring Cloud Alibaba Nacos Config 在 Bootstrap 阶段通过Java配置的方式初始化了一个 NacosPropertySourceLocator 类型的Bean。从而在 locate 方法中将存放在Nacos中的配置信息读取出来,将读取结果存放到 PropertySource 的实例中返回。具体如何从Nacos中读取配置信息可参考 NacosPropertySourceLocator 类的实现。Spring Cloud Config 正是提供了PropertySourceLocator接口,来提供应用外部化配置可动态加载的能力。Spring Ioc 容器在初始化 Bean 的时候,如果发现 Bean 的字段上含有 @Value 的注解,就会从 Enviroment 中的PropertySources 来获取其值,完成属性的注入。Spring Cloud Config 外部化配置可动态刷新感知到外部化配置的变更这部分代码的操作是需要用户来完成的。Spring Cloud Config 只提供了具备外部化配置可动态刷新的能力,并不具备自动感知外部化配置发生变更的能力。比如如果你的配置是基于Mysql来实现的,那么在代码里面肯定要有能力感知到配置发生变化了,然后再显示的调用 ContextRefresher 的 refresh方法,从而完成外部化配置的动态刷新(只会刷新使用RefreshScope注解的Bean)。例如在 Spring Cloud Alibaba Nacos Config 的实现过程中,Nacos 提供了对dataid 变更的Listener 回调。在对每个dataid 注册好了相应的Listener之后,如果Nacos内部通过长轮询的方式感知到数据的变更,就会回调相应的Listener,在 Listener 的实现过程中,就是通过调用 ContextRefresher 的 refresh方法完成配置的动态刷新。具体可参考 NacosContextRefresher 类的实现。Sring Cloud Config的动态配置刷新原理图如下所示:ContextRefresher的refresh的方法主要做了两件事:触发PropertySourceLocator的locator方法,需要加载最新的值,并替换 Environment 中旧值Bean中的引用配置值需要重新注入一遍。重新注入的流程是在Bean初始化时做的操作,那也就是需要将refresh scope中的Bean 缓存失效,当再次从refresh scope中获取这个Bean时,发现取不到,就会重新触发一次Bean的初始化过程。这两个操作所对应的代码如下所示:public synchronized Set refresh() { Map<String, Object> before = extract( this.context.getEnvironment().getPropertySources()); //1、加载最新的值,并替换Envrioment中旧值 addConfigFilesToEnvironment(); Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); this.context.publishEvent(new EnvironmentChangeEvent(context, keys)); //2、将refresh scope中的Bean 缓存失效: 清空 this.scope.refreshAll(); return keys;}addConfigFilesToEnvironment 方法中发生替换的代码如下所示:ConfigurableApplicationContext addConfigFilesToEnvironment() { ConfigurableApplicationContext capture = null; try { //省略… //1、这里会重新触发PropertySourceLoactor的locate的方法,获取最新的外部化配置 capture = (SpringApplicationBuilder)builder.run(); MutablePropertySources target = this.context.getEnvironment() .getPropertySources(); String targetName = null; for (PropertySource<?> source : environment.getPropertySources()) { String name = source.getName(); //省略.. //只有不是标准的 Source 才可替换 if (!this.standardSources.contains(name)) { if (target.contains(name)) { //开始用新的PropertySource替换旧值 target.replace(name, source); } // } } } // return capture;}this.scope.refreshAll() 清空缓存的操作代码如下所示:@Override public void destroy() { List<Throwable> errors = new ArrayList<Throwable>(); //清空Refresh Scope 中的缓存 Collection<BeanLifecycleWrapper> wrappers = this.cache.clear(); //省略… }为了验证每次配置刷新时,Bean 是新创建的,特意写了一个Demo 验证了下,如下所示:Acm Properties: beijing-region//刷新前Object Instance is :com.alibaba.demo.normal.ConfigProperties@1be96342018-11-01 19:16:32.535 INFO 27254 — [gPullingdefault] startup date [Thu Nov 01 19:16:32 CST 2018]; root of context hierarchyAcm Properties: qingdao-region//刷新后Object Instance is :com.alibaba.demo.normal.ConfigProperties@2c6965e0Spring Cloud Config 扩展Scope的核心类:RefreshScope可以看到上面的代码中有 this.scope.refreshAll(),其中的scope就是RefreshScope。是用来存放scope类型为refresh类型的Bean(即使用RefreshScope注解标识的Bean),也就是说当一个Bean既不是singleton也不是prototype时,就会从自定义的Scope中去获取(Spring 允许自定义Scope),然后调用Scope的get方法来获取一个实例,Spring Cloud 正是扩展了Scope,从而控制了整个 Bean 的生命周期。当配置需要动态刷新的时候, 调用this.scope.refreshAll()这个方法,就会将整个RefreshScope的缓存清空,完成配置可动态刷新的可能。更多关于Scope的分析请参考 这里后续关于ContextRefresh 和 RefreshScope的初始化配置是在RefreshAutoConfiguration类中完成的。而RefreshAutoConfiguration类初始化的入口是在spring-cloud-context中的META-INF/spring.factories中配置的。从而完成整个和动态刷新相关的Bean的初始化操作。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 17, 2018 · 3 min · jiezi

Java面试题:面向对象,类加载器,JDBC, Spring 基础概念

为什么说Java是一门平台无关语言?平台无关实际的含义是“一次编写到处运行”。Java 能够做到是因为它的字节码(byte code)可以运行在任何操作系统上,与底层系统无关。2. 为什么 Java 不是100%面向对象?Java 不是100%面向对象,因为它包含8个原始数据类型,例如 boolean、byte、char、int、float、double、long、short。它们不是对象。3. 什么是 singleton class,如何创建一个 singleton class?Singleton class 在任何时间同一个 JVM 中只有一个实例。可以把构造函数加 private 修饰符创建 singleton。4. 什么是多态?多态简单地说“一个接口,多种实现”。多态的出现使得在不同的场合同一个接口能够提供不同功能,具体地说可以让变量、函数或者对象能够提供多种功能。下面是多态的两种类型:编译时多态运行时多态编译时多态主要是对方法进行重载(overload),而运行时多态主要通过使用继承或者实现接口。什么是运行时多态,也称动态方法分配?在 Java 中,运行时多态或动态方法分配是一种在运行过程中的方法重载。在这个过程中,通过调用父类的变量引用被重载的方法。下面是一个例子:Java面试题:面向对象,类加载器,JDBC, Spring 基础概念5. Java类加载器包括几种?它们之间的关系是怎么样的?Java 类加载器有:引导类加载器(bootstrap class loader):只加载 JVM 自身需要的类,包名为 java、javax、sun 等开头。扩展类加载器(extensions class loader):加载 JAVA_HOME/lib/ext 目录下或者由系统变量 -Djava.ext.dir 指定位路径中的类库。应用程序类加载器(application class loader):加载系统类路径 java -classpath 或 -Djava.class.path 下的类库。自定义类加载器(java.lang.classloder):继承 java.lang.ClassLoader 的自定义类加载器。注意:-Djava.ext.dirs 会覆盖 Java 本身的 ext 设置,造成 JDK 内建功能无法使用。可以像下面这样指定参数:Java面试题:面向对象,类加载器,JDBC, Spring 基础概念它们的关系如下:启动类加载器,C++实现,没有父类。扩展类加载器(ExtClassLoader),Java 实现,父类加载器为 null。应用程序类加载器(AppClassLoader),Java 实现,父类加载器为 ExtClassLoader 。自定义类加载器,父类加载器为AppClassLoader。Java学习交流圈:834962734 ,进群可免费获取一份Java架构进阶技术精品视频。(高并发+Spring源码+JVM原理解析+分布式架构+微服务架构+多线程并发原理+BATJ面试宝典)6. 什么是JDBC驱动?JDBC Driver 是一种实现 Java 应用与数据库交互的软件。JDBC 驱动有下面4种:JDBC-ODBC bridge 驱动Native-API 驱动(部分是 Java 驱动)网络协议驱动(全部是 Java 驱动)Thin driver(全部是 Java 驱动)7. 使用 Java 连接数据库有哪几步?注册驱动类新建数据库连接新建语句(statement)查询关闭连接8. 列举Spring配置中常用的重要注解。下面是一些重要的注解:@Required@Autowired@Qualifier@Resource@PostConstruct@PreDestroy9. Spring中的Bean是什么?列举Spring Bean的不同作用域。Bean 是 Spring 应用的骨架。它们由 Spring IoC 容器管理。换句话说,Bean 是一个由 Spring IoC 容器初始化、装配和管理的对象。下面是 Spring Bean 的5种作用域:Singleton:每个容器只创建一个实例,也是 Spring Bean 的默认配置。由于非线程安全,因此确保使用时不要在 Bean 中共享实例变量,一面出现数据不一致。Prototype:每次请求时创建一个新实例。Request:与 prototype 相同,区别在于只针对 Web 应用。每次 HTTP 请求时创建一个新实例。Session:每次收到 HTTP 会话请求时由容器创建一个新实例。全局 Session:为每个门户应用(Portlet App)创建一个全局 Session Bean。

December 17, 2018 · 1 min · jiezi

Java BeanUtils对象复制工具类及方法

BeanUtils.copyProperties(Object source, Object target)用法: 讲source的属性值复制到target,属性为null时也会进行复制。需求:排除null值进行复制public class CopyObjectUtil { public static String[] getNullPropertyNames(Object source) { final BeanWrapper src = new BeanWrapperImpl(source); java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors(); Set<String> emptyNames = new HashSet<String>(); for (java.beans.PropertyDescriptor pd : pds) { Object srcValue = src.getPropertyValue(pd.getName()); if (srcValue == null) emptyNames.add(pd.getName()); } String[] result = new String[emptyNames.size()]; return emptyNames.toArray(result); } public static void copyPropertiesIgnoreNull(Object src, Object target) { BeanUtils.copyProperties(src, target, getNullPropertyNames(src)); }}使用方式与BeanUtils.copyProperties相同:CopyObjectUtil.copyPropertiesIgnoreNull(Object source, Object target);2. BeanUtils.copyProperties(Object source, Object target, new String[] { “id”, “createDate”, “modifyDate” })用法:排除指定字段进行复制

December 14, 2018 · 1 min · jiezi

Spring Cloud Stream消费失败后的处理策略(一):自动重试

之前写了几篇关于Spring Cloud Stream使用中的常见问题,比如:如何处理消息重复消费如何消费自己生产的消息下面几天就集中来详细聊聊,当消息消费失败之后该如何处理的几种方式。不过不论哪种方式,都需要与具体业务结合,解决不同业务场景可能出现的问题。今天第一节,介绍一下Spring Cloud Stream中默认就已经配置了的一个异常解决方案:重试!应用场景依然要明确一点,任何解决方案都要结合具体的业务实现来确定,不要有了锤子看什么问题都是钉子。那么重试可以解决什么问题呢?由于重试的基础逻辑并不会改变,所以通常重试只能解决因环境不稳定等外在因素导致的失败情况,比如:当我们接收到某个消息之后,需要调用一个外部的Web Service做一些事情,这个时候如果与外部系统的网络出现了抖动,导致调用失败而抛出异常。这个时候,通过重试消息消费的具体逻辑,可能在下一次调用的时候,就能完成整合业务动作,从而解决刚才所述的问题。动手试试先通过一个小例子来看看Spring Cloud Stream默认的重试机制是如何运作的。之前在如何消费自己生产的消息一文中的例子,我们可以继续沿用,或者也可以精简一些,都写到一个主类中,比如下面这样:@EnableBinding(TestApplication.TestTopic.class)@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @RestController static class TestController { @Autowired private TestTopic testTopic; /** * 消息生产接口 * @param message * @return / @GetMapping("/sendMessage") public String messageWithMQ(@RequestParam String message) { testTopic.output().send(MessageBuilder.withPayload(message).build()); return “ok”; } } /* * 消息消费逻辑 */ @Slf4j @Component static class TestListener { @StreamListener(TestTopic.INPUT) public void receive(String payload) { log.info(“Received: " + payload); throw new RuntimeException(“Message consumer failed!”); } } interface TestTopic { String OUTPUT = “example-topic-output”; String INPUT = “example-topic-input”; @Output(OUTPUT) MessageChannel output(); @Input(INPUT) SubscribableChannel input(); }}内容很简单,既包含了消息的生产,也包含了消息消费。与之前例子不同的就是在消息消费逻辑中,主动的抛出了一个异常来模拟消息的消费失败。在启动应用之前,还要记得配置一下输入输出通道对应的物理目标(exchange或topic名),比如:spring.cloud.stream.bindings.example-topic-input.destination=test-topicspring.cloud.stream.bindings.example-topic-output.destination=test-topic完成了上面配置之后,就可以启动应用,并尝试访问localhost:8080/sendMessage?message=hello接口来发送一个消息到MQ中了。此时可以看到类似下面的日志:2018-12-10 11:20:21.345 INFO 30499 — [w2p2yKethOsqg-1] c.d.stream.TestApplication$TestListener : Received: hello2018-12-10 11:20:22.350 INFO 30499 — [w2p2yKethOsqg-1] c.d.stream.TestApplication$TestListener : Received: hello2018-12-10 11:20:24.354 INFO 30499 — [w2p2yKethOsqg-1] c.d.stream.TestApplication$TestListener : Received: hello2018-12-10 11:20:54.651 ERROR 30499 — [w2p2yKethOsqg-1] o.s.integration.handler.LoggingHandler : org.springframework.messaging.MessagingException: Exception thrown while invoking com.didispace.stream.TestApplication$TestListener#receive[1 args]; nested exception is java.lang.RuntimeException: Message consumer failed!, failedMessage=GenericMessage [payload=byte[5], headers={amqp_receivedDeliveryMode=PERSISTENT, amqp_receivedRoutingKey=test-topic, amqp_receivedExchange=test-topic, amqp_deliveryTag=2, deliveryAttempt=3, amqp_consumerQueue=test-topic.anonymous.EuqBJu66Qw2p2yKethOsqg, amqp_redelivered=false, id=a89adf96-7de2-f29d-20b6-2fcb0c64cd8c, amqp_consumerTag=amq.ctag-XFy6vXU2w4RB_NRBzImWTA, contentType=application/json, timestamp=1544412051638}] at org.springframework.cloud.stream.binding.StreamListenerMessageHandler.handleRequestMessage(StreamListenerMessageHandler.java:63) at org.springframework.integration.handler.AbstractReplyProducingMessageHandler.handleMessageInternal(AbstractReplyProducingMessageHandler.java:109) at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:158) at org.springframework.integration.dispatcher.AbstractDispatcher.tryOptimizedDispatch(AbstractDispatcher.java:116) at org.springframework.integration.dispatcher.UnicastingDispatcher.doDispatch(UnicastingDispatcher.java:132) at org.springframework.integration.dispatcher.UnicastingDispatcher.dispatch(UnicastingDispatcher.java:105) at org.springframework.integration.channel.AbstractSubscribableChannel.doSend(AbstractSubscribableChannel.java:73) at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:445) at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:394) at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:181) at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:160) at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:47) at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:108) at org.springframework.integration.endpoint.MessageProducerSupport.sendMessage(MessageProducerSupport.java:203) at org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter.access$1100(AmqpInboundChannelAdapter.java:60) at org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter$Listener.lambda$onMessage$0(AmqpInboundChannelAdapter.java:214) at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:287) at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:180) at org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter$Listener.onMessage(AmqpInboundChannelAdapter.java:211) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1414) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.actualInvokeListener(AbstractMessageListenerContainer.java:1337) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:1324) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:1303) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:817) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:801) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$700(SimpleMessageListenerContainer.java:77) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1042) at java.lang.Thread.run(Thread.java:748)Caused by: java.lang.RuntimeException: Message consumer failed! at com.didispace.stream.TestApplication$TestListener.receive(TestApplication.java:65) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:181) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:114) at org.springframework.cloud.stream.binding.StreamListenerMessageHandler.handleRequestMessage(StreamListenerMessageHandler.java:55) … 27 more从日志中可以看到,一共输出了三次Received: hello,也就是说消息消费逻辑执行了3次,然后抛出了最终执行失败的异常。设置重复次数默认情况下Spring Cloud Stream会重试3次,我们也可以通过配置的方式修改这个默认配置,比如下面的配置可以将重试次数调整为1次:spring.cloud.stream.bindings.example-topic-input.consumer.max-attempts=1对于一些纯内部计算逻辑,不需要依赖外部环境,如果出错通常是代码逻辑错误的情况下,不论我们如何重试都会继续错误的业务逻辑可以将该参数设置为0,避免不必要的重试影响消息处理的速度。深入思考完成了上面的基础尝试之后,再思考下面两个问题:问题一:如果在重试过程中消息处理成功了,还会有异常信息吗?答案是不会。因为重试过程是消息处理的一个整体,如果某一次重试成功了,会任务对所收到消息的消费成功了。这个问题可以在上述例子中做一些小改动来验证,比如:@Slf4j@Componentstatic class TestListener { int counter = 1; @StreamListener(TestTopic.INPUT) public void receive(String payload) { log.info(“Received: " + payload + “, " + counter); if (counter == 3) { counter = 1; return; } else { counter++; throw new RuntimeException(“Message consumer failed!”); } }}通过加入一个计数器,当重试是第3次的时候,不抛出异常来模拟消费逻辑处理成功了。此时重新运行程序,并调用接口localhost:8080/sendMessage?message=hello,可以获得如下日志结果,并没有异常打印出来。2018-12-10 16:07:38.390 INFO 66468 — [L6MGAj-MAj7QA-1] c.d.stream.TestApplication$TestListener : Received: hello, 12018-12-10 16:07:39.398 INFO 66468 — [L6MGAj-MAj7QA-1] c.d.stream.TestApplication$TestListener : Received: hello, 22018-12-10 16:07:41.402 INFO 66468 — [L6MGAj-MAj7QA-1] c.d.stream.TestApplication$TestListener : Received: hello, 3也就是,虽然前两次消费抛出了异常,但是并不影响最终的结果,也不会打印中间过程的异常,避免了对日志告警产生误报等问题。问题二:如果重试都失败之后应该怎么办呢?如果消息在重试了还是失败之后,目前的配置唯一能做的就是将异常信息记录下来,进行告警。由于日志中有消息的消息信息描述,所以应用维护者可以根据这些信息来做一些补救措施。当然,这样的做法显然不是最好的,因为太过麻烦。那么怎么做才好呢?且听下回分解!代码示例本文示例读者可以通过查看下面仓库的中的stream-exception-handler-1项目:GithubGitee如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!以下专题教程也许您会有兴趣Spring Boot基础教程Spring Cloud基础教程 ...

December 13, 2018 · 2 min · jiezi

webpack 入门与解析

每次学新东西总感觉自己是不是变笨了,看了几个博客,试着试着就跑不下去,无奈只有去看官方文档。 webpack是基于node的。先安装最新的node。1.初始化安装node后,新建一个目录,比如html5。cmd中切到当前文件夹。npm init -y这个命令会创建一个默认的package.json。它包含了项目的一些配置参数,通过它可以进行初始安装。详细参数:https://docs.npmjs.com/files/package.json。不要y参数的话,会在命令框中设置各项参数,但觉得没啥必要。2.安装webpacknpm install webpack --save-dev将webpack安装到当前目录。虽然npm install webpack -g 可以讲webpack安装到全局,但是容易出现一些模块找不到的错误,所以最好还是安装到当前目录下。3.目录结构webpack是一款模块加载各种资源并打包的工具。所以先建一个如下的目录结构:app包含的开发中的js文件,一个组件,一个入口。build中就是用来存放打包之后的文件的。webpack.config.js 顾名思义用来配置webpack的。package.json就不用说了。component.jsexport default function () {``var element = document.createElement(``'h1'``);``element.innerHTML = 'Hello world'``;``return element;``}component.js 是输出一个内容为h1元素。export default 是ES6语法,表示指定默认输出。import的时候不用带大括号。index.jsimport component from './component'``;``document.body.appendChild(component());index.js 的作用就是引用Component模块,并在页面上输出一个h1元素。但完成这个还需要一个插件,因为目前我们还没有index.html文件。npm install html-webpack-plugin --save-devhtml-webpack-plugin的用来生成html,将其也安装到开发目录下面。4.设置 webpack 配置文件我们需要通过webpack.config.js文件告诉webpack如何开始。配置文件至少需要一个入口和一个输出。多个页面就需要多个入口。node的path模块const path = require(``'path'``);``const HtmlWebpackPlugin = require(``'html-webpack-plugin'``);``const PATHS = {``app: path.join(__dirname, 'app'``),``build: path.join(__dirname, 'build'``),``};``module.exports = {``entry: {``app: PATHS.app,``},``output: {``path: PATHS.build,``filename: '[name].js'``,``},``plugins: [``new HtmlWebpackPlugin({``title: 'Webpack demo'``,``}),``],``};第一次看到这个配置文件是有点懵,主要是exports,分三个部分,一个入口,一个输出,一个插件。入口指向了app文件夹。默认会把包含"index.js"的文件作为入口。输出指定了build地址和一个文件名;[name]这儿表示占位符,可以看成webpack提供的一个变量。这个具体后面再看。而HtmlWebpackPlugin会生成一个默认的html文件。5.打包有了以上准备,直接输入 webpack 就能运行了。这个输出包含了Hash(每次打包值都不同),Version,Time(耗时)。以及输出的文件信息。这时打开build文件夹,发现多了一个app.js和index.html文件,双击index.html:也可以修改下package.json?{``"name"``: "Html5"``,``"version"``: "1.0.0"``,``"description"``: ""``,``"main"``: "index.js"``,``"scripts"``: {``"build"``: "webpack"``},``"keywords"``: [],``"author"``: ""``,``"license"``: "ISC"``,``"devDependencies"``: {``"html-webpack-plugin"``: "^2.28.0"``,``"webpack"``: "^2.2.1"``}``}指定build。在cmd中执行npm run build 得到同样的结果出现helloword。再看下文件内容index.html:&lt;!DOCTYPE html&gt;``&lt;``html``&gt;``&lt;``head``&gt;``&lt;``meta charset``=``"UTF-8"``&gt;``&lt;``title``&gt;Webpack demo&lt;/``title``&gt;``&lt;/``head``&gt;``&lt;``body``&gt;``&lt;``script type``=``"text/javascript" src``=``"app.js"``&gt;&lt;/``script``&gt;&lt;/``body``&gt;``&lt;/``html``&gt;默认引用了app.js。6、解析app.js/******/ (``function``(modules) { // webpackBootstrap``/******/ // The module cache``/******/ var installedModules = {};``/******/ // The require function``/******/ function __webpack_require__(moduleId) {``/*****/ // Check if module is in cache``/******/ if``(installedModules[moduleId])``/******/ return installedModules[moduleId].exports;``/******/ // Create a new module (and put it into the cache)``/******/ var module = installedModules[moduleId] = {``/******/ i: moduleId,``/******/ l: false``,``/******/ exports: {}``/******/ };``/******/ // Execute the module function``/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);``/******/ // Flag the module as loaded``/******/ module.l = true``;``/******/ // Return the exports of the module``/******/ return module.exports;``/******/ }``/******/ // expose the modules object (__webpack_modules__)``/******/ __webpack_require__.m = modules;``/******/ // expose the module cache``/******/ __webpack_require__.c = installedModules;``/******/ // identity function for calling harmony imports with the correct context``/******/ __webpack_require__.i = function``(value) { return value; };``/******/ // define getter function for harmony exports``/******/ __webpack_require__.d = function``(exports, name, getter) {``/******/ if``(!__webpack_require__.o(exports, name)) {``/******/ Object.defineProperty(exports, name, {``/******/ configurable: false``,``/******/ enumerable: true``,``/******/ get: getter``/******/ });``/******/ }``/******/ };``/******/ // getDefaultExport function for compatibility with non-harmony modules``/******/ __webpack_require__.n = function``(module) {``/******/ var getter = module &amp;&amp; module.__esModule ?``/******/ function getDefault() { return module[``'default'``]; } :``/******/ function getModuleExports() { return module; };``/******/ __webpack_require__.d(getter, 'a'``, getter);``/******/ return getter;``/******/ };``/******/ // Object.prototype.hasOwnProperty.call``/******/ __webpack_require__.o = function``(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };``/******/ // __webpack_public_path__``/******/ __webpack_require__.p = ""``;``/******/ // Load entry module and return exports``/******/ return __webpack_require__(__webpack_require__.s = 1);``/******/ })``/************************************************************************/``/******/ ([``/* 0 */``/***/ (``function``(module, __webpack_exports__, __webpack_require__) {``"use strict"``;``/* harmony default export */ __webpack_exports__[``"a"``] = function () {``var element = document.createElement(``'h1'``);``element.innerHTML = 'Hello world'``;``return element;};/***/` `}),/* 1 //***/` `(function(module, __webpack_exports__, __webpack_require__) {“use strict”;Object.defineProperty(webpack_exports, "__esModule", { value:` `true` `});/ harmony import / var WEBPACK_IMPORTED_MODULE_0__component = webpack_require(0);document.body.appendChild(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__component__[“a” / default /])());// })/******/` `]);`而app.js内容比较多了。整体是一个匿名函数。`(function(module) {})([(function` `(){}),` `function() {}])app文件夹中的两个js文件成了这儿的两个模块。函数最开始是从__webpack_require__开始return webpack_require(webpack_require.s = 1);这里指定从模块1执行(赋值语句的返回值为其值)。而模块1的调用是通过__webpack_require__的这句执行的。&lt;u&gt;复制代码&lt;/u&gt; 代码如下:modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);通过call调用模块的主要作用是为了把参数传过去。(function(module, webpack_exports, webpack_require) {"use strict";Object.defineProperty(__webpack_exports__,` `"__esModule", { value: true });/* harmony import */` `var` `__WEBPACK_IMPORTED_MODULE_0__component__ = __webpack_require__(0);document.body.appendChild(webpack_require.i(WEBPACK_IMPORTED_MODULE_0__component["a"` `/* default */])());/***/` `})`webpack_require 每加载一个模块都会先去模块缓存中找,没有就新建一个module对象:`var` `module = installedModules[moduleId] = {i: moduleId,l:` `false,exports: {}};模块1中加载了模块0,var WEBPACK_IMPORTED_MODULE_0__component = webpack_require(0);WEBPACK_IMPORTED_MODULE_0__component 返回的是这个模块0的exports部分。而之前Component.js的默认方法定义成了webpack_exports["a"] = function () {var` `element = document.createElement(‘h1’);element.innerHTML = ‘Hello world’;return element;}`所以再模块1的定义通过"a“来获取这个方法:&lt;u&gt;复制代码&lt;/u&gt; 代码如下:document.body.appendChild(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__component__["a" /* default */])());这样就完整了,但这里使用了__webpack_require__.i 将原值返回。`/******/`&nbsp; `// identity function for calling harmony imports with the correct context/****/&nbsp; webpack_require.i = function``(value) { return value; };`不太明白这个i函数有什么作用。这个注释也不太明白,路过的大神希望可以指点下。小结:webpack通过一个立即执行的匿名函数将各个开发模块作为参数初始化,每个js文件(module)对应一个编号,每个js中export的方法或者对象有各自指定的关键字。通过这种方式将所有的模块和接口方法管理起来。然后先加载最后的一个模块(应该是引用别的模块的模块),这样进而去触发别的模块的加载,使整个js运行起来。到这基本了解了webpack的功能和部分原理,但略显复杂,且没有感受到有多大的好处。继续探索。 ...

December 9, 2018 · 2 min · jiezi

那些让程序员崩溃又想笑的程序命名...

本文旨在用最通俗的语言讲述最枯燥的基本知识===================1===================到一家创业公司上班的第一天,老员工刘XX给我看了公司他负责的项目,奇怪的是,命名是“LiuQXProject”,刘XX看着惊愕的我说:“怎么了?有什么错吗?”===================2===================给同事做双十一活动相关代码的review,学到到了很多中英混血单词,获取双十一拼团活动数据的接口叫做“get_ShuangShiYi_GroupTuan_activity_data”,特等奖的命名:TeDeng_price….更气人的是,我们活动奖等有十级,他就虔诚地继续OneDeng_price、TwoDeng_price直到JiuDeng_price。。。噢,no!!好气啊!!而且他还把”奖“的单词prize写成了price,怎么说呢?好难受..===================3===================公司来了个刚毕业的小伙子,自诩前端未来之星,喜欢研读源码,对开源充满热爱,一个月后,无意间打开他写的一个js文件,让我惊讶的是:变量从a到z全部用完,更气人的是,26个字母用完之后,他竟然丧心病狂的用起来了双拼,var aa=1,var ab=“12”,var ac=null…我问他为什么这样命名,他说你没研读jQuery源码吗?人家就是这样做的,简洁大气上档次!===================4===================因为微信昵称经常有带有一些乱七八糟的表情或者字符,在正常情况下utf-8编码的数据库是存不进去的,因此让同事帮忙写个把微信昵称转换成正常的字符串的一个工具函数,最终我拿到了这个工具函数,名字叫做:convertingWechatNicknameintoNormalCharacters(String nickName)===================5===================实习小伙子来的头一天就搞的满身大汗,我说怎么了,他说我明明写了main方法,为什么运行不了,我一看代码,我噻main写成了mian,怎么可能跑得起来啊!更残暴的是:苹果手机是apple_sj,Android手机是android_sj,哈哈以上的种种让人哭笑不得的命名问题..相信很多小伙伴也会碰过这样,有些是因为经验不足,有些是因为一直没有对自己写的代码做一些规范化的工作,有的是因为被老项目、前辈带出来的坏习惯…这些都是编程世界里非常不好的行为,拒不完全统计:在一个项目中,程序员80%的时间都是在和变量、函数、方法打交道,因此一个好的命名习惯,比注释或一份详细的开发文档都重要。针对于此,小编特意根据行业标准—阿里开发文档,做了一些参考和摘抄,整理出一份关于命名方面的规范,给需要的你作参考。争取多写漂亮代码,少写注释!!!文章提纲:整体规范包规范类规范方法规范OOP的一些强制规范1. 整体规范所有的命名必须以英文意译,不能以中文拼音意译,如:获取我的消息接口,可以写:myMessage;但不能写:myXiaoXi尽量用精简的英文命名,但要完整表达其意义,杜绝int a ,int a1 int aa这种毫无意义的简化写法。所有命名不能以特殊符号开始,如:_age,_username常量用全大写定义,单词之间用下划线分割语义,如:public final int REDIS_MAX_IDLE=5;2. 包规范包名全小写,不能用特殊符号或者驼峰写法如:com.courseLog.uitl_con是不合规范的。包名要符合包的作用,比如数据层要写dao,工具包要写util等3. 类规范类名风格为大写开头的驼峰命名方式,如:ApiController、TestController等异常类命名使用Exception结尾,如:CustomerException抽象类命名使用Abstract开头,如:AbstractCustomer测试类命名以它要测试的类的名称开始,以 Test 结尾,如:CustomerControllerTest枚举类命名要以Enum结尾,如果CustomerRoleEnum其它类型的类命名,在描述类作用的同时,也尽可能表达出类所用的一些设计模式4. 方法规范方法名使用驼峰写法,以小写字母开头,如:getUserCourse();方法内的参数名、成员变量、局部变量均使用驼峰写法,以小写字母开头,如:int userName;接口类的方法和属性不要加上任何修饰符,保证代码的简介。方法定义必须要有注释,包括(方法作用、参数名、返回类型、创建时间等)Service/DAO层方法命名规约:1) 获取单个对象的方法用get做前缀。2) 获取多个对象的方法用list做前缀。 3) 获取统计值的方法用count做前缀。 4) 插入的方法用save/insert做前缀。 5) 删除的方法用remove/delete做前缀。 6) 修改的方法用update做前缀。5. OOP的一些强制规范尽量避免使用可变参数编程,相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object接口过时必须加@Deprecated 注解不能使用过时的类或方法所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能

December 6, 2018 · 1 min · jiezi

Hystrix都停更了,我为什么还要学?

最近小主看到很多公众号都在发布Hystrix停更的文章,spring cloud体系的使用者和拥护者一片哀嚎,实际上,spring作为Java最大的家族,根本不需要担心其中一两个零件的废弃,Hystrix的停更,只会催生更多或者更好的零件来替代它,因此,我们需要做的是:知道Hystrix是干嘛,怎么用的,这样要找替代者就易于反掌了。文章提纲:为什么需要Hystrix?Hystrix如何解决依赖隔离如何使用Hystrix1. 为什么需要Hystrix?在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),如下图:在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接缓慢,资源繁忙,暂时不可用,服务脱机等.如下图:QPS为50的依赖 I 出现不可用,但是其他依赖仍然可用.当依赖I 阻塞时,大多数服务器的线程池就出现阻塞(BLOCK),影响整个线上服务的稳定性.如下图:在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败。高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险。例如:一个依赖30个SOA服务的系统,每个服务99.99%可用。 99.99%的30次方 ≈ 99.7% 0.3% 意味着一亿次请求 会有 3,000,00次失败 换算成时间大约每月有2个小时服务不稳定. 随着服务依赖数量的变多,服务不稳定的概率会成指数性提高. 解决问题方案:对依赖做隔离,Hystrix就是处理依赖隔离的框架,同时也是可以帮我们做依赖服务的治理和监控.Netflix 公司开发并成功使用Hystrix,使用规模如下:he Netflix API processes 10+ billion HystrixCommand executions per day using thread isolation. Each API instance has 40+ thread-pools with 5-20 threads in each (most are set to 10). 2. Hystrix如何解决依赖隔离Hystrix使用命令模式HystrixCommand(Command)包装依赖调用逻辑,每个命令在单独线程中/信号授权下执行。可配置依赖调用超时时间,超时时间一般设为比99.5%平均时间略高即可.当调用超时时,直接返回或执行fallback逻辑。为每个依赖提供一个小的线程池(或信号),如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。依赖调用结果分:成功,失败(抛出异常),超时,线程拒绝,短路。 请求失败(异常,拒绝,超时,短路)时执行fallback(降级)逻辑。提供熔断器组件,可以自动运行或手动调用,停止当前依赖一段时间(10秒),熔断器默认错误率阈值为50%,超过将自动运行。提供近实时依赖的统计和监控Hystrix依赖的隔离架构,如下图:3. 如何使用Hystrix使用maven引入Hystrix依赖<!– 依赖版本 –> <hystrix.version>1.3.16</hystrix.version> <hystrix-metrics-event-stream.version>1.1.2</hystrix-metrics-event-stream.version> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>${hystrix.version}</version> </dependency> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-metrics-event-stream</artifactId> <version>${hystrix-metrics-event-stream.version}</version> </dependency> <!– 仓库地址 –> <repository> <id>nexus</id> <name>local private nexus</name> <url>http://maven.oschina.net/content/groups/public/</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> 使用命令模式封装依赖逻辑public class HelloWorldCommand extends HystrixCommand<String> { private final String name; public HelloWorldCommand(String name) { //最少配置:指定命令组名(CommandGroup) super(HystrixCommandGroupKey.Factory.asKey(“ExampleGroup”)); this.name = name; } @Override protected String run() { // 依赖逻辑封装在run()方法中 return “Hello " + name +” thread:" + Thread.currentThread().getName(); } //调用实例 public static void main(String[] args) throws Exception{ //每个Command对象只能调用一次,不可以重复调用, //重复调用对应异常信息:This instance can only be executed once. Please instantiate a new instance. HelloWorldCommand helloWorldCommand = new HelloWorldCommand(“Synchronous-hystrix”); //使用execute()同步调用代码,效果等同于:helloWorldCommand.queue().get(); String result = helloWorldCommand.execute(); System.out.println(“result=” + result); helloWorldCommand = new HelloWorldCommand(“Asynchronous-hystrix”); //异步调用,可自由控制获取结果时机, Future<String> future = helloWorldCommand.queue(); //get操作不能超过command定义的超时时间,默认:1秒 result = future.get(100, TimeUnit.MILLISECONDS); System.out.println(“result=” + result); System.out.println(“mainThread=” + Thread.currentThread().getName()); } } //运行结果: run()方法在不同的线程下执行 // result=Hello Synchronous-hystrix thread:hystrix-HelloWorldGroup-1 // result=Hello Asynchronous-hystrix thread:hystrix-HelloWorldGroup-2 // mainThread=main note:异步调用使用 command.queue()get(timeout, TimeUnit.MILLISECONDS);同步调用使用command.execute() 等同于 command.queue().get();注册异步事件回调执行//注册观察者事件拦截 Observable<String> fs = new HelloWorldCommand(“World”).observe(); //注册结果回调事件 fs.subscribe(new Action1<String>() { @Override public void call(String result) { //执行结果处理,result 为HelloWorldCommand返回的结果 //用户对结果做二次处理. } }); //注册完整执行生命周期事件 fs.subscribe(new Observer<String>() { @Override public void onCompleted() { // onNext/onError完成之后最后回调 System.out.println(“execute onCompleted”); } @Override public void onError(Throwable e) { // 当产生异常时回调 System.out.println(“onError " + e.getMessage()); e.printStackTrace(); } @Override public void onNext(String v) { // 获取结果后回调 System.out.println(“onNext: " + v); } }); / 运行结果 call execute result=Hello observe-hystrix thread:hystrix-HelloWorldGroup-3 onNext: Hello observe-hystrix thread:hystrix-HelloWorldGroup-3 execute onCompleted / 使用Fallback() 提供降级策略//重载HystrixCommand 的getFallback方法实现逻辑 public class HelloWorldCommand extends HystrixCommand<String> { private final String name; public HelloWorldCommand(String name) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“HelloWorldGroup”)) / 配置依赖超时时间,500毫秒/ .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionIsolationThreadTimeoutInMilliseconds(500))); this.name = name; } @Override protected String getFallback() { return “exeucute Falled”; } @Override protected String run() throws Exception { //sleep 1 秒,调用会超时 TimeUnit.MILLISECONDS.sleep(1000); return “Hello " + name +” thread:” + Thread.currentThread().getName(); } public static void main(String[] args) throws Exception{ HelloWorldCommand command = new HelloWorldCommand(“test-Fallback”); String result = command.execute(); } } /* 运行结果:getFallback() 调用运行 getFallback executed / NOTE: 除了HystrixBadRequestException异常之外,所有从run()方法抛出的异常都算作失败,并触发降级getFallback()和断路器逻辑。 HystrixBadRequestException用在非法参数或非系统故障异常等不应触发回退逻辑的场景。依赖命名:CommandKeypublic HelloWorldCommand(String name) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“ExampleGroup”)) / HystrixCommandKey工厂定义依赖名称 / .andCommandKey(HystrixCommandKey.Factory.asKey(“HelloWorld”))); this.name = name; } NOTE: 每个CommandKey代表一个依赖抽象,相同的依赖要使用相同的CommandKey名称。依赖隔离的根本就是对相同CommandKey的依赖做隔离.依赖分组:CommandGroup命令分组用于对依赖操作分组,便于统计,汇总等.//使用HystrixCommandGroupKey工厂定义 public HelloWorldCommand(String name) { Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“HelloWorldGroup”)) } NOTE: CommandGroup是每个命令最少配置的必选参数,在不指定ThreadPoolKey的情况下,字面值用于对不同依赖的线程池/信号区分.线程池/信号:ThreadPoolKeypublic HelloWorldCommand(String name) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“ExampleGroup”)) .andCommandKey(HystrixCommandKey.Factory.asKey(“HelloWorld”)) / 使用HystrixThreadPoolKey工厂定义线程池名称*/ .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(“HelloWorldPool”))); this.name = name; } NOTE: 当对同一业务依赖做隔离时使用CommandGroup做区分,但是对同一依赖的不同远程调用如(一个是redis 一个是http),可以使用HystrixThreadPoolKey做隔离区分. 最然在业务上都是相同的组,但是需要在资源上做隔离时,可以使用HystrixThreadPoolKey区分.请求缓存 Request-Cachepublic class RequestCacheCommand extends HystrixCommand<String> { private final int id; public RequestCacheCommand( int id) { super(HystrixCommandGroupKey.Factory.asKey(“RequestCacheCommand”)); this.id = id; } @Override protected String run() throws Exception { System.out.println(Thread.currentThread().getName() + " execute id=” + id); return “executed=” + id; } //重写getCacheKey方法,实现区分不同请求的逻辑 @Override protected String getCacheKey() { return String.valueOf(id); } public static void main(String[] args){ HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { RequestCacheCommand command2a = new RequestCacheCommand(2); RequestCacheCommand command2b = new RequestCacheCommand(2); Assert.assertTrue(command2a.execute()); //isResponseFromCache判定是否是在缓存中获取结果 Assert.assertFalse(command2a.isResponseFromCache()); Assert.assertTrue(command2b.execute()); Assert.assertTrue(command2b.isResponseFromCache()); } finally { context.shutdown(); } context = HystrixRequestContext.initializeContext(); try { RequestCacheCommand command3b = new RequestCacheCommand(2); Assert.assertTrue(command3b.execute()); Assert.assertFalse(command3b.isResponseFromCache()); } finally { context.shutdown(); } } } NOTE:请求缓存可以让(CommandKey/CommandGroup)相同的情况下,直接共享结果,降低依赖调用次数,在高并发和CacheKey碰撞率高场景下可以提升性能.Servlet容器中,可以直接实用Filter机制Hystrix请求上下文public class HystrixRequestContextServletFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { chain.doFilter(request, response); } finally { context.shutdown(); } } } <filter> <display-name>HystrixRequestContextServletFilter</display-name> <filter-name>HystrixRequestContextServletFilter</filter-name> <filter-class>com.netflix.hystrix.contrib.requestservlet.HystrixRequestContextServletFilter</filter-class> </filter> <filter-mapping> <filter-name>HystrixRequestContextServletFilter</filter-name> <url-pattern>/</url-pattern> </filter-mapping> 信号量隔离:SEMAPHORE隔离本地代码或可快速返回远程调用(如memcached,redis)可以直接使用信号量隔离,降低线程隔离开销.public class HelloWorldCommand extends HystrixCommand<String> { private final String name; public HelloWorldCommand(String name) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“HelloWorldGroup”)) / 配置信号量隔离方式,默认采用线程池隔离 / .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE))); this.name = name; } @Override protected String run() throws Exception { return “HystrixThread:” + Thread.currentThread().getName(); } public static void main(String[] args) throws Exception{ HelloWorldCommand command = new HelloWorldCommand(“semaphore”); String result = command.execute(); System.out.println(result); System.out.println(“MainThread:” + Thread.currentThread().getName()); } } /* 运行结果 HystrixThread:main MainThread:main */ fallback降级逻辑命令嵌套用场景:用于fallback逻辑涉及网络访问的情况,如缓存访问。public class CommandWithFallbackViaNetwork extends HystrixCommand<String> { private final int id; protected CommandWithFallbackViaNetwork(int id) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“RemoteServiceX”)) .andCommandKey(HystrixCommandKey.Factory.asKey(“GetValueCommand”))); this.id = id; } @Override protected String run() { // RemoteService.getValue(id); throw new RuntimeException(“force failure for example”); } @Override protected String getFallback() { return new FallbackViaNetwork(id).execute(); } private static class FallbackViaNetwork extends HystrixCommand<String> { private final int id; public FallbackViaNetwork(int id) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“RemoteServiceX”)) .andCommandKey(HystrixCommandKey.Factory.asKey(“GetValueFallbackCommand”)) // 使用不同的线程池做隔离,防止上层线程池跑满,影响降级逻辑. .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(“RemoteServiceXFallback”))); this.id = id; } @Override protected String run() { MemCacheClient.getValue(id); } @Override protected String getFallback() { return null; } } } NOTE:依赖调用和降级调用使用不同的线程池做隔离,防止上层线程池跑满,影响二级降级逻辑调用.显示调用fallback逻辑,用于特殊业务处理public class CommandFacadeWithPrimarySecondary extends HystrixCommand<String> { private final static DynamicBooleanProperty usePrimary = DynamicPropertyFactory.getInstance().getBooleanProperty(“primarySecondary.usePrimary”, true); private final int id; public CommandFacadeWithPrimarySecondary(int id) { super(Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(“SystemX”)) .andCommandKey(HystrixCommandKey.Factory.asKey(“PrimarySecondaryCommand”)) .andCommandPropertiesDefaults( HystrixCommandProperties.Setter() .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE))); this.id = id; } @Override protected String run() { if (usePrimary.get()) { return new PrimaryCommand(id).execute(); } else { return new SecondaryCommand(id).execute(); } } @Override protected String getFallback() { return “static-fallback-” + id; } @Override protected String getCacheKey() { return String.valueOf(id); } private static class PrimaryCommand extends HystrixCommand<String> { private final int id; private PrimaryCommand(int id) { super(Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(“SystemX”)) .andCommandKey(HystrixCommandKey.Factory.asKey(“PrimaryCommand”)) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(“PrimaryCommand”)) .andCommandPropertiesDefaults( // we default to a 600ms timeout for primary HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(600))); this.id = id; } @Override protected String run() { // perform expensive ‘primary’ service call return “responseFromPrimary-” + id; } } private static class SecondaryCommand extends HystrixCommand<String> { private final int id; private SecondaryCommand(int id) { super(Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(“SystemX”)) .andCommandKey(HystrixCommandKey.Factory.asKey(“SecondaryCommand”)) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(“SecondaryCommand”)) .andCommandPropertiesDefaults( // we default to a 100ms timeout for secondary HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(100))); this.id = id; } @Override protected String run() { // perform fast ‘secondary’ service call return “responseFromSecondary-” + id; } } public static class UnitTest { @Test public void testPrimary() { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { ConfigurationManager.getConfigInstance().setProperty(“primarySecondary.usePrimary”, true); assertEquals(“responseFromPrimary-20”, new CommandFacadeWithPrimarySecondary(20).execute()); } finally { context.shutdown(); ConfigurationManager.getConfigInstance().clear(); } } @Test public void testSecondary() { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { ConfigurationManager.getConfigInstance().setProperty(“primarySecondary.usePrimary”, false); assertEquals(“responseFromSecondary-20”, new CommandFacadeWithPrimarySecondary(20).execute()); } finally { context.shutdown(); ConfigurationManager.getConfigInstance().clear(); } } } } NOTE:显示调用降级适用于特殊需求的场景,fallback用于业务处理,fallback不再承担降级职责,建议慎重使用,会造成监控统计换乱等问题.命令调用合并:HystrixCollapser命令调用合并允许多个请求合并到一个线程/信号下批量执行。执行流程图如下:public class CommandCollapserGetValueForKey extends HystrixCollapser<List<String>, String, Integer> { private final Integer key; public CommandCollapserGetValueForKey(Integer key) { this.key = key; } @Override public Integer getRequestArgument() { return key; } @Override protected HystrixCommand<List<String>> createCommand(final Collection<CollapsedRequest<String, Integer>> requests) { //创建返回command对象 return new BatchCommand(requests); } @Override protected void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, Integer>> requests) { int count = 0; for (CollapsedRequest<String, Integer> request : requests) { //手动匹配请求和响应 request.setResponse(batchResponse.get(count++)); } } private static final class BatchCommand extends HystrixCommand<List<String>> { private final Collection<CollapsedRequest<String, Integer>> requests; private BatchCommand(Collection<CollapsedRequest<String, Integer>> requests) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“ExampleGroup”)) .andCommandKey(HystrixCommandKey.Factory.asKey(“GetValueForKey”))); this.requests = requests; } @Override protected List<String> run() { ArrayList<String> response = new ArrayList<String>(); for (CollapsedRequest<String, Integer> request : requests) { response.add(“ValueForKey: " + request.getArgument()); } return response; } } public static class UnitTest { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { Future<String> f1 = new CommandCollapserGetValueForKey(1).queue(); Future<String> f2 = new CommandCollapserGetValueForKey(2).queue(); Future<String> f3 = new CommandCollapserGetValueForKey(3).queue(); Future<String> f4 = new CommandCollapserGetValueForKey(4).queue(); assertEquals(“ValueForKey: 1”, f1.get()); assertEquals(“ValueForKey: 2”, f2.get()); assertEquals(“ValueForKey: 3”, f3.get()); assertEquals(“ValueForKey: 4”, f4.get()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getExecutedCommands().size()); HystrixCommand<?> command = HystrixRequestLog.getCurrentRequest().getExecutedCommands().toArray(new HystrixCommand<?>[1])[0]; assertEquals(“GetValueForKey”, command.getCommandKey().name()); assertTrue(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED)); assertTrue(command.getExecutionEvents().contains(HystrixEventType.SUCCESS)); } finally { context.shutdown(); } } } NOTE:使用场景:HystrixCollapser用于对多个相同业务的请求合并到一个线程甚至可以合并到一个连接中执行,降低线程交互次和IO数,但必须保证他们属于同一依赖.觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能 ...

November 30, 2018 · 6 min · jiezi

Spring Flux中Request与HandlerMapping关系的形成过程

一、前言Spring Flux中的核心DispatcherHandler的处理过程分为三步,其中首步就是通过HandlerMapping接口查找Request所对应的Handler。本文就是通过阅读源码的方式,分析一下HandlerMapping接口的实现者之一——RequestMappingHandlerMapping类,用于处理基于注解的路由策略,把所有用@Controller和@RequestMapping标记的类中的Handler识别出来,以便DispatcherHandler调用的。HandlerMapping接口的另外两种实现类:1、RouterFunctionMapping用于函数式端点的路由;2、SimpleUrlHandlerMapping用于显式注册的URL模式与WebHandler配对。<!– more –>文章系列关于非阻塞IO:《从时间碎片角度理解阻塞IO模型及非阻塞模型》关于SpringFlux新手入门:《快速上手Spring Flux框架》为什么Spring要引入SpringFlux框架 尚未完成Spring Flux中Request与HandlerMapping关系的形成过程 本文Spring Flux中执行HandlerMapping的过程 尚未完成Spring Flux中是如何处理HandlerResult的 尚未完成Spring Flux与WEB服务器之Servlet 3.1+ 尚未完成Spring Flux与WEB服务器之Netty 尚未完成二、对基于注解的路由控制器的抽象Spring中基于注解的控制器的使用方法大致如下:@Controllerpublic class MyHandler{ @RequestMapping("/") public String handlerMethod(){ }}在Spring WebFlux中,对上述使用方式进行了三层抽象模型。Mapping用户定义的基于annotation的映射关系该抽象对应的类是:org.springframework.web.reactive.result.method.RequestMappingInfo比如上述例子中的 @RequestMapping("/")所代表的映射关系Handler代表控制器的类该抽象对应的类是:java.lang.Class比如上述例子中的MyHandler类Method具体处理映射的方法该抽象对应的类是:java.lang.reflect.Method比如上述例子中的String handlerMethod()方法基于上述三层抽象模型,进而可以作一些组合。HandlerMethodHandler与Method的结合体,Handler(类)与Method(方法)搭配后就成为一个可执行的单元了Mapping vs HandlerMethod把Mapping与HandlerMethod作为字典存起来,就可以根据请求中的关键信息(路径、头信息等)来匹配到Mapping,再根据Mapping找到HandlerMethod,然后执行HandlerMethod,并传递随请求而来的参数。理解了这个抽象模型后,接下来分析源码来理解Spring WebFlux如何处理请求与Handler之间的Mapping关系时,就非常容易了。HandlerMapping接口及其各实现类负责上述模型的构建与运作。三、HandlerMapping接口实现的设计模式HandlerMapping接口实现,采用了"模版方法"这种设计模式。1层:AbstractHandlerMapping implements HandlerMapping, Ordered, BeanNameAware ^ | 2层:AbstractHandlerMethodMapping implements InitializingBean ^ | 3层:RequestMappingInfoHandlerMapping ^ | 4层:RequestMappingHandlerMapping implements EmbeddedValueResolverAware 下面对各层的职责作简要说明:第1层主要实现了对外提供模型的接口即重载了HandlerMapping接口的"Mono<Object> getHandler(ServerWebExchange exchange) “方法,并定义了骨架代码。第2层有两个责任 —— 解析用户定义的HandlerMethod + 实现对外提供模型接口实现所需的抽象方法通过实现了InitializingBean接口的"void afterPropertiesSet()“方法,解析用户定义的Handler和Method。实现第1层对外提供模型接口实现所需的抽象方法:“Mono<?> getHandlerInternal(ServerWebExchange exchange)“第3层提供根据请求匹配Mapping模型实例的方法第4层实现一些高层次用到的抽象方法来创建具体的模型实例。小结一下,就是HandlerMapping接口及其实现类,把用户定义的各Controller等,抽象为上述的Mapping、Handler及Method模型,并将Mapping与HandlerMethod作为字典关系存起来,还提供通过匹配请求来获得HandlerMethod的公共方法。接下来的章节,将先分析解析用户定义的模型并缓存模型的过程,然后再分析一下匹配请求来获得HandlerMethod的公共方法的过程。四、解析用户定义的模型并缓存模型的过程4-1、实现InitializingBean接口第2层AbstractHandlerMethodMapping抽象类中的一个重要方法——实现了InitializingBean接口的"void afterPropertiesSet()“方法,为Spring WebFlux带来了解析用户定义的模型并缓存模型的机会 —— Spring容器初初始化完成该类的具体类的Bean后,将会回调这个方法。在该方法中,实现获取用户定义的Handler、Method、Mapping以及缓存Mapping与HandlerMethod映射关系的功能。@Overridepublic void afterPropertiesSet() { initHandlerMethods(); // Total includes detected mappings + explicit registrations via registerMapping.. …}4-2、找到用户定义的HandlerafterPropertiesSet方法中主要是调用了void initHandlerMethods()方法,具体如下:protected void initHandlerMethods() { //获取Spring容器中所有Bean名字 String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; try { //获取Bean的类型 beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { // An unresolvable bean type, probably from a lazy bean - let’s ignore it. if (logger.isTraceEnabled()) { logger.trace(“Could not resolve type for bean ‘” + beanName + “’”, ex); } } //如果获取到类型,并且类型是Handler,则继续加载Handler方法。 if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } } //初始化后收尾工作 handlerMethodsInitialized(getHandlerMethods());}这儿首先获取Spring容器中所有Bean名字,然后循环处理每一个Bean。如果Bean名称不是以SCOPED_TARGET_NAME_PREFIX常量开头,则获取Bean的类型。如果获取到类型,并且类型是Handler,则继续加载Handler方法。isHandler(beanType)调用,检查Bean的类型是否符合handler定义。AbstractHandlerMethodMapping抽象类中定义的抽象方法"boolean isHandler(Class<?> beanType)",是由RequestMappingHandlerMapping类实现的。具体实现代码如下:protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));}不难看出,对于RequestMappingHandlerMapping这个实现类来说,只有拥有@Controller或者@RequestMapping注解的类,才是Handler。(言下之意对于其他实现类来说Handler的定义不同)。具体handler的定义,在HandlerMapping各实现类来说是不同的,这也是isHandler抽象方法由具体实现类来实现的原因。4-3、发现Handler的Method接下来我们要重点看一下"detectHandlerMethods(beanName);“这个方法调用。protected void detectHandlerMethods(final Object handler) { Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null) { //将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型) final Class<?> userType = ClassUtils.getUserClass(handlerType); //寻找目标类型userType中的Methods,selectMethods方法的第二个参数是lambda表达式,即感兴趣的方法的过滤规则 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, //回调函数metadatalookup将通过controller定义的mapping与手动定义的mapping合并起来 (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); if (logger.isTraceEnabled()) { logger.trace(“Mapped " + methods.size() + " handler method(s) for " + userType + “: " + methods); } methods.forEach((key, mapping) -> { //再次核查方法与类型是否匹配 Method invocableMethod = AopUtils.selectInvocableMethod(key, userType); //如果是满足要求的方法,则注册到全局的MappingRegistry实例里 registerHandlerMethod(handler, invocableMethod, mapping); }); }}首先将参数handler(即外部传入的BeanName或者BeanType)转换为Class<?>类型变量handlerType。如果转换成功,再将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型)。接下来获取该用户类型里所有的方法(Method)。循环处理每个方法,如果是满足要求的方法,则注册到全局的MappingRegistry实例里。4-4、解析Mapping信息其中,以下代码片段有必要深入探究一下 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));MethodIntrospector.selectMethods方法的调用,将会把用@RequestMapping标记的方法筛选出来,并交给第二个参数所定义的MetadataLookup回调函数将通过controller定义的mapping与手动定义的mapping合并起来。第二个参数是用lambda表达式传入的,表达式中将method、userType传给getMappingForMethod(method, userType)方法。getMappingForMethod方法在高层次中是抽象方法,具体的是现在第4层RequestMappingHandlerMapping类中实现。在具体实现getMappingForMethod时,会调用到RequestMappingHandlerMapping类的下面这个方法。从该方法中,我们可以看到,首先会获得参数element(即用户在Controller中定义的方法)的RequestMapping类型的类实例,然后构造代表Mapping抽象模型的RequestmappingInfo类型实例并返回。private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); }构造代表Mapping抽象模型的RequestmappingInfo类型实例,用的是createRequestMappingInfo方法,如下。可以看到RequestMappingInfo所需要的信息,包括paths、methods、params、headers、consumers、produces、mappingName,即用户定义@RequestMapping注解时所设定的可能的参数,都被存在这儿了。拥有了这些信息,当请求来到时,RequestMappingInfo就可以测试自身是否是处理该请求的人选之一了。protected RequestMappingInfo createRequestMappingInfo( RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) { RequestMappingInfo.Builder builder = RequestMappingInfo .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) .methods(requestMapping.method()) .params(requestMapping.params()) .headers(requestMapping.headers()) .consumes(requestMapping.consumes()) .produces(requestMapping.produces()) .mappingName(requestMapping.name()); if (customCondition != null) { builder.customCondition(customCondition); } return builder.options(this.config).build(); }4-5、缓存Mapping与HandlerMethod关系最后,registerHandlerMethod(handler, invocableMethod, mapping)调用将缓存HandlerMethod,其中mapping参数是RequestMappingInfo类型的。。内部调用的是MappingRegistry实例的void register(T mapping, Object handler, Method method)方法,其中T是RequestMappingInfo类型。MappingRegistry类维护所有指向Handler Methods的映射,并暴露方法用于查找映射,同时提供并发控制。public void register(T mapping, Object handler, Method method) { this.readWriteLock.writeLock().lock(); try { HandlerMethod handlerMethod = createHandlerMethod(handler, method); …… this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name)); } finally { this.readWriteLock.writeLock().unlock(); } }五、匹配请求来获得HandlerMethodAbstractHandlerMethodMapping类的“Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange)”方法,具体实现了根据请求查找HandlerMethod的逻辑。 @Override public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) { //获取读锁 this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod; try { //调用其它方法继续查找HandlerMethod handlerMethod = lookupHandlerMethod(exchange); } catch (Exception ex) { return Mono.error(ex); } if (handlerMethod != null) { handlerMethod = handlerMethod.createWithResolvedBean(); } return Mono.justOrEmpty(handlerMethod); } //释放读锁 finally { this.mappingRegistry.releaseReadLock(); } }handlerMethod = lookupHandlerMethod(exchange)调用,继续查找HandlerMethod。我们继续看一下HandlerMethod lookupHandlerMethod(ServerWebExchange exchange)方法的定义。为方便阅读,我把注释也写在了代码里。 protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws Exception { List<Match> matches = new ArrayList<>(); //查找所有满足请求的Mapping,并放入列表mathes addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); if (!matches.isEmpty()) { //获取比较器comparator Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange)); //使用比较器将列表matches排序 matches.sort(comparator); //将排在第1位的作为最佳匹配项 Match bestMatch = matches.get(0); if (matches.size() > 1) { //将排在第2位的作为次佳匹配项 Match secondBestMatch = matches.get(1); } handleMatch(bestMatch.mapping, bestMatch.handlerMethod, exchange); return bestMatch.handlerMethod; } else { return handleNoMatch(this.mappingRegistry.getMappings().keySet(), exchange); } }六、总结理解了Spring WebFlux在获取映射关系方面的抽象设计模型后,就很容易读懂代码,进而更加理解框架的具体处理方式,在使用框架时做到“知己知彼”。原文:http://www.yesdata.net/2018/11/27/spring-flux-request-mapping/ ...

November 30, 2018 · 3 min · jiezi

如何在优雅地Spring 中实现消息的发送和消费

本文将对rocktmq-spring-boot的设计实现做一个简单的介绍,读者可以通过本文了解将RocketMQ Client端集成为spring-boot-starter框架的开发细节,然后通过一个简单的示例来一步一步的讲解如何使用这个spring-boot-starter工具包来配置,发送和消费RocketMQ消息。通过本文,您将了解到:Spring的消息框架介绍rocketmq-spring-boot具体实现使用示例前言上世纪90年代末,随着Java EE(Enterprise Edition)的出现,特别是Enterprise Java Beans的使用需要复杂的描述符配置和死板复杂的代码实现,增加了广大开发者的学习曲线和开发成本,由此基于简单的XML配置和普通Java对象(Plain Old Java Objects)的Spring技术应运而生,依赖注入(Dependency Injection), 控制反转(Inversion of Control)和面向切面编程(AOP)的技术更加敏捷地解决了传统Java企业及版本的不足。随着Spring的持续演进,基于注解(Annotation)的配置逐渐取代了XML文件配置, 2014年4月1日,Spring Boot 1.0.0正式发布,它基于“约定大于配置”(Convention over configuration)这一理念来快速地开发、测试、运行和部署Spring应用,并能通过简单地与各种启动器(如 spring-boot-web-starter)结合,让应用直接以命令行的方式运行,不需再部署到独立容器中。这种简便直接快速构建和开发应用的过程,可以使用约定的配置并且简化部署,受到越来越多的开发者的欢迎。Apache RocketMQ是业界知名的分布式消息和流处理中间件,简单地理解,它由Broker服务器和客户端两部分组成:其中客户端一个是消息发布者客户端(Producer),它负责向Broker服务器发送消息;另外一个是消息的消费者客户端(Consumer),多个消费者可以组成一个消费组,来订阅和拉取消费Broker服务器上存储的消息。为了利用Spring Boot的快速开发和让用户能够更灵活地使用RocketMQ消息客户端,Apache RocketMQ社区推出了spring-boot-starter实现。随着分布式事务消息功能在RocketMQ 4.3.0版本的发布,近期升级了相关的spring-boot代码,通过注解方式支持分布式事务的回查和事务消息的发送。本文将对当前的设计实现做一个简单的介绍,读者可以通过本文了解将RocketMQ Client端集成为spring-boot-starter框架的开发细节,然后通过一个简单的示例来一步一步的讲解如何使用这个spring-boot-starter工具包来配置,发送和消费RocketMQ消息。Spring 中的消息框架顺便在这里讨论一下在Spring中关于消息的两个主要的框架,即Spring Messaging和Spring Cloud Stream。它们都能够与Spring Boot整合并提供了一些参考的实现。和所有的实现框架一样,消息框架的目的是实现轻量级的消息驱动的微服务,可以有效地简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。2.1 Spring MessagingSpring Messaging是Spring Framework 4中添加的模块,是Spring与消息系统集成的一个扩展性的支持。它实现了从基于JmsTemplate的简单的使用JMS接口到异步接收消息的一整套完整的基础架构,Spring AMQP提供了该协议所要求的类似的功能集。 在与Spring Boot的集成后,它拥有了自动配置能力,能够在测试和运行时与相应的消息传递系统进行集成。单纯对于客户端而言,Spring Messaging提供了一套抽象的API或者说是约定的标准,对消息发送端和消息接收端的模式进行规定,不同的消息中间件提供商可以在这个模式下提供自己的Spring实现:在消息发送端需要实现的是一个XXXTemplate形式的Java Bean,结合Spring Boot的自动化配置选项提供多个不同的发送消息方法;在消息的消费端是一个XXXMessageListener接口(实现方式通常会使用一个注解来声明一个消息驱动的POJO),提供回调方法来监听和消费消息,这个接口同样可以使用Spring Boot的自动化选项和一些定制化的属性。如果有兴趣深入的了解Spring Messaging及针对不同的消息产品的使用,推荐阅读这个文件。参考Spring Messaging的既有实现,RocketMQ的spring-boot-starter中遵循了相关的设计模式并结合RocketMQ自身的功能特点提供了相应的API(如,顺序,异步和事务半消息等)。2.2 Spring Cloud StreamSpring Cloud Stream结合了Spring Integration的注解和功能,它的应用模型如下:Spring Cloud Stream框架中提供一个独立的应用内核,它通过输入(@Input)和输出(@Output)通道与外部世界进行通信,消息源端(Source)通过输入通道发送消息,消费目标端(Sink)通过监听输出通道来获取消费的消息。这些通道通过专用的Binder实现与外部代理连接。开发人员的代码只需要针对应用内核提供的固定的接口和注解方式进行编程,而不需要关心运行时具体的Binder绑定的消息中间件。在运行时,Spring Cloud Stream能够自动探测并使用在classpath下找到的Binder。这样开发人员可以轻松地在相同的代码中使用不同类型的中间件:仅仅需要在构建时包含进不同的Binder。在更加复杂的使用场景中,也可以在应用中打包多个Binder并让它自己选择Binder,甚至在运行时为不同的通道使用不同的Binder。Binder抽象使得Spring Cloud Stream应用可以灵活的连接到中间件,加之Spring Cloud Stream使用利用了Spring Boot的灵活配置配置能力,这样的配置可以通过外部配置的属性和Spring Boo支持的任何形式来提供(包括应用启动参数、环境变量和application.yml或者application.properties文件),部署人员可以在运行时动态选择通道连接destination(例如,Kafka的topic或者RabbitMQ的exchange)。Binder SPI的方式来让消息中间件产品使用可扩展的API来编写相应的Binder,并集成到Spring Cloud Steam环境,目前RocketMQ还没有提供相关的Binder,我们计划在下一步将完善这一功能,也希望社区里有这方面经验的同学积极尝试,贡献PR或建议。spring-boot-starter的实现在开始的时候我们已经知道,spring boot starter构造的启动器对于使用者是非常方便的,使用者只要在pom.xml引入starter的依赖定义,相应的编译,运行和部署功能就全部自动引入。因此常用的开源组件都会为Spring的用户提供一个spring-boot-starter封装给开发者,让开发者非常方便集成和使用,这里我们详细的介绍一下RocketMQ(客户端)的starter实现过程。3.1. spring-boot-starter的实现步骤对于一个spring-boot-starter实现需要包含如下几个部分:在pom.xml的定义定义最终要生成的starter组件信息<groupId>org.apache.rocketmq</groupId><artifactId>spring-boot-starter-rocketmq</artifactId><version>1.0.0-SNAPSHOT</version>定义依赖包,它分为两个部分: A、Spring自身的依赖包; B、RocketMQ的依赖包<dependencies> <!– spring-boot-start internal depdencies –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!– rocketmq dependencies –> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>${rocketmq-version}</version> </dependency></dependencies> <dependencyManagement> <dependencies> <!– spring-boot-start parent depdency definition –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>配置文件类定义应用属性配置文件类RocketMQProperties,这个Bean定义一组默认的属性值。用户在使用最终的starter时,可以根据这个类定义的属性来修改取值,当然不是直接修改这个类的配置,而是spring-boot应用中对应的配置文件:src/main/resources/application.properties.定义自动加载类定义 src/resources/META-INF/spring.factories文件中的自动加载类, 其目的是让spring boot更具文中中所指定的自动化配置类来自动初始化相关的Bean,Component或Service,它的内容如下:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.apache.rocketmq.spring.starter.RocketMQAutoConfiguration在RocketMQAutoConfiguration类的具体实现中,定义开放给用户直接使用的Bean对象. 包括:RocketMQProperties 加载应用属性配置文件的处理类;RocketMQTemplate 发送端用户发送消息的发送模板类;ListenerContainerConfiguration 容器Bean负责发现和注册消费端消费实现接口类,这个类要求:由@RocketMQMessageListener注解标注;实现RocketMQListener泛化接口。最后具体的RocketMQ相关的封装在发送端(producer)和消费端(consumer)客户端分别进行封装,在当前的实现版本提供了对Spring Messaging接口的兼容方式。3.2. 消息发送端实现普通发送端发送端的代码封装在RocketMQTemplate POJO中,下图是发送端的相关代码的调用关系图:为了与Spring Messaging的发送模板兼容,在RocketMQTemplate集成了AbstractMessageSendingTemplate抽象类,来支持相关的消息转换和发送方法,这些方法最终会代理给doSend()方法;doSend()以及RocoketMQ所特有的一些方法如异步,单向和顺序等方法直接添加到RoketMQTempalte中,这些方法直接代理调用到RocketMQ的Producer API来进行消息的发送。事务消息发送端对于事务消息的处理,在消息发送端进行了部分的扩展,参考下图的调用关系类图:RocketMQTemplate里加入了一个发送事务消息的方法sendMessageInTransaction(), 并且最终这个方法会代理到RocketMQ的TransactionProducer进行调用,在这个Producer上会注册其关联的TransactionListener实现类,以便在发送消息后能够对TransactionListener里的方法实现进行调用。3.3. 消息消费端实现在消费端Spring-Boot应用启动后,会扫描所有包含@RocketMQMessageListener注解的类(这些类需要集成RocketMQListener接口,并实现onMessage()方法),这个Listener会一对一的被放置到DefaultRocketMQListenerContainer容器对象中,容器对象会根据消费的方式(并发或顺序),将RocketMQListener封装到具体的RocketMQ内部的并发或者顺序接口实现。在容器中创建RocketMQ Consumer对象,启动并监听定制的Topic消息,如果有消费消息,则回调到Listener的onMessage()方法。使用示例上面的一章介绍了RocketMQ在spring-boot-starter方式的实现,这里通过一个最简单的消息发送和消费的例子来介绍如何使这个rocketmq-spring-boot-starter。4.1 RocketMQ服务端的准备启动NameServer和Broker要验证RocketMQ的Spring-Boot客户端,首先要确保RocketMQ服务正确的下载并启动。可以参考RocketMQ主站的快速开始来进行操作。确保启动NameServer和Broker已经正确启动。创建实例中所需要的Topics在执行启动命令的目录下执行下面的命令行操作bash bin/mqadmin updateTopic -c DefaultCluster -t string-topic4.2. 编译rocketmq-spring-boot-starter目前的spring-boot-starter依赖还没有提交的Maven的中心库,用户使用前需要自行下载git源码,然后执行mvn clean install 安装到本地仓库。git clone https://github.com/apache/rocketmq-externals.gitcd rocketmq-spring-boot-startermvn clean install4.3. 编写客户端代码用户如果使用它,需要在消息的发布和消费客户端的maven配置文件pom.xml中添加如下的依赖:<properties> <spring-boot-starter-rocketmq-version>1.0.0-SNAPSHOT</spring-boot-starter-rocketmq-version></properties><dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>spring-boot-starter-rocketmq</artifactId> <version>${spring-boot-starter-rocketmq-version}</version></dependency>属性spring-boot-starter-rocketmq-version的取值为:1.0.0-SNAPSHOT, 这与上一步骤中执行安装到本地仓库的版本一致。消息发送端的代码发送端的配置文件application.properties# 定义name-server地址spring.rocketmq.name-server=localhost:9876# 定义发布者组名spring.rocketmq.producer.group=my-group1# 定义要发送的topicspring.rocketmq.topic=string-topic发送端的Java代码import org.apache.rocketmq.spring.starter.core.RocketMQTemplate;…@SpringBootApplicationpublic class ProducerApplication implements CommandLineRunner { // 声明并引用RocketMQTemplate @Resource private RocketMQTemplate rocketMQTemplate; // 使用application.properties里定义的topic属性 @Value("${spring.rocketmq.springTopic}") private String springTopic; public static void main(String[] args){ SpringApplication.run(ProducerApplication.class, args); } public void run(String… args) throws Exception { // 以同步的方式发送字符串消息给指定的topic SendResult sendResult = rocketMQTemplate.syncSend(springTopic, “Hello, World!”); // 打印发送结果信息 System.out.printf(“string-topic syncSend1 sendResult=%s %n”, sendResult); }}消息消费端代码消费端的配置文件application.properties# 定义name-server地址spring.rocketmq.name-server=localhost:9876# 定义发布者组名spring.rocketmq.consumer.group=my-customer-group1# 定义要发送的topicspring.rocketmq.topic=string-topic消费端的Java代码@SpringBootApplicationpublic class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); }}// 声明消费消息的类,并在注解中指定,相关的消费信息@Service@RocketMQMessageListener(topic = “${spring.rocketmq.topic}”, consumerGroup = “${spring.rocketmq.consumer.group}")class StringConsumer implements RocketMQListener<String> { @Override public void onMessage(String message) { System.out.printf(”——- StringConsumer received: %s %f", message); }}这里只是简单的介绍了使用spring-boot来编写最基本的消息发送和接收的代码,如果需要了解更多的调用方式,如: 异步发送,对象消息体,指定tag标签以及指定事务消息,请参看github的说明文档和详细的代码。我们后续还会对这些高级功能进行陆续的介绍。预告:近期还会推出第二篇文章解读springboot框架下消息事务的用法。参考文档1.Spring Boot features-Messaging2.Enterprise Integration Pattern-组成简介3.Spring Cloud Stream Reference Guide4.https://dzone.com/articles/creating-custom-springboot-starter-for-twitter4j本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 29, 2018 · 2 min · jiezi

AspectJ在Spring中的使用

在上一篇AspectJ的入门中,简单的介绍了下AspectJ的使用,主要是以AspectJ的example作为例子。介绍完后也留下了几个问题:1)我们在spring中并没有看到需要aspectj之类的关键词,而是使用java代码就可以了,这是如何做到的2)Spring中如何做到不使用特殊的编译器实现aop的(AspectJ如何在运行期使用)3)Spring源码中与aspectJ 相关的AjType究竟是啥?这篇文章会继续试着解决这几个问题。aspectJ的几种织入方式compile-time、post-compile 和 load-time Weavers首先了解下AspectJ的几种织入方式,分别是compile-time、post-compile 和 load-time,分别对应着编译期、后编译期、加载期织入编译期织入首先是编译期织入,上一篇博客所介绍的方式就是使用的编译期织入。很容易理解,普通的java源码+ aspectJ特殊语法的‘配置’ 文件 + aspectJ特殊的编译器,编译时候生成已织入后的.class文件,运行时直接运行即可。后编译期织入后编译期织入和编译期的不同在于,织入的是class字节码或者jar文件。这种形式,可以织入一个已经织入过一次的切面。同样这种情况也需要特殊的编译器加载期织入加载期顾名思义,是在类被加载进虚拟机之前织入,使用这种方式,须使用AspectJ agent。了解了这些概念,下面就要知道,spring是使用哪种呢?spring哪一种都不是,spring是在运行期进行的织入。Spring 如何使用AspectJAspectJ 本身是不支持运行期织入的,日常使用时候,我们经常回听说,spring 使用了aspectJ实现了aop,听起来好像spring的aop完全是依赖于aspectJ其实spring对于aop的实现是通过动态代理(jdk的动态代理或者cglib的动态代理),它只是使用了aspectJ的Annotation,并没有使用它的编译期和织入器,关于这个可以看这篇文章 ,也就是说spring并不是直接使用aspectJ实现aop的spring aop与aspectJ的区别看了很多篇博客以及源码,我对spring aop与aspectJ的理解大概是这样;1)spring aop 使用AspectJ语法的一个子集,一些method call, class member set/get 等aspectJ支持的语法它都不支持2)spring aop 底层是动态代理,所以受限于这点,有些增强就做不到,比如 调用自己的方法就无法走代理看下下面的例子:@Componentpublic class A{ public void method1(){ method2(); } public void method2(){ //… }}这个时候method2是无法被切到的,要想被切到可以通过如下奇葩的方式:@Componentpublic class A{ @Autowired private A a; public void method1(){ a.method2(); } public void method2(){ //… }}之前碰到这样的问题时,我还特别不能理解,现在想下aop的底层实现方式就很容易理解了。在之前写的jdk动态代理与cglib动态代理实现原理,我们知道了jdk动态代理是通过动态生成一个类的方式实现的代理,也就是说代理是不会修改底层类字节码的,所以可能生成的代理方法是这样的public void method1(){ //执行一段代码 a.method1() //执行一段代码}public void method2(){ //执行一段代码 a.method2() //执行一段代码}回头看a.method1()的源码,也就明白了,为啥method2()没有被切到,因为a.method1()执行的方法,最后调用的不是 代理对象.method2(),而是它自己的method2()(this.method2()) 这个方法本身没有任何改动反观aspectJ,aspectJ是在编译期修改了方法(类本身的字节码被改了),所以可以很轻松地实现调用自己的方法时候的增强。3)spring aop的代理必须依赖于bean被spring管理,所以如果项目没有使用spring,又想使用aop,那就只能使用aspectJ了(不过现在没有用spring的项目应该挺少的吧。。。)4)aspectJ由于是编译期进行的织入,性能会比spring好一点5)spring可以通过@EnableLoadTimeWeaving 开启加载期织入(只是知道这个东西,没怎么研究。。有兴趣的可以自己去研究下)6)spring aop很多概念和aspectJ是一致的AspectJ的注解在spring aop中的应用了解了spring与aspectJ的关系后,就能更清晰的了解spring 的aop了。先说明一点,虽然我介绍aspect的配置时,一直介绍的aspectJ文件配置方式,但是aspectJ本身是支持注解方式配置的。可以看官方文档,注解在aspectJ中的使用而spring 使用了aspectJ注解的一小部分(正如前面所说的,受限于jdk的动态代理,spring只支持方法级别的切面)回头看看AjType回头看看之前看到的这段源码,什么是AjType,经过aspectJ解析器解析后对类的一种描述,比如正常的方法可能是这样/* * 配置前置通知,使用在方法aspect()上注册的切入点 * 同时接受JoinPoint切入点对象,可以没有该参数 /@Before(“aspect()")public void before(JoinPoint joinPoint) { log.info(“before " + joinPoint);}在AjType中就能获取到很多其他的aspectJ所需的相关信息(除了java反射所能获取到的信息以外)/* * Return the pointcut object representing the specified pointcut declared by this type /public Pointcut getDeclaredPointcut(String name) throws NoSuchPointcutException;/* * Return the pointcut object representing the specified public pointcut */public Pointcut getPointcut(String name) throws NoSuchPointcutException;比如看着两个方法,可以获取到切入点信息。在看看PerClauseKind.SINGLETON 这里就复用了aspectJ的概念,详细可以看这篇文章最后部分总结下这篇文章回答了之前学习aspectJ时候碰到的几个问题,然后讨论了下aspectJ在spring中的应用。最大的收获是了解了spring与aspectJ 的关系,了解了两者对aop的不同实现所造成的使用上的影响。以后当遇到了spring aop相关的概念如果不理解,可以去aspectJ上去搜搜看了 。参考文章:Intro to AspectJspring 使用 load-time weavingspring aop和 aspectJ 的比较本文作者:端吉阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 22, 2018 · 1 min · jiezi

Spring Cloud底层原理

毫无疑问,Spring Cloud 是目前微服务架构领域的翘楚,无数的书籍博客都在讲解这个技术。不过大多数讲解还停留在对 Spring Cloud 功能使用的层面,其底层的很多原理,很多人可能并不知晓。实际上,Spring Cloud 是一个全家桶式的技术栈,它包含了很多组件。本文先从最核心的几个组件,也就是 Eureka、Ribbon、Feign、Hystrix、Zuul 入手,来剖析其底层的工作原理。业务场景介绍先来给大家说一个业务场景,假设咱们现在开发一个电商网站,要实现支付订单的功能。流程如下:创建一个订单后,如果用户立刻支付了这个订单,我们需要将订单状态更新为“已支付”。扣减相应的商品库存。通知仓储中心,进行发货。给用户的这次购物增加相应的积分。针对上述流程,我们需要有订单服务、库存服务、仓储服务、积分服务。整个流程的大体思路如下:用户针对一个订单完成支付之后,就会去找订单服务,更新订单状态。订单服务调用库存服务,完成相应功能。订单服务调用仓储服务,完成相应功能。订单服务调用积分服务,完成相应功能。至此,整个支付订单的业务流程结束。下面这张图,清晰表明了各服务间的调用过程:好!有了业务场景之后,咱们就一起来看看 Spring Cloud 微服务架构中,这几个组件如何相互协作,各自发挥的作用以及其背后的原理。Spring Cloud 核心组件:Eureka咱们来考虑第一个问题:订单服务想要调用库存服务、仓储服务,或者积分服务,怎么调用?订单服务压根儿就不知道人家库存服务在哪台机器上啊!它就算想要发起一个请求,都不知道发送给谁,有心无力!这时候,就轮到 Spring Cloud Eureka 出场了。Eureka 是微服务架构中的注册中心,专门负责服务的注册与发现。咱们来看看下面的这张图,结合图来仔细剖析一下整个流程:如上图所示,库存服务、仓储服务、积分服务中都有一个 Eureka Client 组件,这个组件专门负责将这个服务的信息注册到 Eureka Server 中。说白了,就是告诉 Eureka Server,自己在哪台机器上,监听着哪个端口。而 Eureka Server 是一个注册中心,里面有一个注册表,保存了各服务所在的机器和端口号。订单服务里也有一个 Eureka Client 组件,这个 Eureka Client 组件会找 Eureka Server 问一下:库存服务在哪台机器啊?监听着哪个端口啊?仓储服务呢?积分服务呢?然后就可以把这些相关信息从 Eureka Server 的注册表中拉取到自己的本地缓存中来。这时如果订单服务想要调用库存服务,不就可以找自己本地的 Eureka Client 问一下库存服务在哪台机器?监听哪个端口吗?收到响应后,紧接着就可以发送一个请求过去,调用库存服务扣减库存的那个接口!同理,如果订单服务要调用仓储服务、积分服务,也是如法炮制。总结一下:Eureka Client:负责将这个服务的信息注册到 Eureka Server 中。Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号。Spring Cloud 核心组件:Feign现在订单服务确实知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,跟其他服务建立网络连接,然后构造一个复杂的请求,接着发送请求过去,最后对返回的响应结果再写一大堆代码来处理吗?这是上述流程翻译的代码片段,咱们一起来看看,体会一下这种绝望而无助的感受!!!友情提示,前方高能:看完上面那一大段代码,有没有感到后背发凉、一身冷汗?实际上你进行服务间调用时,如果每次都手写代码,代码量比上面那段要多至少几倍,所以这个事压根儿就不是地球人能干的。既然如此,那怎么办呢?别急,Feign 早已为我们提供好了优雅的解决方案。来看看如果用 Feign 的话,你的订单服务调用库存服务的代码会变成啥样?看完上面的代码什么感觉?是不是感觉整个世界都干净了,又找到了活下去的勇气!没有底层的建立连接、构造请求、解析响应的代码,直接就是用注解定义一个 Feign Client 接口,然后调用那个接口就可以了。人家 Feign Client 会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起请求、获取响应、解析响应,等等。这一系列脏活累活,人家 Feign 全给你干了。那么问题来了,Feign 是如何做到这么神奇的呢?很简单,Feign 的一个关键机制就是使用了动态代理。咱们一起来看看上面的图,结合图来分析:首先,如果你对某个接口定义了 @FeignClient 注解,Feign 就会针对这个接口创建一个动态代理。接着你要是调用那个接口,本质就是会调用 Feign 创建的动态代理,这是核心中的核心。Feign的动态代理会根据你在接口上的 @RequestMapping 等注解,来动态构造出你要请求的服务的地址。最后针对这个地址,发起请求、解析响应。Spring Cloud 核心组件:Ribbon说完了 Feign,还没完。现在新的问题又来了,如果人家库存服务部署在了 5 台机器上。如下所示:192.168.169:9000192.168.170:9000192.168.171:9000192.168.172:9000192.168.173:9000这下麻烦了!人家 Feign 怎么知道该请求哪台机器呢?这时 Spring Cloud Ribbon 就派上用场了。Ribbon 就是专门解决这个问题的。它的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上。Ribbon 的负载均衡默认使用的最经典的 Round Robin 轮询算法。这是啥?简单来说,就是如果订单服务对库存服务发起 10 次请求,那就先让你请求第 1 台机器、然后是第 2 台机器、第 3 台机器、第 4 台机器、第 5 台机器,接着再来—个循环,第 1 台机器、第 2 台机器。。。以此类推。此外,Ribbon 是和 Feign 以及 Eureka 紧密协作,完成工作的,具体如下:首先 Ribbon 会从 Eureka Client 里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。然后 Ribbon 就可以使用默认的 Round Robin 算法,从中选择一台机器。Feign 就会针对这台机器,构造并发起请求。对上述整个过程,再来一张图,帮助大家更深刻的理解:Spring Cloud 核心组件:Hystrix在微服务架构里,一个系统会有很多的服务。以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有 100 个线程可以处理请求,然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。咱们一起来分析一下,这样会导致什么问题?如果系统处于高并发的场景下,大量请求涌过来的时候,订单服务的 100 个线程都会卡在请求积分服务这块,导致订单服务没有一个线程可以处理请求。然后就会导致别人请求订单服务的时候,发现订单服务也挂了,不响应任何请求了。上面这个,就是微服务架构中恐怖的服务雪崩问题,如下图所示:如上图,这么多服务互相调用,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。比如积分服务挂了,会导致订单服务的线程全部卡在请求积分服务这里,没有一个线程可以工作,瞬间导致订单服务也挂了,别人请求订单服务全部会卡住,无法响应。但是我们思考一下,就算积分服务挂了,订单服务也可以不用挂啊!为什么?我们结合业务来看:支付订单的时候,只要把库存扣减了,然后通知仓库发货就 OK 了。如果积分服务挂了,大不了等它恢复之后,慢慢人肉手工恢复数据!为啥一定要因为一个积分服务挂了,就直接导致订单服务也挂了呢?不可以接受!现在问题分析完了,如何解决?这时就轮到 Hystrix 闪亮登场了。Hystrix 是隔离、熔断以及降级的一个框架。啥意思呢?说白了,Hystrix 会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。打个比方:现在很不幸,积分服务挂了,会咋样?当然会导致订单服务里那个用来调用积分服务的线程都卡死不能工作了啊!但由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。这个时候如果别人请求订单服务,订单服务还是可以正常调用库存服务扣减库存,调用仓储服务通知发货。只不过调用积分服务的时候,每次都会报错。但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!所以我们直接对积分服务熔断不就得了,比如在 5 分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。为帮助大家更直观的理解,接下来用一张图,梳理一下 Hystrix 隔离、熔断和降级的全流程:Spring Cloud 核心组件:Zuul说完了 Hystrix,接着给大家说说最后一个组件:Zuul,也就是微服务网关。这个组件是负责网络路由的。不懂网络路由?行,那我给你说说,如果没有 Zuul 的日常工作会怎样?假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做 inventory-service?部署在 5 台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那真是友谊的小船,说翻就翻!上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面。像 Android、iOS、PC 前端、微信小程序、H5 等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。总结最后再来总结一下,上述几个 Spring Cloud 核心组件,在微服务架构中,分别扮演的角色:Eureka:各个服务启动时,Eureka Client 都会将服务注册到 Eureka Server,并且 Eureka Client 还可以反过来从 Eureka Server 拉取注册表,从而知道其他服务在哪里。Ribbon:服务间发起请求的时候,基于 Ribbon 做负载均衡,从一个服务的多台机器中选择一台。Feign:基于 Feign 的动态代理机制,根据注解和选择的机器,拼接请求 URL 地址,发起请求。Hystrix:发起请求是通过 Hystrix 的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。Zuul:如果前端、移动端要调用后端系统,统一从 Zuul 网关进入,由 Zuul 网关转发请求给对应的服务。以上就是我们通过一个电商业务场景,阐述了 Spring Cloud 微服务架构几个核心组件的底层原理。文字总结还不够直观?没问题!我们将 Spring Cloud 的 5 个核心组件通过一张图串联起来,再来直观的感受一下其底层的架构原理: ...

November 22, 2018 · 1 min · jiezi

猫头鹰的深夜翻译:Spring线程 TaskExecutor

前言在多线程中web应用很常见,尤其当你需要开发长期任务。在Spring中,我们可以额外注意并使用框架已经提供的工具,而不是创造我们自己的线程。Spring提供了TaskExecutor作为Executors的抽象。这个接口类似于java.util.concurrent.Executor接口。在spring中有许多预先开发好的该接口的实现,可以在官方文档中详细查看。通过在Spring上下文中配置一个TaskExecutor的实现,你可以将你的TaskExecutor的实现注入到bean中,并可以在bean中访问到该线程池。在bean中使用线程池的方式如下:package com.gkatzioura.service;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationContext;import org.springframework.core.task.TaskExecutor;import org.springframework.stereotype.Service;import java.util.List;/** * Created by gkatzioura on 4/26/17. /@Servicepublic class AsynchronousService { @Autowired private ApplicationContext applicationContext; @Autowired private TaskExecutor taskExecutor; public void executeAsynchronously() { taskExecutor.execute(new Runnable() { @Override public void run() { //TODO add long running task } }); }}配置首先需要在Spring上下文中注册一个线程池。package com.gkatzioura.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.task.TaskExecutor;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;/* * Created by gkatzioura on 4/26/17. /@Configurationpublic class ThreadConfig { @Bean public TaskExecutor threadPoolTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(4); executor.setThreadNamePrefix(“default_task_executor_thread”); executor.initialize(); return executor; }}这里配置了线程池的核心线程数,最大线程数和线程池中线程名称的前缀。线程池配置完毕后,接下来的步骤就很简单了,将线程池注入到spring的component中,然后将Runnable任务提交给线程池来完成。由于我们的异步代码可能需要与应用程序的其他组件交互并注入它们,因此一种不错的方法是创建Prototype范围的Runnable实例。(Prototype在每一次调用时会新建一个实例)package com.gkatzioura;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Component;/* * Created by gkatzioura on 10/18/17. /@Component@Scope(“prototype”)public class MyThread implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(MyThread.class); @Override public void run() { LOGGER.info(“Called from thread”); }}然后,我们将executors注入我们的服务并使用它来执行可运行的实例。package com.gkatzioura.service;import com.gkatzioura.MyThread;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationContext;import org.springframework.core.task.TaskExecutor;import org.springframework.stereotype.Service;import java.util.List;/* * Created by gkatzioura on 4/26/17. /@Servicepublic class AsynchronousService { @Autowired private TaskExecutor taskExecutor; @Autowired private ApplicationContext applicationContext; public void executeAsynchronously() { MyThread myThread = applicationContext.getBean(MyThread.class); taskExecutor.execute(myThread); }}Async关键字通过spring提供的Async关键字,我们甚至无需将异步任务封装到Runnable类中,直接使用注解即可。@Async@Transactionalpublic void printEmployees() { List<Employee> employees = entityManager.createQuery(“SELECT e FROM Employee e”).getResultList(); employees.stream().forEach(e->System.out.println(e.getEmail()));}该类会通过代理的方式提交给默认的线程池。@Async@Transactionalpublic CompletableFuture<List<Employee>> fetchEmployess() { List<Employee> employees = entityManager.createQuery(“SELECT e FROM Employee e”).getResultList(); return CompletableFuture.completedFuture(employees);}该方法会返回异步执行的Future结果。需要注意,如果要开启Async关键字,则需要在配置中添加EnableAsync信息。package com.gkatzioura.config; import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.task.TaskExecutor;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; /* * Created by gkatzioura on 4/26/17. */@Configuration@EnableAsyncpublic class ThreadConfig { @Bean public TaskExecutor threadPoolTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(4); executor.setThreadNamePrefix(“sgfgd”); executor.initialize(); return executor; } }参考文章spring and async ...

November 21, 2018 · 1 min · jiezi

分布式和事务你真的很熟吗?

本文注重实战或者实现,不涉及CAP,略提ACID。本文适合基础分布式程序员:1、本文会涉及集群中节点的failover和recover问题.2、本文会涉及事务及不透明事务的问题.3、本文会提到微博和tweeter,并引出一个大数据问题.由于分布式这个话题太大,事务这个话题也太大,我们从一个集群的一个小小节点开始谈起。集群中存活的节点与同步分布式系统中,如何判断一个节点(node)是否存活?kafka这样认为:1、此节点和zookeeper能喊话.(Keep sessions with zookeeper through heartbeats.)2、此节点如果是个从节点,必须能够尽可能忠实地反映主节点的数据变化。也就是说,必须能够在主节点写了新数据后,及时复制这些变化的数据,所谓及时,不能拉下太多哦.那么,符合上面两个条件的节点就可以认为是存活的,也可以认为是同步的(in-sync).关于第1点,大家对心跳都很熟悉,那么我们可以这样认为某个节点不能和zookeeper喊话了:zookeeper-node:var timer = new timer().setInterval(10sec).onTime(slave-nodes,function(slave-nodes){ slave-nodes.forEach( node -> { boolean isAlive = node.heartbeatACK(15sec); if(!isAlive) { node.numNotAlive += 1; if(node.numNotAlive >= 3) { node.declareDeadOrFailed(); slave-nodes.remove(node); //回调也可 leader-node-app.notifyNodeDeadOrFailed(node) } }else node.numNotAlive = 0; }); }); timer.run(); //你可以回调也可以像下面这样简单的计时判断 leader-node-app: var timer = new timer() .setInterval(10sec).onTime(slave-nodes,function(slave-nodes){slave-nodes.forEach(node -> { if(node.isDeadOrFailed) { //node不能和zookeeper喊话了 }});});timer.run();关于第二点,要稍微复杂点了,怎么搞呢?来这么分析:1:数据 messages.:2:操作 op-log.3:偏移 position/offset.// 1. 先考虑messages// 2. 再考虑log的postion或者offset// 3. 考虑msg和off都记录在同源数据库或者存储设备上.(database or storage-device.) var timer = new timer().setInterval(10sec).onTime(slave-nodes,function(nodes){var core-of-cpu = 8;//嫌慢就并发呗 mod hash go!nodes.groupParallel(core-of-cpu).forEach(node -> { boolean nodeSucked = false; if(node.ackTimeDiff > 30sec) { //30秒内没有回复,node卡住了 nodeSucked = true; } if(node.logOffsetDiff > 100) { //node复制跟不上了,差距超过100条数据 nodeSucked = true; } if(nodeSucked) { //总之node“死”掉了,其实到底死没死,谁知道呢?network-error在分布式系统中或者节点失败这个事情是正常现象. node.declareDeadOrFailed(); //不和你玩啦,集群不要你了 nodes.remove(node); //该怎么处理呢,抛个事件吧. fire-event-NodeDeadOrFailed(node); }});});timer.run();上面的节点的状态管理一般由zookeeper来做,leader或者master节点也会维护那么点状态。那么应用中的leader或者master节点,只需要从zookeeper拉状态就可以,同时,上面的实现是不是一定最佳呢?不是的,而且多数操作可以合起来,但为了描述节点是否存活这个事儿,咱们这么写没啥问题。节点死掉、失败、不同步了,咋处理呢?好嘛,终于说到failover和recover了,那failover比较简单,因为还有其它的slave节点在,不影响数据读取。1:同时多个slave节点失败了?没有100%的可用性.数据中心和机房瘫痪、网络电缆切断、hacker入侵删了你的根,总之你rp爆表了.2:如果主节点失败了,那master-master不行嘛?keep-alived或者LVS或者你自己写failover吧.高可用架构(HA)又是个大件儿了,此文不展开了。我们来关注下recover方面的东西,这里把视野打开点,不仅关注slave节点重启后追log来同步数据,我们看下在实际应用中,数据请求(包括读、写、更新)失败怎么办?大家可能都会说,重试(retry)呗、重放(replay)呗或者干脆不管了呗!行,都行,这些都是策略,但具体怎么个搞法,你真的清楚了?一个bigdata问题我们先摆个探讨的背景:问题:消息流,比如微博的微博(真绕),源源不断地流进我们的应用中,要处理这些消息,有个需求是这样的:Reach is the number of unique people exposed to a URL on Twitter.那么,统计一下3小时内的本条微博(url)的reach总数。怎么解决呢?把某时间段内转发过某条微博(url)的人拉出来,把这些人的粉丝拉出来,去掉重复的人,然后求总数,就是要求的reach.为了简单,我们忽略掉日期,先看看这个方法行不行:/** ———————————* 1. 求出转发微博(url)的大V. * __________________________________/方法 :getUrlToTweetersMap(String url_id)SQL : / 数据库A,表url_user存储了转发某url的user /SELECT url_user.user_id as tweeter_idFROM url_userWHERE url_user.url_id = ${url_id} 返回 :[user_1,…,user_m]./* ———————————* 2. 求出大V的粉丝 * __________________________________/方法 : getFollowers(String tweeter_id);SQL : / 数据库B /SELECT users.id as user_idFROM usersWHERE users.followee_id = ${tweeter_id}返回:tweeter的粉丝./* ———————————* 3. 求出Reach* __________________________________/var url = queryArgs.getUrl();var tweeters = getUrlToTweetersMap();var result = new HashMap<String,Integer>();tweeters.forEach(t -> {// 你可以批量in + 并发读来优化下面方法的性能var followers = getFollowers(t.tweeter_id);followers.forEach(f -> { //hash去重 result.put(f.user_id,1);});});//Reachreturn result.size(); 顶呱呱,无论如何,求出了Reach啊!其实这又引出了一个很重要的问题,也是很多大谈框架、设计、模式却往往忽视的问题:性能和数据库建模的关系。1:数据量有多大?不知道读者有木有对这个问题的数据库I/O有点想法,或者虎躯一震呢?Computing reach is too intense for a single machine – it can require thousands of database calls and tens of millions of tuples.在上面的数据库设计中避免了JOIN,为了提高求大V粉丝的性能,可以将一批大V作为batch/bulk,然后多个batch并发读,誓死搞死数据库。这里将微博到转发者表所在的库,与粉丝库分离,如果数据更大怎么办?库再分表…OK,假设你已经非常熟悉传统关系型数据库的分库分表及数据路由(读路径的聚合、写路径的分发)、或者你对于sharding技术也很熟悉、或者你良好的结合了HBase的横向扩展能力并有一致性策略来解决其二级索引问题.总之,存储和读取的问题假设你已经解决了,那么分布式计算呢?2:微博这种应用,人与人之间的关系成图状(网),你怎么建模存储?而不仅仅对应这个问题,比如:某人的好友的好友可能和某人有几分相熟?看看用storm怎么来解决分布式计算,并提供流式计算的能力: // url到大V -> 数据库1 TridentState urlToTweeters = topology.newStaticState(getUrlToTweetersState()); // 大V到粉丝 -> 数据库2 TridentState tweetersToFollowers =topology.newStaticState(getTweeterToFollowersState());topology.newDRPCStream(“reach”).stateQuery(urlToTweeters, new Fields(“args”), new MapGet(), new Fields(“tweeters”)).each(new Fields(“tweeters”), new ExpandList(), new Fields(“tweeter”)).shuffle() / 大V的粉丝很多,所以需要分布式处理*/.stateQuery(tweetersToFollowers, new Fields(“tweeter”), new MapGet(), new Fields(“followers”)).parallelismHint(200) /* 粉丝很多,所以需要高并发 / .each(new Fields(“followers”), new ExpandList(), new Fields(“follower”)).groupBy(new Fields(“follower”)).aggregate(new One(), new Fields(“one”)) / 去重 /.parallelismHint(20).aggregate(new Count(), new Fields(“reach”)); / 计算reach数 */最多处理一次(At most once)回到主题,引出上面的例子,一是为了引出一个有关分布式(存储+计算)的问题,二是透漏这么点意思:码农,就应该关注设计和实现的东西,比如Jay Kreps是如何发明Kafka这个轮子的 : ]如果你还是码农级别,咱来务点实吧,前面我们说到recover,节点恢复的问题,那么我们恢复几个东西?基本的:1、节点状态2、节点数据本篇从数据上来讨论下这个问题,为使问题再简单点,我们考虑写数据的场景,如果我们用write-ahead-log的方式来保证数据复制和一致性,那么我们会怎么处理一致性问题呢?1:主节点有新数据写入.2:从节点追log,准备复制这批新数据。从节点做两件事:(1). 把数据的id偏移写入log;(2). 正要处理数据本身,从节点挂了。那么根据上文的节点存活条件,这个从节点挂了这件事被探测到了,从节点由维护人员手动或者其自己恢复了,那么在加入集群和小伙伴们继续玩耍之前,它要同步自己的状态和数据。问题来了:如果根据log内的数据偏移来同步数据,那么,因为这个节点在处理数据之前就把偏移写好了,可是那批数据lost-datas没有得到处理,如果追log之后的数据来同步,那么那批数据lost-datas就丢了。在这种情况下,就叫作数据最多处理一次,也就是说数据会丢失。最少处理一次(At least once)好吧,丢失数据不能容忍,那么我们换种方式来处理:1:主节点有新数据写入.2:从节点追log,准备复制这批新数据。从节点做两件事:(1). 先处理数据;(2). 正要把数据的id偏移写入log,从节点挂了。问题又来了:如果从节点追log来同步数据,那么因为那批数据duplicated-datas被处理过了,而数据偏移没有反映到log中,如果这样追,会导致这批数据重复。这种场景,从语义上来讲,就是数据最少处理一次,意味着数据处理会重复。仅处理一次(Exactly once)Transaction好吧,数据重复也不能容忍?要求挺高啊。大家都追求的强一致性保证(这里是最终一致性),怎么来搞呢?换句话说,在更新数据的时候,事务能力如何保障呢?假设一批数据如下:// 新到数据{transactionId:4urlId:99reach:5}现在要更新这批数据到库里或者log里,那么原来的情况是:// 老数据{transactionId:3urlId:99reach:3}如果说可以保证如下三点:1、事务ID的生成是强有序的.(隔离性,串行)2、同一个事务ID对应的一批数据相同.(幂等性,多次操作一个结果)3、单条数据会且仅会出现在某批数据中.(一致性,无遗漏无重复)那么,放心大胆的更新好了:// 更新后数据{ transactionId:4urlId:99//3 + 5 = 8reach:8}注意到这个更新是ID偏移和数据一起更新的,那么这个操作靠什么来保证:原子性。 你的数据库不提供原子性?后文略有提及。这里是更新成功了。如果更新的时候,节点挂了,那么库里或者log里的id偏移不写,数据也不处理,等节点恢复,就可以放心去同步,然后加入集群玩耍了。所以说,要保证数据仅处理一次,还是挺困难的吧?上面的保障“仅处理一次”这个语义的实现有什么问题呢?性能问题。这里已经使用了batch策略来减少到库或磁盘的Round-Trip Time,那么这里的性能问题是什么呢?考虑一下,采用master-master架构来保证主节点的可用性,但是一个主节点失败了,到另一个主节点主持工作,是需要时间的。假设从节点正在同步,啪!主节点挂了!因为要保证仅处理一次的语义,所以原子性发挥作用,失败,回滚,然后从主节点拉失败的数据(你不能就近更新,因为这批数据可能已经变化了,或者你根本没缓存本批数据),结果是什么呢?老主节点挂了, 新的主节点还没启动,所以这次事务就卡在这里,直到数据同步的源——主节点可以响应请求。如果不考虑性能,就此作罢,这也不是什么大事。你似乎意犹未尽?来吧,看看“银弹”是什么?Opaque-Transaction现在,我们来追求这样一种效果:某条数据在一批数据中(这批数据对应着一个事务),很可能会失败,但是它会在另一批数据中成功。 换句话说,一批数据的事务ID一定相同。来看看例子吧,老数据不变,只是多了个字段:prevReach。// 老数据{transactionId:3urlId:99//注意这里多了个字段,表示之前的reach的值prevReach:2reach:3}// 新到数据{transactionId:4urlId:99reach:5}这种情况,新事务的ID更大、更靠后,表明新事务可以执行,还等什么,直接更新,更新后数据如下:// 新到数据{transactionId:4urlId:99//注意这里更新为之前的值prevReach:3//3 + 5 = 8reach:8}现在来看下另外的情况:// 老数据{transactionId:3urlId:99prevReach:2reach:3}// 新到数据{//注意事务ID为3,和老数据中的事务ID相同transactionId:3urlId:99reach:5}这种情况怎么处理?是跳过吗?因为新数据的事务ID和库里或者log里的事务ID相同,按事务要求这次数据应该已经处理过了,跳过?不,这种事不能靠猜的,想想我们有的几个性质,其中关键一点就是:给定一批数据,它们所属的事务ID相同。仔细体会下,上面那句话和下面这句话的差别: 给定一个事务ID,任何时候,其所关联的那批数据相同。我们应该这么做,考虑到新到数据的事务ID和存储中的事务ID一致,所以这批数据可能被分别或者异步处理了,但是,这批数据对应的事务ID永远是同一个,那么,即使这批数据中的A部分先处理了,由于大家都是一个事务ID,那么A部分的前值是可靠的。所以,我们将依靠prevReach而不是Reach的值来更新:// 更新后数据{transactionId:3urlId:99//这个值不变prevReach:2//2 + 5 = 7reach:7}你发现了什么呢?不同的事务ID,导致了不同的值:1:当事务ID为4,大于存储中的事务ID3,Reach更新为3+5 = 8.2:当事务ID为3,等于存储中的事务ID3,Reach更新为2+5 = 7.这就是Opaque Transaction.这种事务能力是最强的了,可以保证事务异步提交。所以不用担心被卡住了,如果说集群中:Transaction:数据是分批处理的,每个事务ID对应一批确定、相同的数据.保证事务ID的产生是强有序的.保证分批的数据不重复、不遗漏.如果事务失败,数据源丢失,那么后续事务就卡住直到数据源恢复.Opaque-Transaction:数据是分批处理的,每批数据有确定而唯一的事务ID.保证事务ID的产生是强有序的.保证分批的数据不重复、不遗漏.如果事务失败,数据源丢失,不影响后续事务,除非后续事务的数据源也丢了.其实这个全局ID的设计也是门艺术:冗余关联表的ID,以减少join,做到O(1)取ID.冗余日期(long型)字段,以避免order by.冗余过滤字段,以避免无二级索引(HBase)的尴尬.存储mod-hash的值,以方便分库、分表后,应用层的数据路由书写.这个内容也太多,话题也太大,就不在此展开了。你现在知道twitter的snowflake生成全局唯一且有序的ID的重要性了。两阶段提交现在用zookeeper来做两阶段提交已经是入门级技术,所以也不展开了。如果你的数据库不支持原子操作,那么考虑两阶段提交吧。如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。结语To be continued. ...

November 21, 2018 · 2 min · jiezi

从程序员的角度深入理解MySQL

数据库基本原理第一,数据库的组成:存储 + 实例不必多说,数据当然需要存储;存储了还不够,显然需要提供程序对存储的操作进行封装,对外提供增删改查的API,即实例。一个存储,可以对应多个实例,这将提高这个存储的负载能力以及高可用;多个存储可以分布在不同的机房、地域,将实现容灾。第二,按Block or Page读取数据用大腿想也知道,数据库不可能按行读取数据(Why? ? ^_^)。实质上,数据库,如Oracle/MySQL,都是基于固定大小(比如16K)的物理块(Block or Page,我这里就不区分统一称为Block)来实现调度和管理的。要知道Block是数据库的概念,如何对应到文件系统呢?显然需要指出“这个Block的地址在哪里”,当查找到地址后,读取固定大小的数据就相当于完成了Block的读取了。数据库很聪明的,它不会仅仅只读取需要读取的Block,它还会替我们把附近的Block块都读取加载至内存。实际上,这是为了减少IO次数,提高命中率。事实上,一个Block块的附近Block也是热点数据,这种处理方式很有必要!第三,磁盘IO是数据库的性能瓶颈毫无疑问,数据在磁盘上,少不了磁盘IO。什么磁头旋转,定位磁道,寻址的过程,就不说了,我们是程序员,也管不了这些。但是这个过程确实是非常耗时的,和内存读取不是一个数量级,所以后来出现了很多方式来减少IO,提升数据库性能。比如,增加内存,让数据库把数据更多的加载至内存。内存虽好,但也不能滥用,为什么这么说呢?假设数据库中有100G数据,如果都加载至内存,也就说数据库要管理100G磁盘数据+100G内存数据,你说累不累?(数据库要处理磁盘和内存的映射关系,数据的同步,还要对内存数据进行清理,如果涉及数据库事务,又是一系列复杂操作……)不过这里需要指出的是,为了加快内存查找速度,数据库一般对内存进行HASH存放。比如,利用索引,索引相比内存,是一个性价比非常高的东西,后文详细介绍MySQL的索引原理。比如,利用性能更好的磁盘…(和咱们就没关系呢)第四,提出一些问题思考下:为什么我们说利用delete删除一个表的数据较trancate一个表要慢?【一个按行查找删除,多费劲;一个基于Block的体系结构删除】为什么我们说要小表驱动大表?【小表驱动大表会快?什么鬼?MN和NM不是一样的么?有鬼的地方,就有索引!】探索MySQL索引背后的原理对于绝大数的应用系统,读写比例在10:1,甚至100:1,而且insert/update很难出现性能问题,遇到最多的,最棘手的就是select了,select优化是重中之重,显然少不了索引!说起MySQL的索引,我们会冒出很多这些东西:BTree索引/B+Tree索引/Hash索引/聚集索引/非聚集索引…这么多,晕头!索引到底是什么,想解决什么问题?老生常谈了,官网说MySQL索引是一种数据结构,索引的目的就是为了提高查询效率。说白了,不使用索引的话,磁盘IO次数比较多!要想减少磁盘IO次数,怎么办?我们想通过不断缩小想要获取的数据的范围来筛选出最终想要的结果,把每次查找数据的磁盘IO次数控制在一个很小的数量级,最好是常数数量级。为了应对上述问题,B+Tree索引出来了!Hello,B+Tree在MySQL中,不同存储引擎对索引的实现方式是不同的,这里将重点分析MyISAM和Innodb。我们知道对于MyISAM引擎而言,数据文件和索引文件是分离的。从图中也可以看出,通过索引查找到后,就得到了数据的物理地址,然后根据地址定位数据文件中的记录即可。这种方式也叫"非聚集索引"。而对于Innodb引擎而言,数据文件本身是索引文件!通俗点说,叶子节点上,MyISAM存储的是记录的物理地址,而Innodb上存储的是数据内容,这种方式即"聚集索引"。另外一点需要注意的是,对于Innodb而言,主键索引中叶子节点存储的是数据内容,而普通索引的叶子节点中存储的是主键值!也就是说,对于Innodb的普通索引字段查找,先通过普通索引的B+Tree查找到主键后,然后通过主键索引的B+Tree进行查找。从这里你可以看出,对于Innodb而言,主键的建立非常重要!而对于MyISAM而言,主键索引和普通索引仅仅的区别在于主键只需要查找到一条记录即可停止,而普通索引允许重复,找到一条记录后需要继续查找,在结构上没有区别,如上图所示。想利用索引,就得“干净”什么叫“干净”?就是不要让索引参与计算!比如在索引上应用函数,很可能导致索引失效。为什么呢?其实不用想,B+Tree上存储的是数据,要比较的话,需要把所有的数据都应用上函数,显然成本太大。想建立索引,看看区分度索引虽然物美价廉,但是也别乱来。count(distinct col) / count(*)可以算一下col的区分度,显然对于主键而言,就是1。区分度太低的话,可以考虑下,是否还有必要建立索引呢?Hash索引这里并不是要深入分析Hash索引,而是要说明一下Hash的思想真是无处不在!在MySQL的Memory存储引擎中,存在hash函数,给一个key,通过hash函数进行计算得到地址,所以通常情况下,hash索引查找,会非常快,O(1)的速度。但是也存在hash冲突,和HashMap一样,通过单链表的形式解决。思考下,hash索引是否支持范围查询呢?显然是不支持的,它只能给一个KEY去查找。就如同HashMap一样,查找key包含"zhangfengzhe"的,会很快么?在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多SQL优化神器:explainSQL优化的场景很多,网上的技巧也很多,完全记不住!要想彻底解决这个问题,我想只有把索引背后的数据结构和原理做适当的理解,遇到书写SQL或者SQL慢查询的时候,我们有基础去分析,再利用好explain工具去验证,就应该问题不大呢。explain查询的结果,可以告诉你哪些索引正在被使用,表是如何被扫描的等等。这里我将演示个Demo。数据表student:注意复合索引(age,address)符合最左前缀匹配复合索引失效OK,到这里,准备结束了,查询容易,优化不易,且写且珍惜!

November 16, 2018 · 1 min · jiezi

利用springboot创建多模块项目

本文旨在用最通俗的语言讲述最枯燥的基本知识最近要对一个不大不小的项目进行重构,用spring觉得太过于繁琐,用cloud又有觉得过于庞大,维护的人手不够;权衡之下,最终选了springboot作为架子,但是因为项目涉及的业务模块较多,各个模块之间的业务交流不是很多,相对独立,因此想着把项目做成多模块的形式,模块之间可以独立部署,又可以互相调用,满足需求,故而花了点时间,搭了个springboot多模块的架子。文章提纲:多模块的创建关键配置温馨提示1. 根模块的创建springboot的多模块项目构建主要有以下步骤:父模块的创建和设置:打开idea-》选择Create New Project-》spring initialize-》填写项目名称-》next-》next-》完成父模块的创建。打开父模块的pom。把package的值改为pom。子模块的创建和设置:在创建好的父模块中右键-》New-》module-》spring initialize-》填写项目名称-》选择项目中需要的部件-》next-》完成父模块的创建。按照步骤1,创建其它模块在父模块的pom中,增加modules节点,把所有子模块加入到父模块中。 <!–引入多模块–> <modules> <module>module-one</module> <module>module-two</module> </modules>模块间的互相调用在需要调用其它模块的模块的pom文件中,增加对其它模块的依赖即可。<dependency> <groupId>com.example</groupId> <artifactId>module-one</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>由于项目构建都是用idea完成的,一个个截图的方式可能有些看管不能看清楚,因此在此选择用视频的方式,具体过程请看下方视频:点我查看视频教程:《利用springboot创建多模块项目》2. 关键配置看完视频之后,作者会发现,构建一个springboot多模块项目真的太简单了,只需要做好几个关键地方的配置就可以了.父模块的src,直接删掉父模块的pom文件中,打包方式改成pom.子模块的创建要在父模块下以module的形式创建子模块创建成功之后,在父模块中增加子模块的module模块之间的相关关系,用依赖来表示。3. 温馨提示文章仅讲述springboot创建多模块,搭建一个多模块架子,并未对其它组件进行集成,有需要的读者根据自己的需求,在创建模块的时候,选择需要的组件即可。对于多个模块共同的依赖,在父pom中设置即可。对于多模块项目的打包发布,当需要构建某个模块发布时,选择父pom构建,install -pl open-api -am觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能

November 16, 2018 · 1 min · jiezi

Spring是如何处理注解的

如果你看到了注解,那么一定有什么代码在什么地方处理了它.Alan Hohn我教Java课程时强调的一点是注解是惰性的。换句话说,它们只是标记,可能具有某些属性,但没有自己的行为。因此,每当你在一段Java代码上看到一个注解时,就意味着必须有一些其他的Java代码来寻找那个注解并包含真正的智能来做一些有用的东西。不幸的是,这种推理的问题在于,确切地确定哪一段代码正在处理注解是非常困难的,特别是如果它在库中。处理注解的代码可能会令人困惑,因为它使用反射并且必须以非常抽象的方式编写。所以我认为值得看看一个做得很好的例子来看看它是如何运行的。我们详细研究一下 Spring 框架中的 InitDestroyAnnotationBeanPostProcessor 类是如何工作的。选择这个,因为它相对简单,只做了一些相对容易解释的事情, 碰巧和我手头的工作相关。Spring Bean 的后处理首先,我想首先解释一下 Spring 的用途。Spring 框架所做的一件事就是“依赖注入”。这改变了我们以往用代码将模块串在一起的方式。例如,假设我们编写了一些需要连接数据库的应用程序逻辑, 但并想将提供该连接的特定硬类编码到应用程序逻辑中,我们可以在构造函数或setter方法中将其表示为依赖项:class MyApplication { private DataConnection data; … public void setData(DataConnection data) { this.data = data; } …}当然,如果想的话, 我们可以自己编写一个简单的库完成这种依赖注入,从而避免添加对 Spring 的依赖项。但是如果我们在编写一个复杂的应用程序, 想将各模块连接在一起,那么Spring可以非常方便。既然没有什么神秘的,如果我们要让 Spring 为我们注入这些依赖,那么就会有一个权衡。Spring 需要“知道”依赖关系以及应用程序中的类和对象。Spring 处理这个问题的方法多是由 Spring 框架对对象进行实例化; 从而可以在称为"应用程序上下文"的大数据结构中跟踪管理这此对象。后处理和初始化而且这里是 InitDestroyBeanPostProcessor 进入的地方 。如果 Spring 要处理实例化,那么在对象实例化完成之后,但是在应用程序开始真正的运行之前,需要进行一些“额外工作”。需要做的一件“额外工作”就是调用对象来告诉他们什么时候完全设置好,这样他们就可以进行任何需要的额外初始化。如果我们使用“setter”注入,如上所述,便通过调用setXxx() 方法注入依赖项,这一点尤其重要,因为在调用对象的构造函数时这些依赖项并不可用。所以 Spring 需要允许用户指定在初始化对象后才应该调用的某个方法的名称。Spring 一直支持使用XML配置文件来定义由 Spring 来实例化的对象,在这种情况下,有一个 ‘init-method’ 属性可以用来指定初始化的方法。显然,在这种情况下,它仍然需要反射来实际查找并调用该方法。自Java 5起, 增加了注解,所以Spring 也支持带注解的标记方法,将它们标识为Spring应该实例化的对象,识别需要注入的依赖项,以及识别应该调用的初始化和销毁方法。最后一项 InitDestroyBeanPostProcessor 由其子类或其中一个子类处理。后处理器是一种特殊的对象,由Spring实例化,实现后处理器接口。因为它实现了这个接口,所以Spring会在每个Spring实例化的对象上调用一个方法,允许它修改甚至替换该对象。这是Spring采用模块化架构方法的一部分,可以更轻松地扩展功能。这是怎么运作的?事实上, JSR-250 确定了一些“常见”注解,包括 @PostConstruct, 用于标记初始化方法,@PreDestroy 注解, 用于注解销毁方法的。不同的是,InitDestroyBeanPostProcessor 被设计成可以处理任何注解集,因此它提供了识别注解的方法: public void setInitAnnotationType(Class<? extends Annotation> initAnnotationType) { this.initAnnotationType = initAnnotationType; }… public void setDestroyAnnotationType(Class<? extends Annotation> destroyAnnotationType) { this.destroyAnnotationType = destroyAnnotationType; }请注意,这些是普通的 setter 方法,因此这个对象本身可以使用 Spring 进行设置。就我而言,我使用Spring 的 StaticApplicationContext,见我以前的文章。一旦 Spring 实例化了各种对象并注入了所有依赖项,它就会在所有后处理器上为每个对象调用 postProcessBeforeInitialization 方法 。这使后处理器有机会在初始化之前修改或替换对象。因为已经注入了依赖项,所以这是 InitDestroyAnnotationBeanPostProcessor 调用初始化方法的地方。 LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass()); try { metadata.invokeInitMethods(bean, beanName); }由于我们对代码如何处理注解感兴趣,我们感兴趣 findLifecycleMetadata() 方法,因为这是对类进行检查的地方。该方法检查缓存,该缓存用于避免执行超过必要的反射,因为它可能很昂贵。如果尚未检查该类,则调用 buildLifecycleMetadata() 方法。该方法的内容如下:ReflectionUtils.doWithLocalMethods(targetClass, new ReflectionUtils.MethodCallback() { @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { if (initAnnotationType != null) { if (method.getAnnotation(initAnnotationType) != null) { LifecycleElement element = new LifecycleElement(method); currInitMethods.add(element); } } … }});这里 ReflectionUtils 是一个方便的类,简化了反射的使用。除此之外,它还将经过反射的众多已检查异常转换为未经检查的异常(?),从而使事情变得更容易。此特定方法仅迭代本地方法(即不是继承的方法),并为每个方法调用回调。完成所有设置之后,检查注解的部分非常无聊; 它只是调用Java反射方法来检查注解,如果找到它,则将该方法存储为初始化方法。总结事实上,这里最终发生的事情很简单,这就是我在教反射时所要做的事情。调试使用注解来控制行为的代码可能具有挑战性,因为从外部来看它非常不透明,所以很难想象发生了什么(或者没有发生)和什么时候发生。但最终,正在发生的事情只是Java代码; 它可能不会立即显现出代码的位置,但它就在那里。 ...

November 12, 2018 · 1 min · jiezi

什么样的经历,才能领悟成为架构师?

最近我发现,无论是博客也好,还是我写的技术专栏也好,经常会收到很多朋友的留言,留言的内容除了讨论技术问题以外,问的最多的,莫过于职业生涯规划相关的了。例如:我刚毕业,如何入行Java开发这一行业?干了几年Java开发了,感觉进入瓶颈期,不知道下一步该怎么走了?大家做生意的做生意,转管理的也不在少数,我还需要坚持做技术么?问题虽然五花八门,但是总结下来就是一个:Java工程师的职业道路该如何走?我尝试着从各个角度回答大家问题,包括夯实基础,并学习其他例如学python、大数据等其他技能。但是这个回答,可能略显乏力,毕竟我提供的更多的是战术方向,即具体的操作方法。可是战略方向,比如把时间线拉长一点,五年,十年该如何规划你的职业生涯呢?我想,这个问题最好的答案,还是需要那些有历经的过来人,才最有资格和大家谈论这个话题。特别凑巧,前几天在微信上与一位前同事叙旧群聊时,他对自己在通往Java架构之路上做了自己的独特分享。他是如何成为一名成功的Java架构师,甚至公司高管和的历程。瞬间我眼前一亮,这不就是包括我在内以及广大同行们所需要的滋补品吗?话不多说,大家请往下看。开场白在同大家分享之前,先让我唠叨两句。虽然工作的事务不同,技术点不同,但是大家都有一个共同的目标,即成为所谓的人生赢家。重要的是,我们需要关注他这快十年的人生路线是如何规划和走下来的,中间有什么可以学习之处?希望你看了他职业生涯经历以后,或许能够对你的当前的职业规划有所帮助,人生有所启迪。故事,就要从头开始,那才精彩。非科班出身的Java架构师——王贤王贤,89年,工作8年,某一线互联网架构师 P7说起王贤,我第一印象就是他的语速很快,说话很有条理,平常人需要掰扯 60 分钟的事情,到他这里,顶多30分钟给你安排的很明白。对于王贤而言,他人生中最不自信的一件事就是学历了。大专出身的他,在找工作这件事上,并没有十分大的优势。而做为大专出身,王贤的每一次机会都来之不易。10 年大专毕业前夕,王贤拉着一个箱子就去了魔都【上海】。「破釜沉舟,没想着怎么回去」,带着自己的简历,王贤跑遍了上海大大小小的互联网公司,最后终于在一家小型互联网公司急招Android工程师的时候趁虚而入,成为了一名初级Android工程师。【大专学的计算机专业:C/C++/Java基础;没什么项目经验】能够拿到这个机会,王贤十分珍惜。所以当遇到「你去开发一个 app,公司暂时不会给你提供额外的资源」的要求时,王贤迎难而上。据王贤当时回忆,【当时在接这个项目的时候,也是硬着头皮,每天都在琢磨着,怎么样才能把这个 app 开发出来。】这对于王贤来说,是他毕业后人生中的第一桶金的项目。为了能做出这个项目,王贤浏览了很多技术网站,学习和初步认识了很多技术相关,最后终于倒腾出一个还不错的版本。【有这样的毅力,值得我们学习】。我依然清楚的记得我第一次见王贤的时候,他就给我留下来深刻的印象。当时在12年的一次技术交流会上认识的,当时我们交谈了很久,也聊了很多。我们各自也聊了自己在技术上的见解与感悟,当时也互留了联系方式。【王贤十分直率地说出了自己独自一人闯荡的心路历程,再看一下我自己,也深有体会。】我们正式在一起工作的是在2015年,当时我正加班完准备下班回家,就接到了王贤打来的一通电话【最近离职了,再找工作】。当时我也没多想,【毕竟王贤为人不错,肯学习肯努力肯干】就跟他说:“最近我们金服在招人,你可以来试试”。【面试的历程还是比较艰辛的,毕竟学历和技术摆在那里】。最后还是运气好还是等到了金服的offer。在新的公司,王贤除了接触项目上的一些事情外,也慢慢承担了一些项目沟通的工作。王贤自己知道自己的技术还是不行,需要学习的东西还有很多,他自己也明白,不努力、技术跟不上就会被淘汰。所以便每晚的加班到最后一个离开,也抽空余时间学习有关架构的相关技术点【购买了很多架构书籍,视频】。俗话说:“士别三日,则刮目相看”。不到一年的时间,王贤技术长进不少,大家都知道,这是他靠自己的辛勤汗水,每日每夜的加班熬出来的。“对我来说,如果工作有什么进步的诀窍的话,大概就是保持一颗刨根究底的心去做项目,就要孜孜不倦的学习新技术”,王贤如此总结自己能在工作中不断进步的经验。在今年18 年三月,王贤离开了金服,以50 万的年薪加入了目前势头最猛的某互联网公司,定级 P7 。又开始了新的征途。【技术过硬,还怕学历不行?】总结看了以上的经历以后,结合我个人的其他经历。我觉得,可以把这提炼成为三个关键字:学习,人脉,时间。三个关键字按照重要性从高到底排序,他们决定了一个架构师,甚至普通人的进阶的途径和方法。1:学习你可能觉得,以上的经历,很像流水账,貌似没有什么太出彩的地方。无非就是,跳跳槽,找找关系,去个牛叉的公司就行了。但是,仔细想想,好像没有这么简单吧。敢问:假设他肚子里面没有点墨水,即便有人推荐,也会有今天的成就么?假设没有对于未知事物的好奇心,他会跳出自己的舒适区,寻找新的挑战么?所以,永不倦怠的学习,才是成功的基石。甭管你在哪一个行业,别告诉自己学的都足够了,永远天外有天,人外有人。2:人脉这个不用多说,大家都明白,多认识朋友。以上的故事经历中,毫无疑问,他就是通过朋友,熟人介绍进入一家新公司。所以,朋友关系网是多么的重要。换句话说,我可以通过我现有的这些朋友,联系上名企中的任何一个人,你会发现,这太扯了,居然还能这样操作。同样,在人脉的背后,其实隐藏着另外一话题,就是所谓的情商。从人脉的角度来说情商,简单点来讲就是:如何做一个不让别人讨厌的人。只有不让人讨厌,大家相谈甚欢,才会有更深一层的了解,才会建立联系,最终成为同事,或者朋友,才会有人脉。3:时间下面有这么一个公式,可能有些朋友曾经见过。它告诉你,若每天比前一天进步0.01,非常微小的进步。但是一年累积下来,你会比一年前的你牛叉37.8倍。那十年呢,二十年呢?其实,这就是时间的力量。结尾最后,送大家一句话,我是在某个网站上看到的:再牛 x 的梦想也抵不住傻 x 似的坚持!还有,别走。我没有办法助你成功,那是洗脑工程师做的事儿。我倒是有这么个晋升渠道,它可能会帮你完成那每天的0.01的积累。以上这些技术如何学习?有没有免费的学习资料 ?点击右边此链接免费获取:https://jq.qq.com/?_wv=1027&k…

November 2, 2018 · 1 min · jiezi

单手撸了个springboot+mybatis+druid

本文旨在用最通俗的语言讲述最枯燥的基本知识最近身边的程序员掀起了学习springboot的热潮,说什么学会了springboot在大街上就可以横着走、什么有了springboot妈妈再也不担心我的编程了、什么BAT都喜欢的框架…听得作者那个心痒痒的,于是找了个时间,下载了个idea来玩一波springboot,对了…用springboot最好用idea,如果你还在用eclipse,删了吧。在这里解释一下为什么是springboot+mybatis+druid,是因为作者认为但凡任何一个有灵魂的项目,都少不了数据库,作者不喜欢用JPA那种混SQL的语法,因此选了mybatis,而Druid是阿里系(真香~)的一种数据库连接池框架,在上一个项目作者用的屡试不爽,因此打算继续用,为啥屡试不爽?看文末吧。文章提纲:创建springboot工程配置pom.xml配置数据源设置mybatishello world设置Druid监控配置1. 创建springboot工程只要你有idea,创建一个springboot工程,就跟捏死一个蚂蚁一样简单,因为idea里深度集成了对springboot项目的支持,你直接不停的next到最后,它就会帮你创建出一个springboot工程。首先打开idea->Create New project->选择项目类型:这里选择spring initializr,然后next。创建项目,填写项目名称,注意:不要能驼峰写法,可以用中横线,填写完毕后继续next。选择依赖,所谓依赖就是你在设计这个项目的框架时,分析这个项目需要用到哪些jar或者组件,比如缓存要用到Redis,数据库要用到MySQL等…这里因为演示项目,就不选择太多其它乱七八糟的依赖了。选好依赖之后,又是一路狂点next,直到最后finish一下,一个springboot项目就创建好了。2. 配置pom.xml想想,我们需要哪些jar?数据库要用到mybatis,数据库连接池要用到Druid、MySQL桥接器要用到mysql-connector,因此要maven仓库(点我去仓库)中找到搜索这些pom加进去。注意,mybatis要用mybatis-spring-boot-starter。 <!– https://mvnrepository.com/artifact/mysql/mysql-connector-java –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <!– https://mvnrepository.com/artifact/com.alibaba/druid –> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <!– https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter –> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency>把上面这些pom放到pom.xml的dependencies中细心的老铁会发现,MySQL的version里的内容是红色的,这是什么原因呢?这是因为我们引入pom时,这些版本的jar在本地maven仓库还没有,而Druid的pom里的version没有显示红色,是因为之前的项目用到了这个版本的Druid,已经被下载到本地Maven仓库里了。因此我们需要把本地没有的jar下载到本地仓库,右键pom.xml弹出菜单,选择Maven,弹出菜单选择reimportReimport过程中再idea底部会有进度条显示,等进度条消失,在观察pom.xml,红色已经消失,说明依赖已经装备完成。配置数据源接下来就是要多springboot项目做一个全局配置,默认会在src->main->resource目录下生产空白文件application.properties,作者喜欢用yml因此直接改名成yml即可。首先是数据源的配置,下面是一份数据源的配置,每个参数的解释都写了注释,因此读者可以直接复制一下内容进去,只需要改一下url、username、passwordspring: #profiles: dev messages: basename: i18n/Messages,i18n/Pages datasource: type: com.alibaba.druid.pool.DruidDataSource # 配置当前要使用的数据源的操作类型 driver-class-name: org.gjt.mm.mysql.Driver # 配置MySQL的驱动程序类 url: jdbc:mysql://localhost:3306/wkt_stat # 数据库连接地址 username: root # 数据库用户名 password: root # 数据库连接密码 dbcp2: # 进行数据库连接池的配置 min-idle: 5 # 数据库连接池的最小维持连接数 initial-size: 5 # 初始化提供的连接数 max-total: 5 # 最大的连接数 max-wait-millis: 200 # 等待连接获取的最大超时时间4. 设置mybatis继续在application.yml中设置mybatis,mybatis的配置也简单,主要是为了设置mybatis的配置文件已经mapper文件所在。首先在resource目录下创建一个mybatis-config.xml文件,文件内容为:<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE configuration PUBLIC “-//mybatis.org//DTD Config 3.0//EN” “http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <mappers> </mappers></configuration>在main包中的根目录下创建一个存放mapper实体的资源文件,在resource文件下创建一个文件夹mapper用来存放mapper的xml文件。配置好资源文件路径之后,就可以在application.yml中加入mybatis的配置了,如下是一个mybatis的配置内容:#mybatis的mapper配置文件mybatis: config-location: classpath:mybatis-config.xml # mybatis配置文件所在路径 #mapper-locations: classpath:mapper/.xml # 所有的mapper映射文件 type-aliases-package: com.becl.dao.mapper # 定义所有操作类的别名所在包debug: true最终application.yml的内容如下图:此时需要有一个数据库表来做测试,我们在数据库创建一个表,并且插入一条数据:CREATE TABLE Memeber (id int(11) NOT NULL AUTO_INCREMENT ,name varchar(255) NULL ,PRIMARY KEY (id));INSERT INTO memeber VALUES(1,“jas”)在mapper包中创建Memeber的mapper接口:public interface MemeberMapper { /** * 根据ID获取记录 * @param id * @return / public Map findObjectById(Integer id);}在resource中的mapper文件夹创建memberMapper.xml,并且在mapper中增加一个findObjectById的SQL查询语句。<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--映射文件配置,namespace指向接口–><mapper namespace=“com.example.mybatisanddruid.mapper.MemeberMapper”>#根据ID查询记录<select id=“findObjectById” parameterType=“Integer” resultType=“Map”> select * from memeber where id = #{value} </select></mapper>5. hello world走到这一步,基本上已经是大功告成了,我们来写一个测试类试试,在根目录创建一个controller的包,在包中创建一个Java类,如下:@Controller@RequestMapping("/test”)public class TestController { @Resource private MemeberMapper memeberMapper=null; @RequestMapping("/one”) @ResponseBody public Map testdb(){ return memeberMapper.findObjectById(1); }}创建完之后,我们运行项目,找到启动类MybatisAndDruidApplication右键run,发现报错,提示没有扫描到mapper包,为什么呢?那是mapper需要手动在启动类中加入:@MapperScan(“com.example.mybatisanddruid.mapper”)这样启动类就变成:@SpringBootApplication@MapperScan(“com.example.mybatisanddruid.mapper”)public class MybatisAndDruidApplication { public static void main(String[] args) { SpringApplication.run(MybatisAndDruidApplication.class, args); }}再次运行,没有报错,在浏览器输入:http://localhost:8888/test/one输出了ID为1的记录:{“name”:“jas”,“id”:1}由此可见,springboot-mybatis已经搭建成功,此时有人会问,那Druid呢?设置Druid监控配置druid的使用需要做一些配置,现在我们来在根目录下创建一个包config,在config包中间创建一个叫做DruidConfig.java,并且在里写入下面的内容:@Configurationpublic class DruidConfig { @Bean public ServletRegistrationBean druidServlet() { // 主要实现WEB监控的配置处理 ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), “/druid/”); // 进行druid监控的配置处理操作 servletRegistrationBean.addInitParameter(“allow”, “127.0.0.1,192.168.1.159”); // 白名单 servletRegistrationBean.addInitParameter(“deny”, “192.168.1.200”); // 黑名单 servletRegistrationBean.addInitParameter(“loginUsername”, “stat”); // 用户名 servletRegistrationBean.addInitParameter(“loginPassword”, “Wkt_sTat_1031”); // 密码 servletRegistrationBean.addInitParameter(“resetEnable”, “false”); // 是否可以重置数据源 return servletRegistrationBean ; } @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean() ; filterRegistrationBean.setFilter(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/"); // 所有请求进行监控处理 filterRegistrationBean.addInitParameter(“exclusions”, “.js,.gif,.jpg,.css,/druid/*”); return filterRegistrationBean ; } @Bean @ConfigurationProperties(prefix = “spring.datasource”) public DataSource druidDataSource() { return new DruidDataSource(); } }配置中的内容就不一一细讲, 有兴趣的直接百度一下druid就出来很多答案了,现在重新运行一下项目,运行成功之后,在浏览器中输入:http://localhost:8888/druid这时候,druid监控平台就出现了此时我们输入在DruidConfig中设置的loginUsername和loginPassword点击登录,一个完整的druid监控管理平台就呈现在我们啦Druid非常强大,在这里你可以查看SQL的执行情况、慢SQL、API请求情况等,根据这些可以做一些性能的调优,至于详细的用法,就靠大家自行学习啦如果有老铁需要项目源码,请加我微信:sisi-ceo。觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能 ...

November 1, 2018 · 2 min · jiezi

???? Apiggs - 非侵入的RestDoc文档生成工具

前言程序员一直以来都有一个烦恼,只想写代码,不想写文档。代码就表达了我的思想和灵魂。Python提出了一个方案,叫docstring,来试图解决这个问题。即编写代码,同时也能写出文档,保持代码和文档的一致。docstring说白了就是一堆代码中的注释。Python的docstring可以通过help函数直接输出一份有格式的文档,本工具的思想与此类似。代码即文档Apiggs是一个非侵入的RestDoc文档生成工具。工具通过分析代码和注释,获取文档信息,生成RestDoc文档。引入插件apiggs-gradle-pluginapiggs-maven-plugin有这样一段代码/** * 欢迎使用Apiggs * @index 1 /@RestControllerpublic class GreetingController { private static final String template = “Hello, %s!”; private final AtomicLong counter = new AtomicLong(); /* * 示例接口 * @param name 名称 * @return */ @RequestMapping("/greeting") public Greeting greeting(@RequestParam(value=“name”, defaultValue=“apiggs”) String name) { return new Greeting(counter.incrementAndGet(), String.format(template, name)); }}运行插件gradle 运行 task:Tasks/documentation/apiggsmaven 运行compile生成文档在编译目录下生成apiggs文件夹,并生成三个文件:.json文件,可直接导入postman.adoc文件,Asciidoc源文件.html文件,源文件渲染结果,效果如下图想了解更多,请查看Wiki

October 30, 2018 · 1 min · jiezi

SpringCloud微服务部署

微服务的其中一个特点就是有许许多的粒度小(功能单一,比如用户管理,短信发送管理,邮件发送管理,文件管理等)、能独立部署、扩展、运行的小应用,可以称为api,也就是服务提供者。api之间可以相互调用,但更多的是供app调用,比如学生管理系统,它是面向用户的,是许许多多功能的集合体,它需要调用许多api完成业务功能,所以这学生管理系统可以称为app。eureka的作用传统的单体应用开发,就是将api和app的代码全部集成在一起,在同一个进程中运行,对应java web通俗的说就是全部打包在一个war中部署在一个tomcat中运行。而微服务的每个api,app都拥有自己的进程,也就是都有自己的tomcat,某个api挂了,不影响其他api和app运行。api是采取restfull风格暴漏出去的,所以app(api)调用api时,采取http://ip:端口这形式进行通讯。那么问题来了,如果有很多app都调用了某个api,如果api的ip或端口发生了更改,如果app中对api的ip和端口等信息是写在配置文件的,难道要通知每个app,说这个api的地址和端口变了,app要进行修改重新编译部署(这是举例子而已,实际情况有些企业对常量的配置可能写配置文件,也可能写数据库)。这太麻烦了,如果api的地址和端口有发生变化,app能及时获知自行变更,那就好了。还有个弊端,就是api是否挂了,也没法直观观察到。所以,如果在app和api之间增加个服务管理中心,api像服务管理中心注册信息,app从服务管理中心获取api的信息,api有个唯一标识,api有变更的时候通知服务管理中心,服务管理中心通知相关的app或者app定时从服务管理中心获取最新的api信息,同时服务管理中心具有很高的稳定性、可靠性。eureka就是为了这样的一个服务管理中心,下面是我根据自己的理解画的一张图。下面这张是官方的架构图1.Application Service 相当于服务提供者/api2.Application Client 相当于服务消费者/app3.Make Remote Call,其实就是实现服务的使用/比如httpClient,restTemplate4.us-east-1 Eureka 集群服务5.us-east-1c、us-east-1d、us-east-1e 就是具体的某个eurekaEureka:是纯正的 servlet 应用,需构建成war包部署使用了 Jersey 框架实现自身的 RESTful HTTP接口peer之间的同步与服务的注册全部通过 HTTP 协议实现定时任务(发送心跳、定时清理过期服务、节点同步等)通过 JDK 自带的 Timer 实现内存缓存使用Google的guava包实现eureka集群搭建和eureka类似功能的有zookeeper,etcd等。spring boot已经集成了eureka,所以我们可以像spring boot那样搭建环境,部署运行。我是在window10下使用eclipse学习的。准备工作。在修改hosts文件,在最后面加上(位置:C:WindowsSystem32driversetc)127.0.0.1 01.eureka.server 127.0.0.1 02.eureka.server 127.0.0.1 03.eureka.servereclipse下创建个普通的maven项目eureka-server。pom.xml <project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka服务端</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <pluginRepository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>启动类Application.javapackage com.fei.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }注意注解:@EnableEurekaServer,表明这是server服务配置文件application.propertieslogging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=8081 server.session-timeout=60 ########### spring.application.name=eureka-server-01 ####下面2个一定要false,因为这程序是要作为服务端但是jar中存在eureka-client.jar,所以要false,否则启动会报错的 #是否注册到eureka eureka.client.register-with-eureka=false #是否获取注册信息 eureka.client.fetch-registry=false #为了便于测试,取消eureka的保护模式,如果启动的话,比如api提供者关闭了,但是eureka仍然保留信息 eureka.server.enable-self-preservation=false #服务名称 eureka.instance.hostname=01.server.eureka #eureka的服务地址,/eureka是固定的 eureka.client.serviceUrl.defaultZone=http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/ 注意:eureka.client.serviceUrl.defaultZone的配置,如果是01,则填写02、03的地址和端口;如果是02,则填写01、03的地址和端口,也就是说让01,02,03这3个eureka服务能相互间同步数据,如果是01->02->03->01,则api提供者注册信息到01时,01会同步数据到02,但02不会同步到03,01也不会同步到03,也就是说03缺数据了。看源码,服务的注册信息不会被二次传播,看PeerAwareInstanceRegistryImpl.java启动01 eureka,然后修改application.properties,变为02 eureka的配置logging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=8082 server.session-timeout=60 ########### spring.application.name=eureka-server-02 ####下面2个一定要false,因为这程序是要作为服务端,但是jar中存在eureka-client.jar,所以要false,否则启动会报错的 #是否注册到eureka eureka.client.register-with-eureka=false #是否获取注册信息 eureka.client.fetch-registry=false #为了便于测试,取消eureka的保护模式,如果启动的话,比如api提供者关闭了,但是eureka仍然保留信息 eureka.server.enable-self-preservation=false #服务名称 eureka.instance.hostname=02.server.eureka #eureka的服务地址,/eureka是固定的 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/,http://03.server.eureka:8083/eureka/然后执行启动类,03也是一样的操作。浏览器访问http://01.server.eureka:8081/(或者http://02.server.eureka:8082/,或者http://03.server.eureka:8083/)看到api提供者创建个普通的maven项目eureka-api,该api是个用户服务提供者。采取spring boot开发模式pom.xml<project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka服务端</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 它和eureka 服务端,有个依赖是不一样的。application.propertieslogging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=9081 server.session-timeout=60 ########### spring.application.name=api-user-server #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname:${spring.aplication.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/Application.java package com.fei.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @EnableEurekaClient @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @EnableEurekaClient,不管是消费者还是提供者,对应eureka server来说都是客户端client写个普通的controller,UserProvider.java.提供个根据id获取用户信息的接口 package com.fei.springcloud.provider; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user”) public class UserProvider { @GetMapping(value="/find/{id}”) public String find(@PathVariable(“id”) String id,HttpServletRequest request){ //实际项目中,这里可以使用JSONObject,返回json字符串 //为了便于测试消费者app的负载均衡,返回服务端端口 String s = “张三”+” 服务端端口:"+request.getLocalPort(); return s; } } 执行Application.java,将application.properties的端口修改为9082,再次执行浏览器访问http://01.server.eureka:8081/Application就是文件中定义的spring.application.name=api-user-server,它会自动转为大写如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。app消费者在Spring Cloud Netflix中,使用Ribbon实现客户端负载均衡,使用Feign实现声明式HTTP客户端调用——即写得像本地函数调用一样.ribbo负载均衡的app消费者创建个普通的maven项目eureka-app-ribbo.pom.xml<project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-app-ribbo</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka消费者ribbo</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <pluginRepository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <!– 客户端负载均衡 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <!– eureka客户端 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <!– spring boot实现Java Web服务 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> application.propertieslogging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=7081 server.session-timeout=60 ########### spring.application.name=app-user #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname}${spring.application.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/,http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/UserController.java logging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=7081 server.session-timeout=60 ########### spring.application.name=app-user #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname} :${spring.application.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/ ,http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/ 使用restTemplate需要自己拼接url启动类Application.javapackage com.fei.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableEurekaClient @SpringBootApplication public class Application { @Bean //定义REST客户端,RestTemplate实例 @LoadBalanced //开启负债均衡的能力 RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }eureka服务浏览器访问http://127.0.0.1:7081/user/find看到信息“张三 服务端端口:9081”,刷新浏览器看到“张三 服务端端口:9082”,说明的确是有负载均衡。但是访问外网的时候,http://127.0.0.1:7081/user/test,也就是域名不在eureka注册过的,就不行了。以后再研究下如何解决。feign的app消费者feign可以写个接口,加上相关的注解,调用的时候,会自动拼接url,调用者就像调用本地接口一样的操作。Feign也用到ribbon,当你使用@ FeignClient,ribbon自动被应用。像ribbo创建个项目,或者直接在ribbo项目修改都OK。pom.xml 把ribbo的依赖修改为feign<project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-app-feign</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka消费者feign</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content /repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <pluginRepository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <!– Feign实现声明式HTTP客户端 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> <!– eureka客户端 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <!– spring boot实现Java Web服务 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>application.properties和上面一样logging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=7081 server.session-timeout=60 ########### spring.application.name=app-user #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka8081/eureka/,http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/增加个UserService.java接口类package com.fei.springcloud.service; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(“API-USER-SERVER”) public interface UserService { @GetMapping(value="/user/find/{id}”) String find(@PathVariable(“id”) String id); }UserController.javapackage com.fei.springcloud.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import com.fei.springcloud.service.UserService; @Controller @RequestMapping("/user”) public class UserController { @Autowired private UserService userService; @GetMapping(value = “/find”) @ResponseBody public String find() { //url中对应api提供者的名称,全大写 String s = userService.find(“123”); return s; } }浏览器访问http://127.0.0.1:7081/user/find,得到信息“张三 服务端端口:9081”,刷新,得到“张三 服务端端口:9082”,Feign也用到ribbon,当你使用@ FeignClient,ribbon自动被应用。所以也会负载均衡ribbo负载均衡策略选择AvailabilityFilteringRule:过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值) | 使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态RandomRule:随机选择一个serverBestAvailabl:选择一个最小的并发请求的server,逐个考察Server,如果Server被tripped了,则忽略RoundRobinRule:roundRobin方式轮询选择, 轮询index,选择index对应位置的serverWeightedResponseTimeRule:根据响应时间分配一个weight(权重),响应时间越长,weight越小,被选中的可能性越低RetryRule:对选定的负载均衡策略机上重试机制,在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的serverZoneAvoidanceRule:复合判断server所在区域的性能和server的可用性选择serverResponseTimeWeightedRule:作用同WeightedResponseTimeRule,二者作用是一样的,ResponseTimeWeightedRule后来改名为WeightedResponseTimeRule在app消费者的application.properties配置文件中加入#ribbo负载均衡策略配置,默认是依次轮询 API-USER-SERVER.ribbon.NFLoadBalancerRuleClassName=com. netflix.loadbalancer.RandomRule其中API-USER-SERVER是api服务提供者的服务名称,也就是说,可以给每个不同的api服务提供者配置不同的复制均衡策略,验证就不贴图了负载均衡策略在消费端配置的缺点在上面的例子中,ribbon的负载均衡是在消费端完成的。流程是这样的:提供者服务A集群,启动2个进程A1,A2,都注册到eureka,app消费端根据api服务者名称获取到A1,A2的具体连接地址,ribbon就对A1,A2进行负载均衡。缺点:1) 如果所有的app消费端的配置策略不好,导致绝大部分的请求都到A1,那A1的压力就大了。也就是说负载策略不是有api提供者所控制的了(这里就不说A1,A2所在的服务器哪个性能更好了,因为如果app/api都是在Docker中运行,k8s负责资源调配的话,可以认为每个服务的进程所在的docker配置是一样的,比如A服务对应的A1,A2系统环境是一致的,B服务对应的B1,B2,B3系统环境是一致的,只是所对应的宿主机服务器估计不一样而已)。2)如果api提供者需要开放给第三方公司的时候,总不能把A1,A2告诉第三方吧,说我们这A服务集群了,有A1,A2,你随意吧。我们实际项目中的做法,都是每个提供者服务都有自己的nginx管理自己的集群,然后把nginx的域名提供给app消费者即可。之所以每个服务提供者都有自己的nginx,是因为docker被k8s管控的时候,ip都是变化的,需要更新到nginx中。如果A,B都共用一个nginx,那A重构建部署的时候,A1,A2的ip变化了,需要更新到nginx中去,那如果导致nginx出现问题了,岂不是影响到B的使用了,所以A,B都有自己的nginx。那spring cloud没有解决方案了吗?有。spring cloud集成了zuul,zuul服务网关,不但提供了和nginx一样的反向代理功能,还提供了负载均衡、监控、过滤等功能,在后面学习zuul的时候,我们再学习。如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。docker+k8s实现的devOps平台(paas平台),构建好镜像后,部署的时候,k8s负责调控资源,将docker分配到不同的节点服务器,同时将docker的ip相关信息更新到nginx。这是自动化的,不需要开发人员手动操作docker,nginx配置。 ...

October 26, 2018 · 4 min · jiezi

eureka分布式框架demo(springboot、springcloud、Feign、zuul、Mybatis)

eureka分布式框架,内含以下模块:eureka server、zuul 网关、commons 工具包、pojos 实体类、base服务提供者、order 服务提供者、web 服务消费者eureka_demo项目介绍eureka分布式框架,内含以下模块:eureka serverzuul 网关commons 工具包pojos 实体类base 服务提供者(集成Mybatis)order 服务提供者(集成Mybatis)web 服务消费者软件架构eureka分布式框架、集成sringboot、springcloud、Mybatis使用说明创建库表,在项目doc目录下有对应的sql文件然后修改server_base和server_order中的数据库链接order服务中创建了两个启动文件,端口不一样,记得配置两个启动即可测试负载 (Intellij Idea)。启动顺序:eureka -> zuul -> base -> order -> web启动完成打开页面访问:http://localhost:7070/ 查看eureka管理界面,查看服务是否已经注册进来如下图所示:具体的请求方式,在web模块controller中都有注释!模块示例图因资料太多,后台关注我主页领取免费的学习资源(有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等)

October 26, 2018 · 1 min · jiezi

JVM 在遇到OOM(OutOfMemoryError)时生成Dump文件的三种方式

JVM 在遇到OOM(OutOfMemoryError)时生成Dump文件的三种方式,以及如何使用Eclips MemoryAnalyzer(MAT)插件进行堆内存分析。方法一:jmap -dump:format=b,file=文件名 [pid]例如:jmap -dump:format=b,file=/usr/local/base/02.hprof 12942方法二:让JVM在遇到OOM(OutOfMemoryError)时生成Dump文件,需要配置一些信息-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base比如:我用eclipse配置一下。如下图所示:方法三:使用 jcmd 命令生成 dump 文件jcmd <pid> GC.heap_dump d:dumpheap.hprof此方法没有经过博主的测试。分析:dump文件可以通过MemoryAnalyzer(MAT)分析查看,可以查看dump时对象数量,内存占用,线程情况等。我们现在来安装一下eclipse MAT插件打开Help -> new install software名字可以随便起,插件地址:http://archive.eclipse.org/ma…剩下的就不做多的介绍了,安装完成后,我们来使用方法二来营造一个内存溢出的例子:比如我写的Java内存溢出程序是:import java.util.ArrayList;import java.util.List;public class OOM {public static void main(String[] args) { List<Object> list = new ArrayList<>(); // 创建n个1M大小的数组,耗尽内存 for (int i = 0; i < 10000; i++) { list.add(new byte[1024 * 1024]); } } }控制台打印:java.lang.OutOfMemoryError: Java heap spaceDumping heap to /Users/sun/Documents/java_pid31782.hprof …Heap dump file created [1943907998 bytes in 5.035 secs]下边我们切换eclipse视图到 Memory Analysis,点击File -> Open Heap Dump,选择生成的hprof文件,如下图所示打开完成后,如下所示:圈起来的这三个是常用的分析方式现在我们点击 Leak Suspects 来看一下点击 Histogram 来看一下byte 数组创建的很大,溢出的问题一目了然 ...

October 25, 2018 · 1 min · jiezi

dubbo负载均衡策略及对应源码分析

在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。我们还可以扩展自己的负责均衡策略,前提是你已经从一个小白变成了大牛,嘻嘻1、Random LoadBalance1.1 随机,按权重设置随机概率。1.2 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。1.3 源码分析package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.List;import java.util.Random;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;/** * random load balance. * * @author qianlei * @author william.liangf /public class RandomLoadBalance extends AbstractLoadBalance {public static final String NAME = “random”;private final Random random = new Random(); protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int totalWeight = 0; // 总权重 boolean sameWeight = true; // 权重是否都一样 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); totalWeight += weight; // 累计总权重 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; // 计算所有权重是否一样 } } if (totalWeight > 0 && ! sameWeight) { // 如果权重不相同且权重大于0则按总权重数随机 int offset = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < length; i++) { offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { return invokers.get(i); } } } // 如果权重相同或权重为0则均等随机 return invokers.get(random.nextInt(length));}}说明:从源码可以看出随机负载均衡的策略分为两种情况a. 如果总权重大于0并且权重不相同,就生成一个1totalWeight(总权重数)的随机数,然后再把随机数和所有的权重值一一相减得到一个新的随机数,直到随机 数小于0,那么此时访问的服务器就是使得随机数小于0的权重所在的机器b. 如果权重相同或者总权重数为0,就生成一个1length(权重的总个数)的随机数,此时所访问的机器就是这个随机数对应的权重所在的机器2、RoundRobin LoadBalance2.1 轮循,按公约后的权重设置轮循比率。2.2 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。2.3 源码分析package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.common.utils.AtomicPositiveInteger;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker; /** Round robin load balance.** @author qian.lei* @author william.liangf*/public class RoundRobinLoadBalance extends AbstractLoadBalance {public static final String NAME = “roundrobin”; private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();private final ConcurrentMap<String, AtomicPositiveInteger> weightSequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String key = invokers.get(0).getUrl().getServiceKey() + “.” + invocation.getMethodName(); int length = invokers.size(); // 总个数 int maxWeight = 0; // 最大权重 int minWeight = Integer.MAX_VALUE; // 最小权重 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); maxWeight = Math.max(maxWeight, weight); // 累计最大权重 minWeight = Math.min(minWeight, weight); // 累计最小权重 } if (maxWeight > 0 && minWeight < maxWeight) { // 权重不一样 AtomicPositiveInteger weightSequence = weightSequences.get(key); if (weightSequence == null) { weightSequences.putIfAbsent(key, new AtomicPositiveInteger()); weightSequence = weightSequences.get(key); } int currentWeight = weightSequence.getAndIncrement() % maxWeight; List<Invoker<T>> weightInvokers = new ArrayList<Invoker<T>>(); for (Invoker<T> invoker : invokers) { // 筛选权重大于当前权重基数的Invoker if (getWeight(invoker, invocation) > currentWeight) { weightInvokers.add(invoker); } } int weightLength = weightInvokers.size(); if (weightLength == 1) { return weightInvokers.get(0); } else if (weightLength > 1) { invokers = weightInvokers; length = invokers.size(); } } AtomicPositiveInteger sequence = sequences.get(key); if (sequence == null) { sequences.putIfAbsent(key, new AtomicPositiveInteger()); sequence = sequences.get(key); } // 取模轮循 return invokers.get(sequence.getAndIncrement() % length);}}说明:从源码可以看出轮循负载均衡的算法是:a. 如果权重不一样时,获取一个当前的权重基数,然后从权重集合中筛选权重大于当前权重基数的集合,如果筛选出的集合的长度为1,此时所访问的机器就是集合里面的权重对应的机器b. 如果权重一样时就取模轮循3、LeastActive LoadBalance3.1 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差(调用前的时刻减去响应后的时刻的值)。3.2 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大3.3 对应的源码package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.List;import java.util.Random;import com.alibaba.dubbo.common.Constants;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;import com.alibaba.dubbo.rpc.RpcStatus;/*** LeastActiveLoadBalance* * @author william.liangf*/public class LeastActiveLoadBalance extends AbstractLoadBalance {public static final String NAME = “leastactive”;private final Random random = new Random();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int leastActive = -1; // 最小的活跃数 int leastCount = 0; // 相同最小活跃数的个数 int[] leastIndexs = new int[length]; // 相同最小活跃数的下标 int totalWeight = 0; // 总权重 int firstWeight = 0; // 第一个权重,用于于计算是否相同 boolean sameWeight = true; // 是否所有权重相同 for (int i = 0; i < length; i++) { Invoker<T> invoker = invokers.get(i); int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活跃数 int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重 if (leastActive == -1 || active < leastActive) { // 发现更小的活跃数,重新开始 leastActive = active; // 记录最小活跃数 leastCount = 1; // 重新统计相同最小活跃数的个数 leastIndexs[0] = i; // 重新记录最小活跃数下标 totalWeight = weight; // 重新累计总权重 firstWeight = weight; // 记录第一个权重 sameWeight = true; // 还原权重相同标识 } else if (active == leastActive) { // 累计相同最小的活跃数 leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标 totalWeight += weight; // 累计总权重 // 判断所有权重是否一样 if (sameWeight && i > 0 && weight != firstWeight) { sameWeight = false; } } } // assert(leastCount > 0) if (leastCount == 1) { // 如果只有一个最小则直接返回 return invokers.get(leastIndexs[0]); } if (! sameWeight && totalWeight > 0) { // 如果权重不相同且权重大于0则按总权重数随机 int offsetWeight = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; offsetWeight -= getWeight(invokers.get(leastIndex), invocation); if (offsetWeight <= 0) return invokers.get(leastIndex); } } // 如果权重相同或权重为0则均等随机 return invokers.get(leastIndexs[random.nextInt(leastCount)]);}}说明:源码里面的注释已经很清晰了,大致的意思就是活跃数越小的的机器分配到的请求越多4、ConsistentHash LoadBalance4.1 一致性 Hash,相同参数的请求总是发到同一提供者。4.2 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。4.3 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key=“hash.arguments” value=“0,1” />4.4 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=“hash.nodes” value=“320” />4.5 源码分析暂时还没有弄懂,后面弄懂了再补充进来,有兴趣的小伙伴可以自己去看一下源码,然后一起交流一下心得如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。5、dubbo官方的文档的负载均衡配置示例服务端服务级别 <dubbo:service interface="…" loadbalance=“roundrobin” />客户端服务级别 <dubbo:reference interface="…" loadbalance=“roundrobin” />服务端方法级别 <dubbo:service interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:service>客户端方法级别 <dubbo:reference interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:reference> ...

October 25, 2018 · 4 min · jiezi

记录一次亲身经历的dubbo项目实战

一、案例说明存在2个系统,A系统和B系统,A系统调用B系统的接口获取数据,用于查询用户列表。二、环境搭建安装zookeeper,解压(zookeeper-3.4.8.tar.gz)得到如下:然后进入conf将zoo_sample.cfg改名成zoo.cfg。并相关如下内容:该目录为存放数据的目录。然后启动,在bin目录下:三、工程创建1、搭建B工程1.导入依赖<dependencies><!– dubbo采用spring配置方式,所以需要导入spring容器依赖 –><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>4.1.3.RELEASE</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.6.4</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>dubbo</artifactId><version>2.5.3</version><exclusions><exclusion><!– 排除传递spring依赖 –><artifactId>spring</artifactId><groupId>org.springframework</groupId></exclusion></exclusions></dependency><!– zookeeper依赖 –><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.3.3</version></dependency><dependency><groupId>com.github.sgroschupf</groupId><artifactId>zkclient</artifactId><version>0.1</version></dependency></dependencies>2.创建对象public class User implements Serializable{//序列化自动生成private static final long serialVersionUID = 1749666453251148943L;private Long id;private String username;private String password;private Integer age; //getter and setter}3.创建服务public class UserServiceImpl implements UserService {//实现查询,这里做模拟实现,不做具体的数据库查询public List<User> queryAll() {List<User> list = new ArrayList<User>();for (int i = 0; i < 10; i++) {User user = new User();user.setAge(10 + i);user.setId(Long.valueOf(i + 1));user.setPassword(“123456”);user.setUsername(“username_” + i);list.add(user);}return list;}}4.编写Dubbo的配置文件位置我放在根目录下dubbo/dubbo-server.xml,内容如下:<beans xmlns=“http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context" xmlns:p=“http://www.springframework.org/schema/p"xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx=“http://www.springframework.org/schema/tx"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo=“http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"><!-- 提供方应用信息,用于计算依赖关系 –><dubbo:application name=“dubbo-b-server” /><!– 这里使用的注册中心是zookeeper –><dubbo:registry address=“zookeeper://127.0.0.1:2181” client=“zkclient”/><!– 用dubbo协议在20880端口暴露服务 –><dubbo:protocol name=“dubbo” port=“20880” /><!– 将该接口暴露到dubbo中 –><dubbo:service interface=“com.shen.dubbo.service.UserService” ref=“userServiceImpl” /><!– 将具体的实现类加入到Spring容器中 –><bean id=“userServiceImpl” class=“com.shen.dubbo.service.impl.UserServiceImpl” /></beans>5.编写Web.xml<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://java.sun.com/xml/ns/javaee" xsi:schemaLocation=“http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id=“WebApp_ID” version=“2.5”><display-name>dubbo-b</display-name><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:dubbo/dubbo-.xml</param-value></context-param><!–Spring的ApplicationContext 载入 –><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener></web-app>6.启动tomcat在控制台中将会看到如下内容:可以看到,已经将UserService服务注册到zookeeper注册中心,协议采用的是dubbo。2、搭建A工程1.拷贝基本文件从b系统中拷贝User对象、UserService接口到a系统2.编写Dubbo的配置文件<!– 提供方应用信息,用于计算依赖关系 –><dubbo:application name=“dubbo-a-consumer” /><!– 这里使用的注册中心是zookeeper –><dubbo:registry address=“zookeeper://127.0.0.1:2181” client=“zkclient”/><!– 从注册中心中查找服务 –><dubbo:reference id=“userService” interface=“com.shen.dubbo.service.UserService”/>3.编写UserService测试用例public class UserServiceTest {private UserService userService;@Beforepublic void setUp() throws Exception {ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath:dubbo/.xml”);this.userService = applicationContext.getBean(UserService.class);}@Testpublic void testQueryAll() {List<User> users = this.userService.queryAll();for (User user : users) {System.out.println(user);}}}查看效果如下:可以看到,已经查询到10条数据,那么,也就是说A系统通过B系统提供的服务获取到了数据。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3、解决代码重复问题我们可以看到,在上面的案例中User实体和服务接口两个项目都需要使用,代码复用不高。那么我们可以将该部分代码抽取出来打成包,以供所有系统使用。故可以在创建一个工程项目名为dubbo-b-api。然后将相关的代码都放到该项目中,再在其它项目中导入该项目依赖即可。这也是我们在真实项目中应该做的事情,因为调用方未必知道细节。 ...

October 23, 2018 · 1 min · jiezi

结束了我短暂的秋招,说点自己的感受

该文已加入开源文档:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识)。地址:https://github.com/Snailclimb…秋招历程流水账总结笔主大四准毕业生,在秋招末流比较幸运地进入了一家自己非常喜欢一家公司——ThoughtWorks.从9-6号投递出去第一份简历,到10-18号左右拿到第一份 offer ,中间差不多有 1 个半月的时间了。可能自己比较随缘,而且自己所在的大学所处的位置并不是互联网比较发达的城市的原因。所以,很少会有公司愿意跑到我们学校那边来宣讲,来的公司也大多是一些自己没听过或者不太喜欢的公司。所以,在前期,我仅仅能够通过网上投递简历的方式来找工作。零零总总算了一下,自己在网上投了大概有 10 份左右的简历,都是些自己还算喜欢的公司。简单说一下自己投递的一些公司:网上投递的公司有:ThoughtWorks、网易、小米、携程、爱奇艺、知乎、小红书、搜狐、欢聚时代、京东;直接邮箱投递的有:烽火、中电数据、蚂蚁金服花呗部门、今日头条;线下宣讲会投递的有:玄武科技。网上投递的大部分简历都是在做完笔试之后就没有了下文了,即使有几场笔试自我感觉做的很不错的情况下,还是没有收到后续的面试邀请。还有些邮箱投递的简历,后面也都没了回应。所以,我总共也只参加了3个公司的面试,ThoughtWorks、玄武科技和中电数据,都算是拿到了 offer。拿到 ThoughtWorks 的 offer之后,后面的一些笔试和少部分面试都拒了。决定去 ThoughtWorks 了,春招的大部队会没有我的存在。我个人对 ThoughtWorks 最有好感,ThoughtWorks 也是我自己之前很想去的一家公司。不光是因为我投递简历的时候可以不用重新填一遍表格可以直接发送我已经编辑好的PDF格式简历的友好,这个公司的文化也让我很喜欢。每次投递一家公司几乎都要重新填写一遍简历真的很让人头疼,即使是用牛客网的简历助手也还是有很多东西需要自己重新填写。说句实话,自己在拿到第一份 offer 之前心里还是比较空的,虽然说对自己还是比较自信。包括自己当时来到武汉的原因,也是因为自己没有 offer ,就感觉心里空空的,我相信很多人在这个时候与我也有一样的感觉。然后,我就想到武汉参加一下别的学校宣讲会。现在看来,这个决定也是不必要的,因为我最后去的公司 ThoughtWorks,虽然就在我租的房子的附近,但之前投递的时候,选择的还是远程面试。来到武汉,简单的修整了一下之后,我就去参加了玄武科技在武理工的宣讲会,顺便做了笔试,然后接着就是技术面、HR面、高管面。总体来说,玄武科技的 HR 真的很热情,为他们点个赞,虽然自己最后没能去玄武科技,然后就是技术面非常简单,HR面和高管面也都还好,不会有压抑的感觉,总体聊得很愉快。需要注意的是 玄武科技和很多公司一样都有笔试中有逻辑题,我之前没有做过类似的题,所以当时第一次做有点懵逼。高管面的时候,高管还专门在我做的逻辑题上聊了一会,让我重新做了一些做错的题,并且给他讲一些题的思路,可以看出高层对于应聘者的这项能力还是比较看重的。中电数据的技术面试是电话进行的,花了1个多小时一点,个人感觉问的还是比较深的,感觉自己总体回答的还是比较不错的。这里我着重说一下 ThoughtWorks,也算是给想去 ThoughtWorks 的同学一点小小的提示。我是 9.11 号在官网:https://join.thoughtworks.cn/ 投递的简历,9.20 日邮件通知官网下载作业,作业总体来说不难,9.21 号花了半天多的时间做完,然后就直接在9.21 号下午提交了。然后等了挺长时间的,可能是因为 ThoughtWorks 在管理方面比较扁平化的原因,所以总体来说效率可能不算高。因为我选的是远程面试,所以直接下载好 zoom 之后,等HR打电话过来告诉你一个房间号,你就可以直接进去面试就好,一般技术面试有几个人看着你。技术面试的内容,首先就是在面试官让你在你之前做的作业的基础上新增加一个或者两个功能(20分钟)。所以,你在技术面试之前一定要保证你的程序的扩展性是不错的,另外就是你在技术面试之前最好能重构一下自己写的程序。重构本身就是你自己对你写的程序的理解加强很好的一种方式,另外重构也能让你发现你的程序的一些小问题。然后,这一步完成之后,面试官可能会问你一些基础问题,比较简单,所以我觉得 ThoughtWorks 可能更看重你的代码质量。ThoughtWorks 的 HR 面和其他公司的唯一不同可能在于,他会让你用英语介绍一下自己或者说自己的技术栈啊这些。关于面试一些重要的问题总结另外,再给大家总结一些我个人想到一些关于面试非常重要的一些问题。面试前如何准备运筹帷幄之后,决胜千里之外!不打毫无准备的仗,我觉得大家可以先从下面几个方面来准备面试:自我介绍。(你可千万这样介绍:“我叫某某,性别,来自哪里,学校是那个,自己爱干什么”,记住:多说点简历上没有的,多说点自己哪里比别人强!)自己面试中可能涉及哪些知识点、那些知识点是重点。面试中哪些问题会被经常问到、面试中自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)自己的简历该如何写。另外,如果你想去类似阿里巴巴、腾讯这种比较大的互联网公司的话,一定要尽早做打算。像阿里巴巴在7月份左右就开始了提前批招聘,到了9月份差不多就已经招聘完毕了。所以,秋招没有参加到阿里的面试还是很遗憾的,毕竟面试即使失败了,也能从阿里难度Max的面试中学到很多东西。关于着装穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。关于自我介绍如果你简历上写的基本信息就不要说了,比如性别、年龄、学校。另外,你也不要一上来就说自己爱好什么这方面内容。因为,面试官根本不关心这些东西。你直接挑和你岗位相关的重要经历和自己最突出的特点讲就好了。比如:面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个项目已经有多少Star了。提前准备面试之前可以在网上找找有没有你要面试的公司的面经。在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准备,我觉得 20 分钟我很大几率会完不成这项任务。面试中面试的时候一定要自信,千万不要怕自己哪里会答不出来,或者说某个问题自己忘记怎么回答了。面试过程中,很多问题可能是你之前没有碰到过的,这个时候你就要通过自己构建的知识体系来思考这些问题。如果某些问题你回答不上来,你也可以让面试官给你简单的提示一下。总之,你要自信,你自信的前提是自己要做好充分的准备。下面给大家总结一些面试非常常见的问题:SpringMVC 工作原理说一下自己对 IOC 、AOP 的理解Spring 中用到了那些设计模式,讲一下自己对于这些设计模式的理解Spring Bean 的作用域和生命周期了解吗Spring 事务中的隔离级别Spring 事务中的事务传播行为手写一个 LRU 算法知道那些排序算法,简单介绍一下快排的原理,能不能手写一下快排String 为什么是不可变的?String为啥要设计为不可变的?Arraylist 与 LinkedList 异同HashMap的底层实现HashMap 的长度为什么是2的幂次方ConcurrentHashMap 和 Hashtable 的区别ConcurrentHashMap线程安全的具体实现方式/底层具体实现如果你的简历写了redis 、dubbo、zookeeper、docker的话,面试官还会问一下这些东西。比如redis可能会问你:为什么要用 redis、为什么要用 redis 而不用 map/guava 做缓存、redis 常见数据结构以及使用场景分析、 redis 设置过期时间、redis 内存淘汰机制、 redis 持久化机制、 缓存雪崩和缓存穿透问题、如何解决 Redis 的并发竞争 Key 问题、如何保证缓存与数据库双写时的数据一致性。一些简单的 Linux 命令。为什么要用 消息队列关于 Java多线程,在面试的时候,问的比较多的就是①悲观锁和乐观锁②synchronized 和 ReenTrantLock 区别以及 volatile 和 synchronized 的区别,③可重入锁与非可重入锁的区别、④多线程是解决什么问题的、⑤线程池解决什么问题,为什么要用线程池 ⑥Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 ReenTrantLock 对比;⑦线程池使用时的注意事项、⑧AQS 原理以及 AQS 同步组件:Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock、⑨ReentranLock源码,设计原理,整体过程 等等问题。关于 Java 虚拟机问的比较多的是:①Java内存区域、②虚拟机垃圾算法、③虚拟机垃圾收集器、④JVM内存管理、⑤JVM调优这些问题。面试后如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!你若盛开,清风自来。 欢迎关注我的微信公众号:“Java面试通关手册”,一个有温度的微信公众号。公众号后台回复关键字“1”,可以免费获取一份我精心准备的小礼物哦! ...

October 23, 2018 · 1 min · jiezi

如何看待Spring下单例模式与线程安全的矛盾

前言有多少人在使用Spring框架时,很多时候不知道或者忽视了多线程的问题? 因为写程序时,或做单元测试时,很难有机会碰到多线程的问题,因为没有那么容易模拟多线程测试的环境。那么当多个线程调用同一个bean的时候就会存在线程安全问题。如果是Spring中bean的创建模式为非单例的,也就不存在这样的问题了。 但如果不去考虑潜在的漏洞,它就会变成程序的隐形杀手,在你不知道的时候爆发。而且,通常是程序交付使用时,在生产环境下触发,会是很麻烦的事。Spring使用ThreadLocal解决线程安全问题 我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。 一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程 ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。 1) 常量始终是线程安全的,因为只存在读操作。 2)每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。 3)局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。 有状态对象: 无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。 Struts2默认的实现是Prototype模式。也就是每个请求都新生成一个Action实例,所以不存在线程安全问题。需要注意的是,如果由Spring管理action的生命周期, scope要配成prototype作用域线程安全案例 SimpleDateFormat( 下面简称 sdf) 类内部有一个 Calendar 对象引用 , 它用来储存和这个 sdf 相关的日期信息 , 例如 sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关 String, Date 等等 , 都是交友 Calendar 引用来储存的 . 这样就会导致一个问题 , 如果你的 sdf 是个 static 的 , 那么多个 thread 之间就会共享这个 sdf, 同时也是共享这个 Calendar 引用 , 并且 , 观察 sdf.parse() 方法 , 你会发现有如下的调用 : Date parse() { calendar.clear(); // 清理calendar … // 执行一些操作, 设置 calendar 的日期什么的 calendar.getTime(); // 获取calendar的时间 } 这里会导致的问题就是 , 如果 线程 A 调用了 sdf.parse(), 并且进行了 calendar.clear() 后还未执行 calendar.getTime() 的时候 , 线程 B 又调用了 sdf.parse(), 这时候线程 B 也执行了 sdf.clear() 方法 , 这样就导致线程 A 的的 calendar 数据被清空了 ( 实际上 A,B 的同时被清空了 ). 又或者当 A 执行了 calendar.clear() 后被挂起 , 这时候 B 开始调用 sdf.parse() 并顺利 i 结束 , 这样 A 的 calendar 内存储的的 date 变成了后来 B 设置的 calendar 的 date 这个问题背后隐藏着一个更为重要的问题 – 无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。 format 方法在运行过程中改动了SimpleDateFormat 的 calendar 字段,所以,它是有状态的。 这也同时提醒我们在开发和设计系统的时候注意下以下三点 :自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明对线程环境下,对每一个共享的可变变量都要注意其线程安全性我们的类和方法在做设计的时候,要尽量设计成无状态的解决办法1. 需要的时候创建新实例: 说明:在需要用到 SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。2. 使用同步:同步 SimpleDateFormat 对象public class DateSyncUtil { private static SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); public static String formatDate(Date date)throws ParseException{ synchronized(sdf){ return sdf.format(date); } } public static Date parse(String strDate) throws ParseException{ synchronized(sdf){ return sdf.parse(strDate); } } } 说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block ,多线程并发量大的时候会对性能有一定的影响。3. 使用 ThreadLocal :public class ConcurrentDateUtil { private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); } }; public static Date parse(String dateStr) throws ParseException { return threadLocal.get().parse(dateStr); } public static String format(Date date) { return threadLocal.get().format(date); }} 或ThreadLocal<DateFormat>(); public static DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if(df==null){ df = new SimpleDateFormat(date_format); threadLocal.set(df); } return df; } public static String formatDate(Date date) throws ParseException { return getDateFormat().format(date); } public static Date parse(String strDate) throws ParseException { return getDateFormat().parse(strDate); } } 说明:使用 ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。4. 抛弃 JDK ,使用其他类库中的时间格式化类:使用 Apache commons 里的 FastDateFormat ,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行 format, 不能对日期串进行解析。使用 Joda-Time 类库来处理时间相关问题 做一个简单的压力测试,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系统方法一和方法二就可以满足,所以说在这个点很难成为你系统的瓶颈所在。从简单的角度来说,建议使用方法一或者方法二,如果在必要的时候,追求那么一点性能提升的话,可以考虑用方法三,用 ThreadLocal 做缓存。 Joda-Time 类库对时间处理方式比较完美,建议使用。总结 回到文章开头的问题:《有多少人在使用Spring框架时,很多时候不知道或者忽视了多线程的问题?》 其实代码谁都会写,为什么架构师写的代码效果和你的天差地别呢?应该就是此类你没考虑到的小问题而架构师都考虑到了。 架构师知识面更广,见识到的具体情况更多,解决各类问题的经验更丰富。只要你养成架构师的思维和习惯,那你离架构师还会远吗? ...

October 23, 2018 · 2 min · jiezi

Spring事务事件监控

前面我们讲到了Spring在进行事务逻辑织入的时候,无论是事务开始,提交或者回滚,都会触发相应的事务事件。本文首先会使用实例进行讲解Spring事务事件是如何使用的,然后会讲解这种使用方式的实现原理。1.示例对于事务事件,Spring提供了一个注解@TransactionEventListener,将这个注解标注在某个方法上,那么就将这个方法声明为了一个事务事件处理器,而具体的事件类型则是由TransactionalEventListener.phase属性进行定义的。如下是TransactionalEventListener的声明:@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@EventListenerpublic @interface TransactionalEventListener {// 指定当前标注方法处理事务的类型TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;// 用于指定当前方法如果没有事务,是否执行相应的事务事件监听器boolean fallbackExecution() default false;// 与classes属性一样,指定了当前事件传入的参数类型,指定了这个参数之后就可以在监听方法上// 直接什么一个这个参数了@AliasFor(annotation = EventListener.class, attribute = “classes”)Class<?>[] value() default {};// 作用于value属性一样,用于指定当前监听方法的参数类型@AliasFor(annotation = EventListener.class, attribute = “classes”)Class<?>[] classes() default {};// 这个属性使用Spring Expression Language对目标类和方法进行匹配,对于不匹配的方法将会过滤掉String condition() default “”;}关于这里的classes属性需要说明一下,如果指定了classes属性,那么当前监听方法的参数类型就可以直接使用所发布的事件的参数类型,如果没有指定,那么这里监听的参数类型可以使用两种:ApplicationEvent和PayloadApplicationEvent。对于ApplicationEvent类型的参数,可以通过其getSource()方法获取发布的事件参数,只不过其返回值是一个Object类型的,如果想获取具体的类型还需要进行强转;对于PayloadApplicationEvent类型,其可以指定一个泛型参数,该泛型参数必须与发布的事件的参数类型一致,这样就可以通过其getPayload()方法获取事务事件发布的数据了。关于上述属性中的TransactionPhase,其可以取如下几个类型的值:public enum TransactionPhase { // 指定目标方法在事务commit之前执行 BEFORE_COMMIT, // 指定目标方法在事务commit之后执行 AFTER_COMMIT, // 指定目标方法在事务rollback之后执行 AFTER_ROLLBACK, // 指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了 AFTER_COMPLETION } 这里我们假设数据库有一个user表,对应的有一个UserService和User的model,用于往该表中插入数据,并且插入动作时使用注解标注目标方法。如下是这几个类的声明:public class User {private long id;private String name;private int age;// getter and setter…}.@Service@Transactionalpublic class UserServiceImpl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate; @Autowired private ApplicationEventPublisher publisher;@Overridepublic void insert(User user) {jdbcTemplate.update(“insert into user (id, name, age) value (?, ?, ?)”, user.getId(), user.getName(), user.getAge());publisher.publishEvent(user);}}上述代码中有一点需要注意的是,对于需要监控事务事件的方法,在目标方法执行的时候需要使用ApplicationEventPublisher发布相应的事件消息。如下是对上述消息进行监控的程序:@Componentpublic class UserTransactionEventListener {@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)public void beforeCommit(PayloadApplicationEvent<User> event) {System.out.println(“before commit, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)public void afterCommit(PayloadApplicationEvent<User> event) {System.out.println(“after commit, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)public void afterCompletion(PayloadApplicationEvent<User> event) {System.out.println(“after completion, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)public void afterRollback(PayloadApplicationEvent<User> event) {System.out.println(“after rollback, id: " + event.getPayload().getId());}}这里对于事件的监控,只需要在监听方法上添加@TransactionalEventListener注解即可。这里需要注意的一个问题,在实际使用过程中,对于监听的事务事件,需要使用其他的参数进行事件的过滤,因为这里的监听还是会监听所有事件参数为User类型的事务,而无论其是哪个位置发出来的。如果需要对事件进行过滤,这里可以封装一个UserEvent对象,其内保存一个类似EventType的属性和一个User对象,这样在发布消息的时候就可以指定EventType属性,而在监听消息的时候判断当前方法监听的事件对象的EventType是否为目标type,如果是,则对其进行处理,否则直接略过。下面是上述程序的xml文件配置和驱动程序:<bean id=“dataSource” class=“org.apache.commons.dbcp.BasicDataSource”><property name=“url” value=“jdbc:mysql://localhost/test?useUnicode=true”/><property name=“driverClassName” value=“com.mysql.jdbc.Driver”/><property name=“username” value=””/><property name=“password” value=””/></bean><bean id=“jdbcTemplate” class=“org.springframework.jdbc.core.JdbcTemplate”><property name=“dataSource” ref=“dataSource”/></bean><bean id=“transactionManager” class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”><property name=“dataSource” ref=“dataSource”/></bean><context:component-scan base-package=“com.transaction”/><tx:annotation-driven/>.public class TransactionApp {@Testpublic void testTransaction() {ApplicationContext ac = new ClassPathXmlApplicationContext(“applicationContext.xml”);UserService userService = context.getBean(UserService.class);User user = getUser();userService.insert(user);}private User getUser() {int id = new Random() .nextInt(1000000);User user = new User();user.setId(id);user.setName(“Mary”);user.setAge(27);return user;}}运行上述程序,其执行结果如下:before commit, id: 935052after commit, id: 935052after completion, id: 935052可以看到,这里确实成功监听了目标程序的相关事务行为。2.实现原理关于事务的实现原理,这里其实是比较简单的,在前面的文章中,我们讲解到,Spring对事务监控的处理逻辑在TransactionSynchronization中,如下是该接口的声明:public interface TransactionSynchronization extends Flushable {// 在当前事务挂起时执行default void suspend() {}// 在当前事务重新加载时执行default void resume() {}// 在当前数据刷新到数据库时执行default void flush() {}// 在当前事务commit之前执行default void beforeCommit(boolean readOnly) {}// 在当前事务completion之前执行default void beforeCompletion() {}// 在当前事务commit之后实质性default void afterCommit() {}// 在当前事务completion之后执行default void afterCompletion(int status) {}}很明显,这里的TransactionSynchronization接口只是抽象了一些行为,用于事务事件发生时触发,这些行为在Spring事务中提供了内在支持,即在相应的事务事件时,其会获取当前所有注册的TransactionSynchronization对象,然后调用其相应的方法。那么这里TransactionSynchronization对象的注册点对于我们了解事务事件触发有至关重要的作用了。这里我们首先回到事务标签的解析处,在前面讲解事务标签解析时,我们讲到Spring会注册一个TransactionalEventListenerFactory类型的bean到Spring容器中,这里关于标签的解析读者可以阅读本人前面的文章Spring事务用法示例与实现原理。这里注册的TransactionalEventListenerFactory实现了EventListenerFactory接口,这个接口的主要作用是先判断目标方法是否是某个监听器的类型,然后为目标方法生成一个监听器,其会在某个bean初始化之后由Spring调用其方法用于生成监听器。如下是该类的实现:public class TransactionalEventListenerFactory implements EventListenerFactory, Ordered {// 指定当前监听器的顺序private int order = 50;public void setOrder(int order) { this.order = order;}@Overridepublic int getOrder() { return this.order;}// 指定目标方法是否是所支持的监听器的类型,这里的判断逻辑就是如果目标方法上包含有// TransactionalEventListener注解,则说明其是一个事务事件监听器@Overridepublic boolean supportsMethod(Method method) { return (AnnotationUtils.findAnnotation(method, TransactionalEventListener.class) != null);}// 为目标方法生成一个事务事件监听器,这里ApplicationListenerMethodTransactionalAdapter实现了// ApplicationEvent接口@Overridepublic ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) { return new ApplicationListenerMethodTransactionalAdapter(beanName, type, method);}}这里关于事务事件监听的逻辑其实已经比较清楚了。ApplicationListenerMethodTransactionalAdapter本质上是实现了ApplicationListener接口的,也就是说,其是Spring的一个事件监听器,这也就是为什么进行事务处理时需要使用ApplicationEventPublisher.publish()方法发布一下当前事务的事件。ApplicationListenerMethodTransactionalAdapter在监听到发布的事件之后会生成一个TransactionSynchronization对象,并且将该对象注册到当前事务逻辑中,如下是监听事务事件的处理逻辑:@Override public void onApplicationEvent(ApplicationEvent event) {// 如果当前TransactionManager已经配置开启事务事件监听,// 此时才会注册TransactionSynchronization对象if (TransactionSynchronizationManager.isSynchronizationActive()) { // 通过当前事务事件发布的参数,创建一个TransactionSynchronization对象 TransactionSynchronization transactionSynchronization = createTransactionSynchronization(event); // 注册TransactionSynchronization对象到TransactionManager中 TransactionSynchronizationManager .registerSynchronization(transactionSynchronization);} else if (this.annotation.fallbackExecution()) { // 如果当前TransactionManager没有开启事务事件处理,但是当前事务监听方法中配置了 // fallbackExecution属性为true,说明其需要对当前事务事件进行监听,无论其是否有事务 if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn(“Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase”); } processEvent(event);} else { // 走到这里说明当前是不需要事务事件处理的,因而直接略过 if (logger.isDebugEnabled()) { logger.debug(“No transaction is active - skipping " + event); }}}这里需要说明的是,上述annotation属性就是在事务监听方法上解析的TransactionalEventListener注解中配置的属性。可以看到,对于事务事件的处理,这里创建了一个TransactionSynchronization对象,其实主要的处理逻辑就是在返回的这个对象中,而createTransactionSynchronization()方法内部只是创建了一个TransactionSynchronizationEventAdapter对象就返回了。这里我们直接看该对象的源码: private static class TransactionSynchronizationEventAdapter extends TransactionSynchronizationAdapter { private final ApplicationListenerMethodAdapter listener; private final ApplicationEvent event; private final TransactionPhase phase;public TransactionSynchronizationEventAdapter(ApplicationListenerMethodAdapter listener, ApplicationEvent event, TransactionPhase phase) { this.listener = listener; this.event = event; this.phase = phase;}@Overridepublic int getOrder() { return this.listener.getOrder();}// 在目标方法配置的phase属性为BEFORE_COMMIT时,处理before commit事件public void beforeCommit(boolean readOnly) { if (this.phase == TransactionPhase.BEFORE_COMMIT) { processEvent(); }}// 这里对于after completion事件的处理,虽然分为了三个if分支,但是实际上都是执行的processEvent()// 方法,因为after completion事件是事务事件中一定会执行的,因而这里对于commit,// rollback和completion事件都在当前方法中处理也是没问题的public void afterCompletion(int status) { if (this.phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_COMPLETION) { processEvent(); }}// 执行事务事件protected void processEvent() { this.listener.processEvent(this.event);}}可以看到,对于事务事件的处理,最终都是委托给了ApplicationListenerMethodAdapter.processEvent()方法进行的。如下是该方法的源码: public void processEvent(ApplicationEvent event) {// 处理事务事件的相关参数,这里主要是判断TransactionalEventListener注解中是否配置了value// 或classes属性,如果配置了,则将方法参数转换为该指定类型传给监听的方法;如果没有配置,则判断// 目标方法是ApplicationEvent类型还是PayloadApplicationEvent类型,是则转换为该类型传入Object[] args = resolveArguments(event);// 这里主要是获取TransactionalEventListener注解中的condition属性,然后通过// Spring expression language将其与目标类和方法进行匹配if (shouldHandle(event, args)) { // 通过处理得到的参数借助于反射调用事务监听方法 Object result = doInvoke(args); if (result != null) { // 对方法的返回值进行处理 handleResult(result); } else { logger.trace(“No result object given - no result to handle”); }} } // 处理事务监听方法的参数protected Object[] resolveArguments(ApplicationEvent event) {// 获取发布事务事件时传入的参数类型ResolvableType declaredEventType = getResolvableType(event);if (declaredEventType == null) { return null;}// 如果事务监听方法的参数个数为0,则直接返回if (this.method.getParameterCount() == 0) { return new Object[0];}// 如果事务监听方法的参数不为ApplicationEvent或PayloadApplicationEvent,则直接将发布事务// 事件时传入的参数当做事务监听方法的参数传入。从这里可以看出,如果事务监听方法的参数不是// ApplicationEvent或PayloadApplicationEvent类型,那么其参数必须只能有一个,并且这个// 参数必须与发布事务事件时传入的参数一致Class<?> eventClass = declaredEventType.getRawClass();if ((eventClass == null || !ApplicationEvent.class.isAssignableFrom(eventClass)) && event instanceof PayloadApplicationEvent) { return new Object[] {((PayloadApplicationEvent) event).getPayload()};} else { // 如果参数类型为ApplicationEvent或PayloadApplicationEvent,则直接将其传入事务事件方法 return new Object[] {event};} } // 判断事务事件方法方法是否需要进行事务事件处理private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) {if (args == null) { return false;}String condition = getCondition();if (StringUtils.hasText(condition)) { Assert.notNull(this.evaluator, “EventExpressionEvaluator must no be null”); EvaluationContext evaluationContext = this.evaluator.createEvaluationContext( event, this.targetClass, this.method, args, this.applicationContext); return this.evaluator.condition(condition, this.methodKey, evaluationContext);}return true; } // 对事务事件方法的返回值进行处理,这里的处理方式主要是将其作为一个事件继续发布出去,这样就可以在// 一个统一的位置对事务事件的返回值进行处理protected void handleResult(Object result) {// 如果返回值是数组类型,则对数组元素一个一个进行发布if (result.getClass().isArray()) { Object[] events = ObjectUtils.toObjectArray(result); for (Object event : events) { publishEvent(event); }} else if (result instanceof Collection<?>) { // 如果返回值是集合类型,则对集合进行遍历,并且发布集合中的每个元素 Collection<?> events = (Collection<?>) result; for (Object event : events) { publishEvent(event); }} else { // 如果返回值是一个对象,则直接将其进行发布 publishEvent(result);}}对于事务事件的处理,总结而言,就是为每个事务事件监听方法创建了一个TransactionSynchronizationEventAdapter对象,通过该对象在发布事务事件的时候,会在当前线程中注册该对象,这样就可以保证每个线程每个监听器中只会对应一个TransactionSynchronizationEventAdapter对象。在Spring进行事务事件的时候会调用该对象对应的监听方法,从而达到对事务事件进行监听的目的。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3.小结本文首先对事务事件监听程序的使用方式进行了讲解,然后在源码层面讲解了Spring事务监听器是如何实现的。在Spring事务监听器使用过程中,需要注意的是要对当前接收到的事件类型进行判断,因为不同的事务可能会发布同样的消息对象过来。 ...

October 22, 2018 · 3 min · jiezi

微服务架构组件分析

微服务架构组件1、 如何发布和引用服务服务描述:服务调用首先解决的问题就是服务如何对外描述。 常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。RESTful API主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用优势:HTTP 协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本,所以比较适合用作跨业务平台之间的服务协议。劣势:-性能相对比较低XML 配置一般是私有 RPC 框架会选择 XML 配置这种方式来描述接口,因为私有 RPC 协议的性能比 HTTP 协议高,所以在对性能要求比较高的场景下,采用 XML 配置比较合适。这种方式的服务发布和引用主要分三个步骤:服务提供者定义接口,并实现接口服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。服务消费者进程启动时,通过加载 client.xml 配置文件引入要调用的接口。优势:私有 RPC 协议的性能比 HTTP 协议高,所以在对性能要求比较高的场景下,采用 XML 配置方式比较合适 劣势:对业务代码侵入性比较高XML 配置有变更的时候,服务消费者和服务提供者都要更新(建议:公司内部联系比较紧密的业务之间采用)IDL 文件IDL 就是接口描述语言(interface description language)的缩写,通过一种中立的方式来描接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。常用的 IDL:一个是 Facebook 开源的 Thrift 协议,另一个是 Google 开源的 gRPC 协议。无论是 Thrift 协议还是 gRPC 协议,他们的工作原来都是类似的。优势:用作跨语言平台的服务之间的调用劣势:在描述接口定义时,IDL 文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用 IDL文件方式的接口定义就不太合适了。一方面会造成 IDL 文件过大难以维护另一方面只要 IDL 文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本太高了。总结具体采用哪种服务描述方式是根据实际情况决定,通常情况下, 如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。如果企业内部存在多个服务,并且服务采用的是不同语言平台,建议使用 IDL 文件方式进行描述服务。如果还存在对外开放服务调用的情形的话,使用 RESTful API 方式则更加通用。2、 如何注册和发现服务注册中心原理在微服务架构下, 主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系如图RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中配置的信息,向 Registry 注册服务,把Registry 返回的服务节点列表缓存在本地内存中,并于 RPC Server 建立连接。RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向 Registry 订阅服务,把Registry 返回的服务节点列表缓存在本地内存中,并于 RPC Client 建立连接。当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Server 发起调用。注册中心实现方式注册中心API服务注册接口:服务提供者通过调用注册接口来完成服务注册服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存货状态上报服务订阅接口:服务消费者调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表服务查询接口:查询注册中心当前住了哪些服务信息服务修改接口:修改注册中心某一服务的信息集群部署注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。Zookeeper 的工作原理:每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 ServerZookeeper 启动时,将从实例中选举一个 leader(Paxos 协议)Leader 负责处理数据更新等操作(ZAB 协议)一个更新操作方式,Zookeeper 保证了高可用性以及数据一致性目录存储ZooKeeper作为注册中心存储服务信息一般采用层次化的目录结构:每个目录在 ZooKeeper 中叫作 znode,并且其有一个唯一的路径标识znode 可以包含数据和子 znode。znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。服务健康状态检测注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。在 ZooKeeper 中,客户端和服务端建立连接后,会话也也随之建立,并生成一个全局唯一的 SessionID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT,ZooKeeper 就会认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。服务状态变更通知一旦注册中心探测到有服务器提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。基于 Zookeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 Zookeeper 的getData 方式订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData方法来获取变更后的数据,刷新本地混存的服务节点信息。白名单机制注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。总结注册中心可以说是实现服务话的关键,因为服务话之后,服务提供者和服务消费者不在同一个进程中运行,实现了解耦,这就需要一个纽带去连接服务提供者和服务消费者,而注册中心就正好承担了这一角色。此外,服务提供者可以任意伸缩即增加节点或者减少节点,通过服务健康状态检测,注册中心可以保持最新的服务节点信息,并将变化通知给订阅服务的服务消费者。注册中心一般采用分布式集群部署,来保证高可用性,并且为了实现异地多活,有的注册中心还采用多 IDC 部署,这就对数据一致性产生了很高的要求,这些都是注册中心在实现时必须要解决的问题。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3、如何实现 RPC 远程服务调用客户端和服务端如何建立网络连接HTTP 通信HTTP 通信是基于应用层HTTP 协议的,而 HTTP 协议又是基于传输层 TCP 协议的。一次 HTTP 通信过程就是发起一次 HTTP 调用,而一次 HTTP 调用就会建立一个 TCP 连接,经历一次下图所示的 “三次握手”的过程来建立连接。完成请求后,再经历一次“四次挥手”的过程来断开连接。Socket 通信Socket 通信是基于 TCP/IP 协议的封装,建立一次Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket ;另一个运行于服务器端,称为 ServerSocket 。服务器监听:ServerSocket 通过点用 bind() 函数绑定某个具体端口,然后调用 listen() 函数实时监控网络状态,等待客户端的连接请求。客户端请求:ClientSocket 调用 connect() 函数向 ServerSocket 绑定的地址和端口发起连接请求。服务端连接确认:当 ServerSocket 监听都或者接收到 ClientSocket 的连接请求时,调用 accept() 函数响应ClientSocket 的请求,同客户端建立连接。数据传输:当 ClientSocket 和 ServerSocket 建立连接后,ClientSocket 调用 send() 函数,ServerSocket 调用 receive() 函数,ServerSocket 处理完请求后,调用 send() 函数,ClientSocket 调用 receive() 函数,就可以得到返回结果。当客户端和服务端建立网络连接后,就可以起发起请求了。但网络不一定总是可靠的,经常会遇到网络闪断、连接超时、服务端宕机等各种异常,通常的处理手段有两种:链路存活检测:客户端需要定时地发送心跳检测小心(一般通过 ping 请求) 给服务端,如果服务端连续 n次心跳检测或者超过规定的时间没有回复消息,则认为此时链路已经失效,这个时候客户端就需要重新与服务端建立连接。断连重试:通常有多种情况会导致连接断开,比如客户端主动关闭、服务端宕机或者网络故障等。这个时候客户端就需要与服务端重新建立连接,但一般不能立刻完成重连,而是要等待固定的间隔后再发起重连,避免服务端的连接回收不及时,而客户端瞬间重连的请求太多而把服务端的连接数占满。服务端如何处理请求同步阻塞方式(BIO)客户端每发一次请求,服务端就生成一个线程去处理。当客户端同时发起的请求很多事,服务端需要创建很多的线程去处理每一个请求,如果达到了系统最大的线程数瓶颈,新来的请求就没法处理了。BIO 适用于连接数比较小的业务场景,这样的话不至于系统中没有可用线程去处理请求。这种方式写的程序也比较简单直观,易于理解。同步非阻塞(NIO)客户端每发一次请求,服务端并不是每次都创建一个新线程来处理,而是通过 I/O 多路复用技术进行处理。就是把多个 I/O 的阻塞复用到听一个 select 的阻塞上,从而使系统在单线程的情况下可以同时处理多个客户端请求。这种方式的优势是开销小,不用为每个请求创建一个线程,可以节省系统开销。NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。异步非阻塞(AIO)客户端只需要发起一个 I/O 操作然后立即返回,等 I/O 操作真正完成以后,客户端会得到 I/O 操作完成的通知,此时客户端只需要对数据进行处理就好了,不需要进行实际的 I/O 读写操作,因为真正的 I/O 读取或者写入操作已经由内核完成了。这种方式的优势是客户端无需等待,不存在阻塞等待问题。AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难难度最大,程序也不易于理解。建议最为稳妥的方式是使用成熟的开源方案,比如 Netty、MINA 等,它们都是经过业界大规模应用后,被充分论证是很可靠的方案。数据传输采用什么协议无论是开放的还是私有的协议,都必须定义一个“契约”,以便服务消费和服务提供者之间能够达成共识。服务消费者按照契约,对传输的数据进行编码,然后通过网络传输过去;服务提供者从网络上接收到数据后,按照契约,对传输的数据进行解码,然后处理请求,再把处理后的结果进行编码,通过网络传输返回给服务消费者;服务消费者再对返回的结果进行解码,最终得到服务提供者处理后的返回值。HTTP 协议消息头Server 代表是服务端服务器类型Content-Length 代表返回数据的长度Content-Type 代表返回数据的类型消息体具体的返回结果数据该如何序列化和反序列化一般数据在网络中进行传输,都要先在发送方一段对数据进行编码,经过网络传输到达另一段后,再对数据进行解码,这个过程就是序列化和反序列化常用的序列化方式分为两类:文本类如 XML/JSON 等,二进制类如 PB/Thrift 等,而具体采用哪种序列化方式,主要取决于三个方面的因素。支持数据结构类型的丰富度。数据结构种类支持的越多越好,这样的话对于使用者来说在编程时更加友好,有些序列化框架如 Hessian 2.0还支持复杂的数据结构比如 Map、List等。跨语言支持。性能。主要看两点,一个是序列化后的压缩比,一个是序列化的速度。以常用的 PB 序列化和 JSON 序列化协议为例来对比分析,PB序列化的压缩比和速度都要比 JSON 序列化高很多,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合;而 JSON序列化虽然性能要差一些,但可读性更好,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合适对外部提供服务。总结通信框架:它主要解决客户端和服务端如何建立连接、管理连接以及服务端如何处理请求的问题。通信协议:它主要解决客户端和服务端采用哪些数据传输协议的问题。序列化和反序列化:它主要解决客户端和服务端采用哪种数据编码的问题。这三部分就组成了一个完成的RPC 调用框架,通信框架提供了基础的通信能力,通信协议描述了通信契约,而序列化和反序列化则用于数据的编/解码。一个通信框架可以适配多种通信协议,也可以采用多种序列化和反序列化的格式,比如服务话框架 不仅支持 Dubbo 协议,还支持 RMI 协议、HTTP 协议等,而且还支持多种序列化和反序列化格式,比如 JSON、Hession 2.0 以及 Java 序列化等。4、如何监控微服务调用在谈论监控微服务监控调用前,首先要搞清楚三个问题:监控的对象是什么?具体监控哪些指标?从哪些维度进行监控?监控对象用户端监控:通常是指业务直接对用户提供的功能的监控。接口监控:通常是指业务提供的功能所以来的具体 RPC 接口监控。资源监控:通常是指某个接口依赖的资源的监控。(eg:Redis 来存储关注列表,对 Redis 的监控就属于资源监控。)基础监控:通常是指对服务器本身的健康状况的监控。(eg: CPU、MEM、I/O、网卡带宽等)监控指标1、请求量实时请求量(QPS Queries Per Second):即每秒查询次数来衡量,反映了服务调用的实时变化情况统计请求量(PV Page View):即一段时间内用户的访问量来衡量,eg:一天的 PV 代表了服务一天的请求量,通常用来统计报表2、响应时间:大多数情况下,可以用一段时间内所有调用的平均耗时来反应请求的响应时间。但它只代表了请求的平均快慢情况,有时候我们更关心慢请求的数量。为此需要把响应时间划分为多个区间,比如0~10ms、10ms~50ms、50ms~100ms、100ms~500ms、500ms 以上这五个区间,其中 500ms 以上这个区间内的请求数就代表了慢请求量,正常情况下,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数会大幅增加,可能平均耗时并不能反映出这一变化。除此之外,还可以从P90、P95、P99、P999 角度来监控请求的响应时间在 500ms 以内,它代表了请求的服务质量,即 SLA。3、错误率:通常用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用接口返回错误码为 503 的比率来表示。监控维度全局维度:从整体角度监控对象的请求量、平均耗时以及错误率,全局维度的监控一般是为了让你对监控对象的调用情况有个整体了解。分机房维度:为了业务高可用,服务部署不止一个机房,因为不同机房地域的不同,同一个监控对象的各种指标可能会相差很大。单机维度:同一个机房内部,可能由于采购年份和批次不的不同,各种指标也不一样。时间维度:同一个监控对象,在每天的同一时刻各种指标通常也不会一样,这种差异要么是由业务变更导致,要么是运营活动导致。为了了解监控对象各种指标的变化,通常需要与一天前、一周前、一个月前,甚至三个月前比较。核心维度:业务上一般会依据重要性成都对监控对象进行分级,最简单的是分成核心业务和非核心业务。核心业务和非核心业务在部署上必须隔离,分开监控,这样才能对核心业务做重点保障。对于一个微服务来说,必须要明确监控哪些对象、哪些指标,并且还要从不同的维度进行监控,才能掌握微服务的调用情况。监控系统原理数据采集:收集到每一次调用的详细信息,包括调用的响应时间、调用是否成功、调用的发起者和接收者分别是谁,这个过程叫做数据采集。数据传输:采集到数据之后,要把数据通过一定的方式传输给数据处理中心进行处理,这个过程叫做数据出传输。数据处理:数据传输过来后,数据处理中心再按照服务的维度进行聚合,计算出不同服务的请求量、响应时间以及错误率等信息并存储起来,这个过程叫做数据处理。数据展示:通过接口或者 DashBoard 的形式对外展示服务的调用情况,这个过程叫做数据展示。数据采集服务主动上报代理收集:这种处理方式通过服务调用后把调用的详细信息记录到本地日志文件中,然后再通过代理去解析本地日志文件,然后再上报服务的调用信息。不管是哪种方式,首先要考虑的问题就是采样率,也就是采集数据的频率。一般来说,采样率越高,监控的实时性就越高,精确度也越高。但采样对系统本身的性能也会有一定的影响,尤其是采集后的数据需要写到本地磁盘的时候,过高的采样率会导致系统写入的 I/O 过高,进而会影响到正常的服务调用。所以合理的采样率是数据采集的关键,最好是可以动态控制采样率,在系统比较空闲的时候加大采样率,追求监控的实时性与精确度;在系统负载比较高的时候减少采样率,追求监控的可用性与系统的稳定性。数据传输UDP传输:这种处理方式是数据处理单元提供服务器的请求地址,数据采集后通过 UDP 协议与服务器建立连接,然后把数据发送过去。Kafka传输:这种处理方式是数据采集后发送都指定的 Topic,然后数据处理单元再订阅对应的 Topic,就可以从 Kafka 消息队列中读取对应的数据。无论哪种传输方式,数据格式十分重要,尤其是对带宽敏感以及解析性能要求比较高的场景,一般数据传输时采用的数据格式有两种:二进制协议,最常用的就是 PB 对象文本协议,最常用的就是 JSON 字符串数据处理接口维度聚合:把实时收到的数据按照调用的节点维度聚合在一起,这样就可以得到每个接口的实时请求、平均耗时等信息。机器维度聚合:把实时收到的数据按照调用的节点维度聚合在一起,这样就可以从单机维度去查看每个接口的实时请求量、平均耗时等信息。聚合后的数据需要持久化到数据库中存储,所选用的数据库一般分为两种:索引数据库:比如 Elasticsearcher,以倒排索引的数据结构存书,需要查询的时候,根据索引来查询。时序数据库:比如 OpenTSDB,以时序序列数据的方式存储,查询的时候按照时序如 1min、5min 等维度查询数据展示曲线图:监控变化趋势。饼状图:监控占比分布。格子图:主要坐一些细粒度的监控。总结服务监控子啊微服务改造过程中的重要性不言而喻,没有强大的监控能力,改造成微服务架构后,就无法掌控各个不同服务的情况,在遇到调用失败时,如果不能快速发现系统的问题,对于业务来说就是一场灾难。搭建一个服务监控系统,设计数据采集、数据传输、数据处理、数据展示等多个环节,每个环节都需要根据自己的业务特点选择合适的解决方案在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多5、如何追踪微服务调用跟踪记录一次用户请求都发起了哪些调用,经过哪些服务处理,并且记录每一次调用所涉及的详细信息,这时候如果发生调用失败,就可以通过这个日志快速定位是在哪个环节出了问题。服务追踪的作用优化系统瓶颈通过记录调用经过的每一条链路上的耗时,可以快速定位整个系统的瓶颈点在哪里。可能出现的原因如下:运营商网络延迟网关系统异常某个服务异常缓存或者数据库异常通过服务追踪,可以从全局视角上去观察,找出整个系统的瓶颈点所在,然后做出针对性的优化优化链路调用通过服务追踪可以分析调用所经过的路径,然后评估是否合理一般业务都会在多个数据中心都部署服务,以实现异地容灾,这个时候经常会出现一种状况就是服务 A 调用了另外一个数据中心的服务B,而没有调用同处于一个数据中心的服务B。跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到了30ms以上,这对于有些业务几乎是不可接受的。通过对调用链路进行分析,可以找出跨数据中的服务调用,从而进行优化,尽量规避这总情况出现。生成网络拓扑通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。除此之外,在网络拓扑图上还可以把服务调用的详细信息也标出来,也能起到服务监控的作用。透明传输数据除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息。比如业务想做一些A/B 测试,这时候就想通过服务追踪系统,把 A/B 测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能够统一进行A/B 测试。服务追踪原理服务追踪鼻祖:Google 发布的一篇的论文Dapper, [a Large-Scale Distributed Systems Tracing Infrastructure核心理念:通过一个全局唯一的 ID将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题、分析调用数据并统计各种系统指标可以说后面的诞生各种服务追踪系统都是基于 Dapper 衍生出来的,比较有名的有 Twitter的Zipkin、阿里的鹰眼、美团的MTrace等。讲解下服务追踪系统中几个最基本概念traceId:用于标识某一次具体的请求ID。spanId:用于标识一次 RPC 调用在分布式请求中的位置。annotation:用于业务自定义埋点数据,可以是业务感兴趣的上上传到后端的数据,比如一次请求的用户 UID。traceId 是用于串联某一次请求在系统中经过的所有路径,spanId 是用于区分系统不同服务之间调用的先后关系,而annotation 是用于业务自定义一些自己感兴趣的数据,在上传 traceId 和 spanId 这些基本信息之外,添加一些自己感兴趣的信息。服务追踪系统实现上面是服务追踪系统架构图,一个服务追踪系统可以分三层:数据采集层:负责数据埋点并上报数据处理层:负责数据的存储与计算数据展示层:负责数据的图形化展示数据采集层作用:在系统的各个不同的模块中尽心埋点,采集数据并上报给数据处理层进行处理。CS(Client Send)阶段 : 客户端发起请求,并生成调用的上下文。SR(Server Recieve)阶段 : 服务端接收请求,并生成上下文。SS(Server Send)阶段 :服务端返回请求,这个阶段会将服务端上下文数据上报,下面这张图可以说明上报的数据有:traceId=123456,spanId=0.1,appKey=B,method=B.method,start=103,duration=38.CR(Client Recieve)阶段 :客户端接收返回结果,这个阶段会将客户端上下文数据上报,上报的数据有:traceid=123456,spanId=0.1,appKey=A,method=B.method,start=103,duration=38。数据处理层作用:把数据上报的数据按需计算,然后落地存储供查询使用实时数据处理:要求计算效率比较高,一般要对收集的链路数据能够在秒级别完成聚合计算,以供实时查询针对实时数据处理,一般使用 Storm 或者 Spack Streaming 来对链路数据进行实时聚合加工,存储一拜是用 OLTP 数据仓库,比如 HBase,使用 traceId 作为 RowKey,能天然地把一条调用链聚合在一起,提高查询效率。离线数据处理:要求计算效率相对没那么高,一般能在小时级别完成链路数据的聚合计算即可,一般用作汇总统计。针对离线数据处理,一般通过运行 MapReduce 或者 Spark 批处理程序来对链路数据进行离线计算,存储一般使用 Hive数据展示作用:将处理后的链路信息以图形化的方式展示给用户和做故障定位调用链路图(eg:Zipkin)服务整体情况:服务总耗时、服务调用的网络深度、每一层经过的系统,以及多少次调用。下图展示的一次调用,总耗时 209.323ms,经过了 5个不同系统模块,调用深度为 7 层,共发生了 2调用拓扑图(Pinpoint)调用拓扑图是一种全局视野,在实际项目中,主要用作全局监控,用户发现系统异常的点,从而快速做出决策。比如,某一个服务突然出现异常,那么在调用链路拓扑图中可以看出对这个服务的调用耗时都变高了,可以用红色的图样标出来,用作监控报警。总结服务追踪能够帮助查询一次用户请求在系统中的具体执行路径,以及每一条路径下的上下游的详细情况,对于追查问题十分有用。实现一个服务追踪系统,设计数据采集、数据处理和数据展示三个流程,有多种实现方式,具体采取某一种要根据自己的业务情况来选择。6、微服务治理的手段有哪些一次服务调用,服务提供者、注册中心、网络这三者都可能会有问题,此时服务消费者应该如何处理才能确保调用成功呢?这就是服务治理要解决的问题。节点管理服务调用失败一般是由两类原因引起的服务提供者自身出现问题,比如服务器宕机、进程意外退出等网络问题,如服务提供者、注册中心、服务消费者这三者任意两者之间的网络问题 无论是服务哪种原因,都有两种节点管理手段:注册中心主动摘除机制这种机制要求服务提供者定时的主动向注册中心汇报心跳,注册中心根据服务提供者节点最近一次汇报心跳的时间与上一次汇报心跳时间做比较,如果超出一定时间,就认为服务提供者出现问题,继而把节点从服务列表中摘除,并把最近的可用服务节点列表推送给服务消费者。服务消费者摘除机制虽然注册中心主动摘除机制可以解决服务提供者节点异常的问题,但如果是因为注册中心与服务提供者之间的网络出现异常,最坏的情况是注册中心会把服务节点全部摘除,导致服务消费者没有可能的服务节点调用,但其实这时候提供者本身是正常的。所以,将存活探测机制用在服务消费者这一端更合理,如果服务消费者调用服务提供者节点失败,就将这个节点从内存保存的可用夫提供者节点列表一处。负载均衡算法常用的负载均衡算法主要包括以下几种:随机算法(均匀)轮询算法(按照固定的权重,对可用服务节点进行轮询)最少活跃调用算法(性能理论最优)一致性 Hash 算法(相同参数的请求总是发到同一服务节点)服务路由对于服务消费者而言,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则决定。所谓的路由规则,就是通过一定的规则如条件表达式或者正则表达式来限定服务节点的选择范围。为什么要指定路由规则呢?主要有两个原因:业务存在灰度发布的需求比如,服务提供者做了功能变更,但希望先只让部分人群使用,然后根据这部分人群的使用反馈,再来决定是否全量发布。多机房就近访问的需求跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到了30ms以上,这对于有些业务几乎是不可接受的,所以就要一次服务调用尽量选择同一个 IDC 内部节点,从而减少网络耗时开销,提高性能。这时一般可以通过 IP 段规则来控制访问,在选择服务节点时,优先选择同一 IP段的节点。那么路由规则该如何配置?静态配置:服务消费者本地存放调用的路由规则,如果改变,需重新上线才能生效动态配置:路由规则存放在配置中心,服务消费者定期去请求注册中心来保持同步,要想改变消费者的路由配置,可以通过修改注册中心的配置,服务消费者在下一个同步周期之后,就会请求注册中心更新配置,从而实现动态变更服务容错常用的手段主要有以下几种:FailOver:失败自动切换(调用失败或者超时,可以设置重试次数)FailBack:失败通知(调用失败或者超时,不立即发起重试,而是根据失败的详细信息,来决定后续的执行策略)FailCache:失败缓存(调用失败或者超时,不立即发起重试,而是隔一段时间后再次尝试发起调用)FailFirst:快速失败(调用一次失败后,不再充实,一般非核心业务的调用,会采取快速失败策略,调用失败后一般就记录下失败日志就返回了)一般对于幂等的调用可以选择 FailOver 或者 FailCache,非幂等的调用可以选择 Failback 或者 FailFast在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 总结节点管理是从服务节点健康状态角度来考虑,负载均衡和服务路由是从服务节点访问优先级角度来考虑,而服务容错是从调用的健康状态来考虑,可谓殊途同归。在实际的微服务架构中,上面的服务治理手段一般都会在服务框架中默认即成,比如 阿里的 Dubbo、微博开源的服务架构 Motan等。原文出处:https://my.oschina.net/rosett… ...

October 21, 2018 · 2 min · jiezi

关于MySQL 通用查询日志和慢查询日志分析

MySQL中的日志包括:错误日志、二进制日志、通用查询日志、慢查询日志等等。这里主要介绍下比较常用的两个功能:通用查询日志和慢查询日志。1)通用查询日志:记录建立的客户端连接和执行的语句。2)慢查询日志:记录所有执行时间超过longquerytime秒的所有查询或者不使用索引的查询(1)通用查询日志在学习通用日志查询时,需要知道两个数据库中的常用命令:1) show variables like ‘%general%’;可以查看,当前的通用日志查询是否开启,如果general_log的值为ON则为开启,为OFF则为关闭(默认情况下是关闭的)。1) show variables like ‘%log_output%’;查看当前慢查询日志输出的格式,可以是FILE(存储在数数据库的数据文件中的hostname.log),也可以是TABLE(存储在数据库中的mysql.general_log)问题:如何开启MySQL通用查询日志,以及如何设置要输出的通用日志输出格式呢?开启通用日志查询: set global general_log=on;关闭通用日志查询: set global general_log=off;设置通用日志输出为表方式: set global log_output=’TABLE’;设置通用日志输出为文件方式: set global log_output=’FILE’;设置通用日志输出为表和文件方式:set global log_output=’FILE,TABLE’;(注意:上述命令只对当前生效,当MySQL重启失效,如果要永久生效,需要配置 my.cnf)my.cnf文件的配置如下:general_log=1 #为1表示开启通用日志查询,值为0表示关闭通用日志查询log_output=FILE,TABLE#设置通用日志的输出格式为文件和表(2)慢查询日志MySQL的慢查询日志是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中(日志可以写入文件或者数据库表,如果对性能要求高的话,建议写文件)。默认情况下,MySQL数据库是不开启慢查询日志的,long_query_time的默认值为10(即10秒,通常设置为1秒),即运行10秒以上的语句是慢查询语句。一般来说,慢查询发生在大表(比如:一个表的数据量有几百万),且查询条件的字段没有建立索引,此时,要匹配查询条件的字段会进行全表扫描,耗时查过long_query_time,则为慢查询语句。问题:如何查看当前慢查询日志的开启情况?在MySQL中输入命令:show variables like ‘%quer%’;主要掌握以下的几个参数:(1)slow_query_log的值为ON为开启慢查询日志,OFF则为关闭慢查询日志。(2)slow_query_log_file 的值是记录的慢查询日志到文件中(注意:默认名为主机名.log,慢查询日志是否写入指定文件中,需要指定慢查询的输出日志格式为文件,相关命令为:show variables like ‘%log_output%’;去查看输出的格式)。(3)long_query_time 指定了慢查询的阈值,即如果执行语句的时间超过该阈值则为慢查询语句,默认值为10秒。(4)log_queries_not_using_indexes 如果值设置为ON,则会记录所有没有利用索引的查询(注意:如果只是将log_queries_not_using_indexes设置为ON,而将slow_query_log设置为OFF,此时该设置也不会生效,即该设置生效的前提是slow_query_log的值设置为ON),一般在性能调优的时候会暂时开启。问题:设置MySQL慢查询的输出日志格式为文件还是表,或者两者都有?通过命令:show variables like ‘%log_output%’;通过log_output的值可以查看到输出的格式,上面的值为TABLE。当然,我们也可以设置输出的格式为文本,或者同时记录文本和数据库表中,设置的命令如下:慢查询日志输出到表中(即mysql.slow_log)set globallog_output=’TABLE’;慢查询日志仅输出到文本中(即:slow_query_log_file指定的文件)setglobal log_output=’FILE’;在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多慢查询日志同时输出到文本和表中setglobal log_output=’FILE,TABLE’; 关于慢查询日志的表中的数据个文本中的数据格式分析:慢查询的日志记录myql.slow_log表中,格式如下:慢查询的日志记录到hostname.log文件中,格式如下:可以看到,不管是表还是文件,都具体记录了:是那条语句导致慢查询(sql_text),该慢查询语句的查询时间(query_time),锁表时间(Lock_time),以及扫描过的行数(rows_examined)等信息。问题:如何查询当前慢查询的语句的个数?在MySQL中有一个变量专门记录当前慢查询语句的个数:输入命令:show global status like ‘%slow%’;(注意:上述所有命令,如果都是通过MySQL的shell将参数设置进去,如果重启MySQL,所有设置好的参数将失效,如果想要永久的生效,需要将配置参数写入my.cnf文件中)。补充知识点:如何利用MySQL自带的慢查询日志分析工具mysqldumpslow分析日志?perlmysqldumpslow –s c –t 10 slow-query.log具体参数设置如下:-s 表示按何种方式排序,c、t、l、r分别是按照记录次数、时间、查询时间、返回的记录数来排序,ac、at、al、ar,表示相应的倒叙;-t 表示top的意思,后面跟着的数据表示返回前面多少条;-g 后面可以写正则表达式匹配,大小写不敏感。上述中的参数含义如下:Count:414 语句出现了414次;Time=3.51s(1454) 执行最长时间为3.51s,累计总耗费时间1454s;Lock=0.0s(0) 等待锁最长时间为0s,累计等待锁耗费时间为0s;Rows=2194.9(9097604) 发送给客户端最多的行数为2194.9,累计发送给客户端的函数为90976404(注意:mysqldumpslow脚本是用perl语言写的,具体mysqldumpslow的用法后期再讲)问题:实际在学习过程中,如何得知设置的慢查询是有效的?很简单,我们可以手动产生一条慢查询语句,比如,如果我们的慢查询log_query_time的值设置为1,则我们可以执行如下语句:selectsleep(1);该条语句即是慢查询语句,之后,便可以在相应的日志输出文件或表中去查看是否有该条语句。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..原文:https://my.oschina.net/u/3575…

October 11, 2018 · 1 min · jiezi

深入理解高并发下分布式事务的解决方案

1、什么是分布式事务分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。2、分布式事务的产生的原因2.1、数据库分库分表当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。2.2、应用SOA化所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。以上两种情况表象不同,但是本质相同,都是因为要操作的数据库变多了!3、事务的ACID特性3.1、原子性(A)所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。3.2、一致性(C)事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。3.3、隔离性(I)所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。3.4、持久性(D)所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。4、分布式事务的应用场景4.1、支付最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。4.2、在线下单买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。5、常见的分布式事务解决方案5.1、基于XA协议的两阶段提交XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。5.2、消息事务+最终一致性所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:1、A系统向消息中间件发送一条预备消息2、消息中间件保存预备消息并返回成功3、A执行本地事务4、A发送提交消息给消息中间件通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:步骤一出错,则整个事务失败,不会执行A的本地操作步骤二出错,则整个事务失败,不会执行A的本地操作步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。5.3、TCC编程模式所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多6、总结分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力

October 10, 2018 · 1 min · jiezi

深入理解高并发下分布式事务的方案

编辑推荐:本文主要从分布式的原因,事务特性,和解决方案中深入理解了分布式事务,希望对您的学习有所帮助。1、什么是分布式事务分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。2、分布式事务的产生的原因2.1、数据库分库分表当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。2.2、应用SOA化所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。以上两种情况表象不同,但是本质相同,都是因为要操作的数据库变多了!3、事务的ACID特性3.1、原子性(A)所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。3.2、一致性(C)事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。3.3、隔离性(I)所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。3.4、持久性(D)所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。4、分布式事务的应用场景4.1、支付最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。4.2、在线下单买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。5、常见的分布式事务解决方案5.1、基于XA协议的两阶段提交XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。5.2、消息事务+最终一致性所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:1、A系统向消息中间件发送一条预备消息2、消息中间件保存预备消息并返回成功3、A执行本地事务4、A发送提交消息给消息中间件通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:步骤一出错,则整个事务失败,不会执行A的本地操作步骤二出错,则整个事务失败,不会执行A的本地操作步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。5.3、TCC编程模式所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。6、总结分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力。顺便在此给大家推荐一个Java架构方面的交流学习群:698581634,里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,主要针对Java开发人员提升自己,突破瓶颈,相信你来学习,会有提升和收获。在这个群里会有你需要的内容 朋友们请抓紧时间加入进来吧。

October 10, 2018 · 1 min · jiezi

百亿级日志系统架构设计及优化'

本文将从海量日志系统在优化、部署、监控方向如何更适应业务的需求入手,重点从多种日志系统的架构设计对比;后续调优过程:横向扩展与纵向扩展,分集群,数据分治,重写数据链路等实际现象与问题展开。日志系统架构基准有过项目开发经验的朋友都知道:从平台的最初搭建到实现核心业务,都需要有日志平台为各种业务保驾护航。如上图所示,对于一个简单的日志应用场景,通常会准备 master/slave 两个应用。我们只需运行一个 Shell 脚本,便可查看是否存在错误信息。随着业务复杂度的增加,应用场景也会变得复杂。虽然监控系统能够显示某台机器或者某个应用的错误。然而在实际的生产环境中,由于实施了隔离,一旦在上图下侧的红框内某个应用出现了 Bug,则无法访问到其对应的日志,也就谈不上将日志取出了。另外,有些深度依赖日志平台的应用,也可能在日志产生的时候就直接采集走,进而删除掉原始的日志文件。这些场景给我们日志系统的维护都带来了难度。参考 Logstash,一般会有两种日志业务流程:正常情况下的简单流程为:应用产生日志→根据预定义的日志文件大小或时间间隔,通过执行 Logrotation,不断刷新出新的文件→定期查看→定期删除。复杂应用场景的流程为:应用产生日志→采集→传输→按需过滤与转换→存储→分析与查看。我们可以从实时性和错误分析两个维度来区分不同的日志数据场景:实时,一般适用于我们常说的一级应用,如:直接面向用户的应用。我们可以自定义各类关键字,以方便在出现各种 error 或 exception 时,相关业务人员能够在第一时间被通知到。准实时,一般适用于一些项目管理的平台,如:在需要填写工时的时候出现了宕机,但这并不影响工资的发放。平台在几分钟后完成重启,我们可以再登录填写,该情况并不造成原则性的影响。因此,我们可以将其列为准实时的级别。除了直接采集错误与异常,我们还需要进行分析。例如:仅知道某人的体重是没什么意义的,但是如果增加了性别和身高两个指标,那么我们就可以判断出此人的体重是否为标准体重。也就是说:如果能给出多个指标,就可以对庞大的数据进行去噪,然后通过回归分析,让采集到的数据更有意义。此外,我们还要不断地去还原数字的真实性。特别是对于实时的一级应用,我们要能快速地让用户明白他们所碰到现象的真实含义。例如:商家在上架时错把商品的价格标签 100 元标成了 10 元。这会导致商品马上被抢购一空。但是这种现象并非是业务的问题,很难被发现,因此我们只能通过日志数据进行逻辑分析,及时反馈以保证在几十秒之后将库存修改为零,从而有效地解决此问题。可见,在此应用场景中,实时分析就显得非常有用。最后是追溯,我们需要在获取历史信息的同时,实现跨时间维度的对比与总结,那么追溯就能够在各种应用中发挥其关联性作用了。上述提及的各个要素都是我们管理日志的基准。如上图所示,我们的日志系统采用的是开源的 ELK 模式:ElasticSearch(后简称 ES),负责后端集中存储与查询工作。单独的 Beats 负责日志的搜集。FileBeat 则改进了 Logstash 的资源占用问题;TopBeat 负责搜集监控资源,类似系统命令 top 去获取 CPU 的性能。由于日志服务对于业务来说仅起到了维稳和保障的作用,而且我们需要实现快速、轻量的数据采集与传输,因此不应占用服务器太多资源。在方式上我们采用的是插件模式,包括:input 插件、output 插件、以及中间负责传输过滤的插件。这些插件有着不同的规则和自己的格式,支持着各种安全性的传输。日志系统优化思路有了上述日志的架构,我们针对各种实际的应用场景,进一步提出了四个方面的优化思路:基础优化内存:如何分配内存、垃圾回收、增加缓存和锁。网络:网络传输序列化、增加压缩、策略、散列、不同协议与格式。CPU:用多线程提高利用率和负载。此处利用率和负载是两个不同的概念:利用率:在用满一个核后再用下一个内核,利用率是逐步升高的。负载:一下子把八个核全用上了,则负载虽然是满的,但是利用率很低。即,每核都被占用了,但是所占用的资源却不多,计算率比较低下。磁盘:尝试通过文件合并,减少碎片文件的产生,并减少寻道次数。同时在系统级别,通过修改设置,关闭各种无用的服务。平台扩展做加减法,或称替代方案:无论是互联网应用,还是日常应用,我们在查询时都增加了分布式缓存,以有效提升查询的效率。另外,我们将不被平台使用到的地方直接关闭或去除。纵向扩展:如增加扩展磁盘和内存。横向扩展:加减/平行扩展,使用分布式集群。数据分治根据数据的不同维度,对数据进行分类、分级。例如:我们从日志中区分error、info、和 debug,甚至将 info 和 debug 级别的日志直接过滤掉。数据热点:例如:某种日志数据在白天的某个时间段内呈现暴涨趋势,而晚上只是平稳产生。我们就可以根据此热点情况将它们取出来单独处理,以打散热点。系统降级我们在对整体业务进行有效区分的基础上,通过制定一些降级方案,将部分不重要的功能停掉,以满足核心业务。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多日志系统优化实践面对持续增长的数据量,我们虽然增加了许多资源,但是并不能从根本上解决问题。特别体现在如下三方面:日志产生量庞大,每天有几百亿条。由于生产环境隔离,我们无法直接查看到数据。代理资源限制,我们的各种日志采集和系统资源采集操作,不可超过业务资源的一个核。一级业务架构我们日志系统的层次相对比较清晰,可简单分为数据接入、数据存储和数据可视化三大块。具体包括:Rsyslog,是目前我们所接触到的采集工具中最节省性能的一种。Kafka,具有持久化的作用。当然它在使用到达一定数据量级时,会出现 Bug。Fluentd,它与 Rsyslog 类似,也是一种日志的传输工具,但是它更偏向传输服务。ES 和 Kibana。该架构在实现上会用到 Golang、Ruby、Java、JS 等不同的语言。在后期改造时,我们会将符合 Key-Value 模式的数据快速地导入 HBase 之中。基于 HBase 的自身特点,我们实现了它在内存层的 B+ 树,并且持久化到我们的磁盘之上,从而达到了理想的快速插入的速度。这也正是我们愿意选择 HBase 作为日志方案的原因。二级业务架构我们直接来看二级业务架构的功能图,它是由如下流程串联而成的:在完成了数据采集之后,为了节省自己占用磁盘的空间,许多应用会完全依赖于我们的日志系统。因此在数据采集完以后,我们增加了一个持久缓存。完成缓存之后系统执行传输。传输的过程包括:过滤和转换,这个过程可以进行数据抽稀。值得强调的是:如果业务方尽早合作并给予我们一些约定的话,我们就能够通过格式化来实现结构化的数据。随后执行的是分流,其主要包括两大块:一种是 A 来源的数据走 A 通道,B 来源的数据走 B 通道。另一种是让 A 数据流入到我们的存储设备,并触发保护机制。即为了保障存储系统,我们额外增加了一个队列。例如:队列为 100,里面的一个 chunk 为 256 兆,我们现在设置高水位为 0.7、低水位为 0.3。在写操作的堆积时,由于我们设置了 0.7,即 100 兆赫。那么在一个 256 兆会堆积到 70 个 chunk 时,我们往该存储平台的写速度就已经跟不上了。此时高水位点会被触发,不允许继续写入,直到整个写入过程把该 chunk 消化掉,并降至 30 个时,方可继续往里写入。我们就是用该保护机制来保护后台以及存储设备的。接着是存储,由于整个数据流的量会比较大,因此在存储环节主要执行的是存储的索引、压缩、和查询。最后是 UI 的一些分析算法,运用 SQL 的一些查询语句进行简单、快速地查询。通常从采集(logstash/rsyslog/heka/filebeat)到面向缓存的 Kafka 是一种典型的宽依赖。所谓宽依赖,是指每个 App 都可能跟每个 Broker 相关联。在 Kafka 处,每次传输都要在哈希之后,再把数据写到每个 Broker 上。而窄依赖,则是其每一个 Fluentd 进程都只对应一个 Broker 的过程。最终通过宽依赖过程写入到 ES。采集如 Rsyslog 不但占用资源最少,而且可以添加各种规则,它还能支持像 TSL、SSL 之类的安全协议。Filebeat 轻量,在版本 5.x 中,Elasticsearch 具有解析的能力(像 Logstash 过滤器)— Ingest。这也就意味着可以将数据直接用 Filebeat 推送到 Elasticsearch,并让 Elasticsearch 既做解析的事情,又做存储的事情。Kafka接着是 Kafka,Kafka 主要实现的是顺序存储,它通过 topic 和消息队列的机制,实现了快速地数据存储。而它的缺点:由于所有的数据都向 Kafka 写入,会导致 topic 过多,引发磁盘竞争,进而严重拖累 Kafka 的性能。另外,如果所有的数据都使用统一标签的话,由于不知道所采集到的数据具体类别,我们将很难实现对数据的分治。因此,在后面的优化传输机制方面,我们改造并自己实现了顺序存储的过程,进而解决了一定要做持久化这一安全保障的需求。FluentdFluentd 有点类似于 Logstash,它的文档和插件非常齐全。其多种插件可保证直接对接到 Hadoop 或 ES。就接入而言,我们可以采用 Fluentd 到 Fluentd 的方式。即在原有一层数据接入的基础上,再接一次 Fluentd。同时它也支持安全传输。当然我们在后面也对它进行了重点优化。ES+Kibana最后我们用到了 ES 和 Kibana。ES 的优势在于通过 Lucene 实现了快速的倒排索引。由于大量的日志是非结构化的,因此我们使用 ES 的 Lucene 进行包装,以满足普通用户执行非结构化日志的搜索。而 Kibana 则基于 Lucene 提供可视化显示工具。问题定位与解决下面介绍一下我们碰到过的问题和现象,如下这些都是我们着手优化的出发点:传输服务器的 CPU 利用率低下,每个核的负载不饱满。传输服务器 Full gc 的频次过高。由于我们是使用 Ruby 来实现的过程,其内存默认设置的数据量有时会过大。存储服务器出现单波峰现象,即存储服务器磁盘有时会突然出现性能直线骤升或骤降。频繁触发高水位。如前所述的高水位保护机制,一旦存储磁盘触发了高水位,则不再提供服务,只能等待人工进行磁盘“清洗”。如果 ES 的一台机器“挂”了,则集群就 hang 住了。即当发现某台机器无法通讯时,集群会认为它“挂”了,则快速启动数据恢复。而如果正值系统繁忙之时,则此类数据恢复的操作会更加拖累系统的整体性能。由于所有数据都被写入 Kafka,而我们只用到了一个 topic,这就造成了每一类数据都要经过不一定与之相关的规则链,并进行不一定适用的规则判断,因此数据的传输效率整体被降低了。Fluentd 的 host 轮询机制造成高水位频发。由于 Fluentd 在与 ES 对接时遵循一个默认策略:首选前五台进行数据写入,即与前五台的前五个接口交互。在我们的生产环境中,Fluentd 是用 CRuby 写的。每一个进程属于一个 Fluentd 进程,且每一个进程都会对应一个 host 文件。而该 host 文件的前五个默认值即为 ES 的写入入口,因此所有机器都会去找这五个入口。倘若有一台机器宕机,则会轮询到下一台。如此直接造成了高水位的频繁出现、和写入速度的下降。众所周知,对日志的查询是一种低频次的查询,即只有在出现问题时才会去查看。但是在实际操作中,我们往往通过检索的方式全部取出,因此意义不大。另外 ES 为了达到较好的性能,会将数据存储在 raid0 中,存储的时间跨度往往会超过 7 天,因此其成本也比较高。通过对数据的实时线分析,我们发现并未达到写入/写出的平衡状态。为了提高 Fluentd 的利用率,我们用 Kafka 去数据的时候提高了量,原来是 5 兆,现在我们改到了 6 兆。如果只是单纯传输,不论计算的话,其实可以改更高。只不过因为我们考虑到这里包含了计算的一些东西,所以只提到了 6 兆。我们的 Fluentd 是基于 JRuby 的,因为 JRuby 可以多线程,但是我们的 CRuby 没有任何意义。为了提高内存,我把 Ruby 所有的内存机制了解了一下,就是散列的一些 host 文件,因为我们每个进程都选前五列就可以了,我多开了几个口。ES 的优化这一块,在上 ES 之前,我们已经有人做过一次优化了。因为基于我刚才说的有时候日志量很高,有时候日志量很少。我们会考虑做动态配置。因为 ES 就是支持动态配置的,所以它动态配置的时候,我们在某些场景下可以提高它的写入速度,某些场景下可以支持它的这种查询效率。我们可以尝试去做一些动态配置负载。改造一:存储降低降低存储在整体架构上并没有太大变化,我们只是在传输到 Fluentd 时把天数降下来,改成了一天。同时,我们直接进行了分流,把数据往 Hadoop 里写,而把一些符合 Kibana 的数据直接放入 ES。上面提过,日志查询是低频次的,一般需要查询两天以上数据的可能性很小,因此我们降低存储是非常有意义的。改造二:数据分治我们在日志文件节点数较少(机器数量小于 5 台)的情况下,去掉了 Kafka 层。由于 Fluentd 可以支持数据和大文件存储,因此数据能够被持久化地存入磁盘。我们给每个应用都直接对应了一个 tag,以方便各个应用对应到自己的 tag、遵循自己的固定规则、并最终写入 ES,这样就方便了出现问题的各自定位。另外,我们运用延迟计算和文件切分也能快速地找到问题的根源。因此我们节约了 Kafka 和 ES 各种计算资源。在实际操作中,由于 HBase 不用去做 raid,它自己完全能够控制磁盘的写入,因此我们进行了数据压缩。就其效果而言,ES 的存储开销大幅降低。在后期,我们也尝试过一种更为极端的方案:让用户直接通过客户端的 Shell 去查询数据,并采用本地缓存的留存机制。优化效果优化的效果如下:服务器资源的有效利用。在实施了新的方案之后,我们省了很多服务器,而且单台服务器的存储资源也节省了 15%。单核处理每秒原来能够传输 3000 条,实施后提升到了 1.5~1.8 万条。而且,在服务器单独空跑,即不加任何计算时,单核每秒能传输近 3 万条。很少触发 ES 保护机制。原因就是我们已把数据分流出来了。以前历史数据只能存 7 天,由于我们节省了服务器,因此我们现在可以存储更长时间的数据。而且,对于一些他人查询过的日志,我们也会根据最初的策略,有选择性地保留下来,以便追溯。日志系统优化总结关于日志平台优化,我总结了如下几点:由于日志是低频次的,我们把历史数据存入了廉价存储之中,普通用户需要的时候,我们再导到 ES 里,通过 Kibana 的前端界面便可快速查询到。而对于程序员来说,则不需要到 ES 便可直接查询到。数据存在的时间越长,则意义越小。我们根据实际情况制定了有效的、留存有意义数据的策略。顺序写盘替代内存。例如:区别于平常的随机写盘,我们在操作读写一个流文件时采取的是按顺序写数据的模式。而在存储量大的时候,则应当考虑 SSD。特别是在 ES 遇到限流时,使用 SSD 可以提升 ES 的性能。提前定制规范,从而能够有效解决后期分析等工作。日志格式如上图所示,常用的日志格式类型包括:uuid、timestamp、host 等。特别是 host,由于日志会涉及到几百个节点,有了 host 类型,我们就能判定是哪台机器上的标准。而图中其他的环境变量类型,则能够有效地追溯到一些历史的信息。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 日志方案如上图所示,我们通过 Rsyslog 可以直接将采集端的数据写入文件或数据库之中。当然,对于一些暂时用不上的日志,我们不一定非要实施过滤传输的规则。如上图,Fluentd 也有一些传输的规则,包括:Fluentd 可以直接对接 Fluentd,也可以直接对接 MongoDB、MySQL 等。另外,我们也有一些组件可以快速地对接插件和系统,例如让 Fluentd 和 Rsyslog 能够直接连到 ES 上。这是我个人给大家定制的一些最基本的基线,我认为日志从采集、缓存、传输、存储,到最终可视化,分成了三套基线。采集到存储是最简单的一个,像 Rsyslog 到 hdfs 或者其他 filesystem,我们有这种情况。比较常见的情况,就是从采集、传输、到存储可视化,然后形成最终我们现在最复杂的一套系统,大家可以根据实际情况取舍。最后是我考虑到一个实际情况,假如这个案例,我们尽可能少的占有服务器,然后传输需要过滤转换,日志可以比较简单,符合这种 Key value(KV)格式。我们可以按照取了一个 Rsyslog、取了一个 Fluentd、取了一个 Hbase,取了一个 echars 等这么一个方式做一个方案就可以了。我觉得 Rsyslog、Fluentd、heka 这些都可以做采集。然后传输这块有 Fluentd 传输,因为 Fluentd 和 Kafka 到插件非常灵活可以直接对接我们很多存储设备,也可以对应很多的文件、连 ES 都可以。可视化可以用 Kibana,主要是跟 ES 结合得比较紧密,它们结合在一起需要一点学习成本。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. 原文连接:https://blog.csdn.net/yunzhaj… ...

October 8, 2018 · 2 min · jiezi

Spring中注解大全和应用

@Controller@RestController:@Service@Autowired@RequestMapping@RequestParam@ModelAttribute@Cacheable@CacheEvict@Resource@PostConstruct@PreDestroy@Repository@Component @Scope@SessionAttributes@Required@Qualifier1. @Controller标识一个该类是Spring MVC controller处理器,用来创建处理http请求的对象.1@Controller2public class TestController {3 @RequestMapping("/test")4 public String test(Map<String,Object> map){56 return “hello”;7 }8}2. @RestControllerSpring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式。1@RestController2public class TestController {3 @RequestMapping("/test")4 public String test(Map<String,Object> map){56 return “hello”;7 }8}3. @Service用于标注业务层组件,说白了就是加入你有一个用注解的方式把这个类注入到spring配置中4. @Autowired用来装配bean,都可以写在字段上,或者方法上。默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,例如:@Autowired(required=false)5. @RequestMapping类定义处: 提供初步的请求映射信息,相对于 WEB 应用的根目录。方法处: 提供进一步的细分映射信息,相对于类定义处的 URL。用过RequestMapping的同学都知道,他有非常多的作用,因此详细的用法我会在下一篇文章专门讲述,请关注公众号哦,以免错过。6. @RequestParam用于将请求参数区数据映射到功能处理方法的参数上例如1public Resp test(@RequestParam Integer id){2 return Resp.success(customerInfoService.fetch(id));3 }这个id就是要接收从接口传递过来的参数id的值的,如果接口传递过来的参数名和你接收的不一致,也可以如下1public Resp test(@RequestParam(value=“course_id”) Integer id){2 return Resp.success(customerInfoService.fetch(id));3 }其中course_id就是接口传递的参数,id就是映射course_id的参数名7. @ModelAttribute使用地方有三种:1. 标记在方法上。标记在方法上,会在每一个@RequestMapping标注的方法前执行,如果有返回值,则自动>将该返回值加入到ModelMap中。A.在有返回的方法上:当ModelAttribute设置了value,方法返回的值会以这个value为key,以参数接受到的值作为value,存入到Model中,如下面的方法执行之后,最终相当于 model.addAttribute(“user_name”, name);假如 @ModelAttribute没有自定义value,则相当于model.addAttribute(“name”, name);1@ModelAttribute(value=“user_name”)2 public String before2(@RequestParam(required = false) String name, Model model) {3 System.out.println(“进入了2:” + name);4 return name;5 }B.在没返回的方法上:需要手动model.add方法1 @ModelAttribute2 public void before(@RequestParam(required = false) Integer age, Model model) {3 model.addAttribute(“age”, age);4 System.out.println(“进入了1:” + age);5 }我们在当前类下建一个请求方法: 1@RequestMapping(value="/mod") 2 public Resp mod( 3 @RequestParam(required = false) String name, 4 @RequestParam(required = false) Integer age, 5 Model model){ 6 System.out.println(“进入mod”); 7 System.out.println(“参数接受的数值{name="+name+";age="+age+”}"); 8 System.out.println(“model传过来的值:"+model); 9 return Resp.success(“1”);10 }在浏览器中输入访问地址并且加上参数:http://localhost:8081/api/test/mod?name=我是小菜&age=12最终输出如下:1进入了1:402进入了2:我是小菜3进入mod4参数接受的数值{name=我是小菜;age=12}5model传过来的值:{age=40, user_name=我是小菜}2. 标记在方法的参数上。标记在方法的参数上,会将客户端传递过来的参数按名称注入到指定对象中,并且会将这个对象自动加入ModelMap中,便于View层使用.我们在上面的类中加入一个方法如下 1@RequestMapping(value="/mod2”) 2 public Resp mod2(@ModelAttribute(“user_name”) String user_name, 3 @ModelAttribute(“name”) String name, 4 @ModelAttribute(“age”) Integer age,Model model){ 5 System.out.println(“进入mod2”); 6 System.out.println(“user_name:"+user_name); 7 System.out.println(“name:"+name); 8 System.out.println(“age:"+age); 9 System.out.println(“model:"+model);10 return Resp.success(“1”);11 }在浏览器中输入访问地址并且加上参数:http://localhost:8081/api/test/mod2?name=我是小菜&age=12最终输出:1进入了1:402进入了2:我是小菜3进入mod24user_name:我是小菜5name:我是小菜6age:407model:{user_name=我是小菜, org.springframework.validation.BindingResult.user_name=org.springframework.validation.BeanPropertyBindingResult: 0 errors, name=我是小菜, org.springframework.validation.BindingResult.name=org.springframework.validation.BeanPropertyBindingResult: 0 errors, age=40, org.springframework.validation.BindingResult.age=org.springframework.validation.BeanPropertyBindingResult: 0 errors}从结果就能看出,用在方法参数中的@ModelAttribute注解,实际上是一种接受参数并且自动放入Model对象中,便于使用。8. @Cacheable用来标记缓存查询。可用用于方法或者类中,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。参数列表参数解释例子value名称@Cacheable(value={”c1”,”c2”}keykey@Cacheable(value=”c1”,key=”#id”)condition条件@Cacheable(value=”c1”,condition=”#id=1”)比如@Cacheable(value=“UserCache”) 标识的是当调用了标记了这个注解的方法时,逻辑默认加上从缓存中获取结果的逻辑,如果缓存中没有数据,则执行用户编写查询逻辑,查询成功之后,同时将结果放入缓存中。但凡说到缓存,都是key-value的形式的,因此key就是方法中的参数(id),value就是查询的结果,而命名空间UserCache是在spring*.xml中定义.1@Cacheable(value=“UserCache”)// 使用了一个缓存名叫 accountCache 2public Account getUserAge(int id) { 3 //这里不用写缓存的逻辑,直接按正常业务逻辑走即可,4 //缓存通过切面自动切入 5 int age=getUser(id); 6 return age; 7} 9. @CacheEvict用来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。 @CacheEvict(value=”UserCache”)参数列表参数解释例子value名称@CachEvict(value={”c1”,”c2”}keykey@CachEvict(value=”c1”,key=”#id”)condition缓存的条件,可以为空allEntries是否清空所有缓存内容@CachEvict(value=”c1”,allEntries=true)beforeInvocation是否在方法执行前就清空@CachEvict(value=”c1”,beforeInvocation=true)10. @Resource@Resource的作用相当于@Autowired只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了。@Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。@Resource装配顺序:如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配;11. @PostConstruct用来标记是在项目启动的时候执行这个方法。用来修饰一个非静态的void()方法也就是spring容器启动时就执行,多用于一些全局配置、数据字典之类的加载被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。PreDestroy()方法在destroy()方法执行执行之后执12. @PreDestroy被@PreDestroy修饰的方法会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。被@PreDestroy修饰的方法会在destroy()方法之后运行,在Servlet被彻底卸载之前13. @Repository用于标注数据访问组件,即DAO组件14. @Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注15. @Scope用来配置 spring bean 的作用域,它标识 bean 的作用域。默认值是单例singleton:单例模式,全局有且仅有一个实例prototype:原型模式,每次获取Bean的时候会有一个新的实例request:request表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效session:session作用域表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效global session:只在portal应用中有用,给每一个 global http session 新建一个Bean实例。16. @SessionAttributes默认情况下Spring MVC将模型中的数据存储到request域中。当一个请求结束后,数据就失效了。如果要跨页面使用。那么需要使用到session。而@SessionAttributes注解就可以使得模型中的数据存储一份到session域中参数:names:这是一个字符串数组。里面应写需要存储到session中数据的名称。types:根据指定参数的类型,将模型中对应类型的参数存储到session中value:和names是一样的。 1@Controller 2@SessionAttributes(value={“names”},types={Integer.class}) 3public class ScopeService { 4 @RequestMapping("/testSession”) 5 public String test(Map<String,Object> map){ 6 map.put(“names”, Arrays.asList(“a”,“b”,“c”)); 7 map.put(“age”, 12); 8 return “hello”; 9 }10}17. @Required适用于bean属性setter方法,并表示受影响的bean属性必须在XML配置文件在配置时进行填充。否则,容器会抛出一个BeanInitializationException异常。18. @Qualifier当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用 @Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。—觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能 ...

October 7, 2018 · 2 min · jiezi

Springboot 2.0 - 集成redis

最近在入门SpringBoot,然后在感慨 SpringBoot较于Spring真的方便多时,顺便记录下自己在集成redis时的一些想法。从springboot官网查看redis的依赖包<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>操作redis /* 操作k-v都是字符串的 / @Autowired StringRedisTemplate stringRedisTemplet; / 操作k-v都是对象的 /@AutowiredRedisTemplate redisTemplate;redis的包中提供了两个可以操作方法,根据不同类型的值相对应选择。两个操作方法对应的redis操作都是相同的stringRedisTemplet.opsForValue() // 字符串stringRedisTemplet.opsForList() // 列表stringRedisTemplet.opsForSet() // 集合stringRedisTemplet.opsForHash() // 哈希stringRedisTemplet.opsForZSet() // 有序集合修改数据的存储方式在StringRedisTemplet中,默认都是存储字符串的形式;在RedisTemplet中,值可以是某个对象,而redis默认把对象序列化后存储在redis中(所以存放的对象默认情况下需要序列化)如果需要更改数据的存储方式,如采用json来存储在redis中,而不是以序列化后的形式。1)自己创建一个RedisTemplate实例,在该实例中自己定义json的序列化格式(org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer)// 这里传入的是employee对象(employee 要求可以序列化)Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);2)把定义的格式放进自己定义的RedisTemplate实例中RedisTemplate<Object,Employee> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);// 定义格式Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = newJackson2JsonRedisSerializer<Employee>(Employee.class);// 放入RedisTemplate实例中template.setDefaultSerializer(jackson2JsonRedisSerializer);参考代码:@Beanpublic RedisTemplate<Object,Employee> employeeRedisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException{ RedisTemplate<Object,Employee> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(jackson2JsonRedisSerializer); return template;} 原理: @Configuration @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {“redisTemplate”} ) // 在容器当前没有redisTemplate时运行 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template;}@Bean@ConditionalOnMissingBean // 在容器当前没有stringRedisTemplate时运行public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template;}}如果你自己定义了RedisTemplate后并添加@Bean注解,(要在配置类中定义),那么默认的RedisTemplate就不会被添加到容器中,运行的就是自己定义的ReidsTemplate实例,而你在实例中自己定义了序列化格式,所以就会以你采用的格式定义存放在redis中的对象。更改默认的缓冲springboot默认提供基于注解的缓冲,只要在主程序类(xxxApplication)标注@EnableCaching,缓冲注解有@Cachingable、@CachingEvict、@CachingPut,并且该缓冲默认使用的是ConcurrentHashMapCacheManager当引入redis的starter后,容器中保存的是RedisCacheManager ,RedisCacheManager创建RedisCache作为缓冲组件,RedisCache通过操纵redis缓冲数据在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多修改redis缓冲的序列化机制在SpringBoot中,如果要修改序列化机制,可以直接建立一个配置类,在配置类中自定义CacheManager,在CacheManager中可以自定义序列化的规则,默认的序列化规则是采用jdk的序列化注:在SpringBoot 1.5.6 和SpringBoot 2.0.5 的版本中自定义CacheManager存在差异参考代码:// springboot 1.x的版本public RedisCacheManager employeeCacheManager(RedisConnectionFactory redisConnectionFactory){// 1、自定义RedisTemplateRedisTemplate<Object,Employee> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(jackson2JsonRedisSerializer);// 2、自定义RedisCacheManagerRedisCacheManager cacheManager = new RedisCacheManager(template);cacheManager.setUsePrefix(true); // 会将CacheName作为key的前缀return cacheManager;}// springboot 2.x的版本/** serializeKeysWith() 修改key的序列化规则,这里采用的是StringRedisSerializer()* serializeValuesWith() 修改value的序列化规则,这里采用的是 Jackson2JsonRedisSerializer<Employee>(Employee.class)* @param factory* @return */@Beanpublic RedisCacheManager employeeCacheManager(RedisConnectionFactory redisConnectionFactory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() . serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<Employee>(Employee.class))); RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build(); return cacheManager; }tip:可以通过查看各版本的org.springframework.data.redis.cache.RedisCacheConfiguration去自定义CacheManager.因为不同版本的SpringBoot对应的Redis版本也是不同的,所以要重写时可以查看官方是怎么定义CacheManager,才知道怎样去自定义CacheManager。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《 乐趣区 》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. ...

October 5, 2018 · 1 min · jiezi

优秀架构师必须掌握的架构思维

一、抽象思维如果要问软件研发/系统架构中最重要的能力是什么,我会毫不犹豫回答是抽象能力。抽象(abstraction)这个词大家经常听到,但是真正理解和能讲清楚什么是抽象的人少之又少。抽象其实是这样定义的:对某种事物进行简化表示或描述的过程,抽象让我们关注要素,隐藏额外细节。举一个例子,见下图:你看到什么?你看到的是一扇门,对不对?你看到的不是木头,也不是碳原子,这个门就是抽象,而木头或者碳原子是细节。另外你可以看到门上有个门把手,你看到的不是铁,也不是铁原子,门把手就是抽象,铁和铁原子是细节。在系统架构和设计中,抽象帮助我们从大处着眼(get our mind about big picture),隐藏细节(temporarily hide details)。抽象能力的强弱,直接决定我们所能解决问题的复杂性和规模大小。下图是我们小时候玩的积木,我发现小时候喜欢玩搭积木的,并且搭得快和好的小朋友,一般抽象能力都比较强。上图右边的积木城堡就是抽象,这个城堡如果你细看的话,它其实还是由若干个子模块组成,这些模块是子抽象单元,左边的各种形状的积木是细节。搭积木的时候,小朋友脑袋里头先有一个城堡的大图(抽象),然后他/她大脑里头会有一个初步的子模块分解(潜意识中完成),然用利用积木搭建每一个子模块,最终拼装出最后的城堡。这里头有一个自顶向下的分治设计,然后自底向上的组合过程,这个分治思维非常重要,我们后面会讲。我认为软件系统架构设计和小朋友搭积木无本质差异,只是解决的问题域和规模不同罢了。架构师先要在大脑中形成抽象概念,然后是子模块分解,然后是依次实现子模块,最后将子模块拼装组合起来,形成最后系统。所以我常说编程和架构设计就是搭积木,优秀的架构师受职业习惯影响,眼睛里看到的世界都是模块化拼装组合式的。抽象能力不仅对软件系统架构设计重要,对建筑、商业、管理等人类其它领域活动同样非常重要。其实可以这样认为,我们生存的世界都是在抽象的基础上构建起来的,离开抽象人类将寸步难行。这里顺便提一下抽象层次跳跃问题,这个在开发中是蛮普遍的。有经验的程序员写代码会保持抽象层次的一致性,代码读起来像讲故事,比较清晰易于理解;而没有经验的程序员会有明显的抽象层次跳跃问题,代码读起来就比较累,这个是抽象能力不足造成。举个例子:一个电商网站在处理订单时,一般会走这样一个流程:1.更新库存(InventoryUpdate)2.打折计算(Discounting)3.支付卡校验(PaycardVerification)4.支付(Pay)5.送货(Shipping)上述流程中的抽象是在同一个层次上的,比较清晰易于理解,但是没有经验的程序员在实现这个流程的时候,代码层次会跳,比方说主流程到支付卡校验一块,他的代码会突然跳出一行某银行API远程调用,这个就是抽象跳跃,银行API调用是细节,应该封装在PaycardVerification这个抽象里头。二、分层思维除了抽象,分层也是我们应对和管理复杂性的基本思维武器,如下图,为了构建一套复杂系统,我们把整个系统划分成若干个层次,每一层专注解决某个领域的问题,并向上提供服务。有些层次是纵向的,它贯穿所有其它层次,称为共享层。分层也可以认为是抽象的一种方式,将系统抽象分解成若干层次化的模块。分层架构的案例很多,一个中小型的Spring Web应用程序,我们一般会设计成三层架构:操作系统是经典的分层架构,如下图:TCP/IP协议栈也是经典的分层架构,如下图:如果你关注人类文明演化史,你会发现今天的人类世界也是以分层方式一层层搭建和演化出来的。今天的互联网系统可以认为是现代文明的一个层次,其上是基于互联网的现代商业,其下是现代电子工业基础设施,诸如此类。三、分治思维分而治之(divide and combine或者split and merge)也是应对和管理复杂性的一般性方法,下图展示一个分治的思维流程:对于一个无法一次解决的大问题,我们会先把大问题分解成若干个子问题,如果子问题还无法直接解决,则继续分解成子子问题,直到可以直接解决的程度,这个是分解(divide)的过程;然后将子子问题的解组合拼装成子问题的解,再将子问题的解组合拼装成原问题的解,这个是组合(combine)的过程。面试时为了考察候选人的分治思维,我经常会面一个分治题:给你一台8G内存/500G磁盘空间的普通电脑,如何对一个100G的大文件进行排序?假定文件中都是字符串记录,一行约100个字符。这是一个典型的分治问题,100G的大文件肯定无法一次加载到内存直接排序,所以需要先切分成若干小问题来解决。那么8G内存的计算机一次大概能排多大的数据量,可以在有限的时间内排完呢?也就是100G的大文件要怎么切法,切成多少份比较合适?这个是考察候选人的时间空间复杂度估算能力,需要一定的计算机组织和算法功底,也需要一定实战经验和sense。实际上8G内存的话,操作系统要用掉一部分,如果用Java开发排序程序,大致JVM可用24G内存,基于一般的经验值,一次排1G左右的数据应该没有问题(我实际在计算机上干过1G数据的排序,是OK的)。所以100G的文件需要先切分成100份,每份1G,这样每个子文件可以直接加载到内存进行排序。对于1G数据量的字符串排序,采用Java里头提供的快速排序算法是比较合适的。好,经过有限时间的排序(取决于计算机性能,快的一天内能排完),假定100个1G的文件都已经排好了,相当于现在硬盘上有100个已经排好序的文件,但是我们最终需要的是一个排好序的文件,下面该怎么做?这个时候我们需要把已经解决的子问题组合起来,合并成我们需要的最终结果文件。这个时候该采用什么算法呢?这里考察候选人对外排序和归并排序算法的掌握程度,我们可以将100个排好序的文件进行两两归并排序,这样不断重复,我们就会得到50个排好序的文件,每个大小是2G。然后再两两归并,不断重复,直到最后两个文件归并成目标文件,这个文件就是100G并且是排好序的。因为是外排序+归并排序,每次只需要读取当前索引指向的文件记录到内存,进行比较,小的那个输出到目标文件,内存占用极少。另外,上面的算法是两路归并,也可以采用多路归并,甚至是采用堆排序进行优化,但是总体分治思路没有变化。在此我向大家推荐一个架构学习交流群。交流学习群号:897889510 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多总体上这是一个非常好的面试题,除了考察候选人的分治思维之外,还考察对各种排序算法(快排,外排序,归并排序,堆排序)的理解,计算的时间空间复杂度估算,计算机的内外存特性和组织,文件操作等等。实际上能完全回答清楚这个问题的候选人极少,如果有幸被我面到一个,我会如获至宝,因为这个人有成长为优秀架构师的潜质。另外,递归也是一种特殊的分治技术,掌握递归技术的开发人员,相当于掌握了一种强大的编程武器,可以解决一些一般开发人员无法解决的问题。比方说最近我的团队在研发一款新的服务框架,其中包括契约解析器(parser),代码生产器(code generator),序列化器(serializer)等组件,里头大量需要用到递归的思维和技术,没有这个思维的开发人员就干不了这个事情。所以我在面试候选人的时候,一般都会出递归相关的编程题,考察候选人的递归思维。大自然中递归结构比比皆是,如下图,大家有兴趣不妨思考,大自然通过递归给我们人类何种启示?四、演化思维社区里头经常有人在讨论:架构是设计出来的?还是演化出验认为,架构既是设计出来的,同时也是演化出来的,对于互联网系统,基本上可以说是三分设计,七分演化,而且是在设计中演化,在演化中设计,一个不断迭代的过程。在互联网软件系统的整个生命周期过程中,前期的设计和开发大致只占三分,在后面的七分时间里,架构师需要根据用户的反馈对架构进行不断的调整。我认为架构师除了要利用自身的架构设计能力,同时也要学会借助用户反馈和进化的力量,推动架构的持续演进,这个就是演化式架构思维。当然一开始的架构设计非常重要,架构定系统基本就成型了,不容马虎。同时,优秀的架构师深知,能够不断应对环境变化的系统,才是有生命力的系统,架构的好坏,很大部分取决于它应对变化的灵活性。所以具有演化式思维的架构师,能够在一开始设计时就考虑到后续架构的演化特性,并且将灵活应对变化的能力作为架构设计的主要考量。当前,社区正在兴起一种新的架构方法学演化式架构,微服务架构就是一种典型的演化式架构,它能够快速响应市场用户需求的变化,而单块架构就缺乏这种灵活性。马丁·福乐曾经在其博客上给出过一张微服务架构的演化路线图[附录8.2],可以用来解释设计式思维和演化式思维的差异,如下图所示:上面的路线是一开始就直奔微服务架构,其实背后体现的是设计式架构的思维,认为架构师可以完全设计整个系统和它的演化方向。马丁认为这种做法风险非常高,一个是成本高昂,另外一个是刚开始架构师对业务域理解不深,无法清晰划分领域边界,开发出来的系统很可能无法满足用户需求。下面的路线是从单块架构开始,随着架构师对业务域理解的不断深入,也随着业务和团队规模的不断扩大,渐进式地把单块架构拆分成微服务架构的思路,这就是演化式架构的思维。如果你观察现实世界中一些互联网公司(例如eBay,阿里,Netflix等等)的系统架构,大部分走得都是演化式架构的路线。下图是建筑的演化史,在每个阶段,你可以看到设计的影子,但如果时间线拉得足够长,演化的特性就出来了。五、如何培养架构设计思维良好的架构设计思维的培养,离不开工作中大量高质量项目的实战锻炼,然后是平时的学习、思考和提炼总结。另外,基本的架构设计思维,其实在我们大学计算机课程(比如数据结构和算法)中可以找到影子,只不过当时以学习为主,问题域比较小和理想化。所以大学教育其实非常重要,基本的架构设计思维在那个时候就已经埋下种子,后面工程实践中进一步消化和应用,随着经验的积累,我们能够解决的问题域复杂性和规模逐渐变大,但基本的武器还是抽象、分层和分治等思维。我认为一个架构师的成长高度和他大学期间的思维习惯的养成关系密切。我所知道世界一流的互联网公司,例如谷歌等,招聘工程师新人时,对数据结构和算法的要求可以用苛刻来形容,这个可以理解,谷歌级别公司要解决的问题都是超级复杂的,基本思维功底薄弱根本无法应对。对于工作经验<5年的工程师新手,如果你大学时代是属于荒废型的,建议工作之余把相关课程再好好自学一把。个人推荐参考美国Berkeley大学的数据结构课程CS61B[附录8.1]进行学习,对建立抽象编程思维非常有帮助,我本人在研究生阶段自学过这门课程,现在回想起来确实受益匪浅,注意该课程中的所有Lab/Homework/Project都要实际动手做一遍,才有好的效果。我当年自学的是CS61B 2006秋季版的课程,上图是课程Logo对于演化设计思维,当前的大学教育其实培养很少,相反,当前大学教育大都采用脱离现实场景的简化理想模型,有些还是固定答案的应试教学,这种方式会造成学生思维确定化,不利于培养演化式设计思维。我个人的体会,演化式设计思维更多在实际工作中通过实战锻炼和培养。结论1.架构的本质是管理复杂性,抽象、分层、分治和演化思维是架构师征服复杂性的四种根本性武器。2.掌握了抽象、分层、分治和演化这四种基本的武器,你可以设计小到一个类,一个模块,一个子系统,或者一个中型的系统,也可以大到一个公司的基础平台架构,微服务架构,技术体系架构,甚至是组织架构,业务架构等等。3.架构设计不是静态的,而是动态演化的。只有能够不断应对环境变化的系统,才是有生命力的系统。所以即使你掌握了抽象、分层和分治这三种基本思维,仍然需要演化式思维,在设计的同时,借助反馈和进化的力量推动架构的持续演进。4.架构师在关注技术,开发应用的同时,需要定期梳理自己的架构设计思维,积累时间长了,你看待世界事物的方式会发生根本性变化,你会发现我们生活其中的世界,其实也是在抽象、分层、分治和演化的基础上构建起来的。另外架构设计思维的形成,会对你的系统架构设计能力产生重大影响。可以说对抽象、分层、分治和演化掌握的深度和灵活应用的水平,直接决定架构师所能解决问题域的复杂性和规模大小,是区分普通应用型架构师和平台型/系统型架构师的一个分水岭。

October 1, 2018 · 1 min · jiezi

基于 Spring Cloud 完整的微服务架构实战

技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构应用组件启动项目使用 Docker 快速启动配置 Docker 环境mvn clean package 打包项目及 Docker 镜像在项目根目录下执行 docker-compose up -d 启动所有项目本地手动启动配置 rabbitmq修改 hosts 将主机名指向到本地127.0.0.1 registry config monitor rabbitmq auth-service或者修改各服务配置文件中的相应主机名为本地 ip启动 registry、config、monitor、zipkin启动 gateway、auth-service、svca-service、svcb-service项目预览注册中心访问 http://localhost:8761/ 默认账号 user,密码 password监控访问 http://localhost:8040/ 默认账号 admin,密码 admin控制面板bp.jpg应用注册历史Turbine Hystrix面板应用信息、健康状况、垃圾回收等详情计数器查看和修改环境变量管理 Logback 日志级别查看并使用 JMX查看线程认证历史查看 Http 请求轨迹Hystrix 面板链路跟踪访问 http://localhost:9411/ 默认账号 admin,密码 admin控制面板链路跟踪明细服务依赖关系RabbitMQ 监控Docker 启动访问 http://localhost:15673/ 默认账号 guest,密码 guest(本地 rabbit 管理系统默认端口15672)接口测试在此我向大家推荐一个架构学习交流群。交流学习群号:897889510 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多接口测试1.获取 Tokencurl -X POST -vu client:secret http://localhost:8060/uaa/oauth/token -H “Accept: application/json” -d “password=password&username=anil&grant_type=password&scope=read%20write"返回如下格式数据:{“access_token”: “eac56504-c4f0-4706-b72e-3dc3acdf45e9”,“token_type”: “bearer”,“refresh_token”: “da1007dc-683c-4309-965d-370b15aa4aeb”,“expires_in”: 3599,“scope”: “read write”}2.使用 access token 访问 service a 接口curl -i -H “Authorization: Bearer eac56504-c4f0-4706-b72e-3dc3acdf45e9” http://localhost:8060/svca 返回如下数据:svca-service (172.18.0.8:8080)===>name:zhangxdsvcb-service (172.18.0.2:8070)===>Say Hello 3.使用 access token 访问 service b 接口curl -i -H “Authorization: Bearer eac56504-c4f0-4706-b72e-3dc3acdf45e9” http://localhost:8060/svcb 返回如下数据:svcb-service (172.18.0.2:8070)===>Say Hello 4.使用 refresh token 刷新 tokencurl -X POST -vu client:secret http://localhost:8060/uaa/oauth/token -H “Accept: application/json” -d “grant_type=refresh_token&refresh_token=da1007dc-683c-4309-965d-370b15aa4aeb” 返回更新后的 Token:{“access_token”: “63ff57ce-f140-482e-ba7e-b6f29df35c88”,“token_type”: “bearer”,“refresh_token”: “da1007dc-683c-4309-965d-370b15aa4aeb”,“expires_in”: 3599,“scope”: “read write”} 5.刷新配置curl -X POST -vu user:password http://localhost:8888/bus/refresh ...

September 30, 2018 · 3 min · jiezi

分布式的系统核心是什么——日志

什么是日志?日志就是按照时间顺序追加的、完全有序的记录序列,其实就是一种特殊的文件格式,文件是一个字节数组,而这里日志是一个记录数据,只是相对于文件来说,这里每条记录都是按照时间的相对顺序排列的,可以说日志是最简单的一种存储模型,读取一般都是从左到右,例如消息队列,一般是线性写入log文件,消费者顺序从offset开始读取。由于日志本身固有的特性,记录从左向右开始顺序插入,也就意味着左边的记录相较于右边的记录“更老”, 也就是说我们可以不用依赖于系统时钟,这个特性对于分布式系统来说相当重要。日志的应用日志在数据库中的应用日志是什么时候出现已经无从得知,可能是概念上来讲太简单。在数据库领域中日志更多的是用于在系统crash的时候同步数据以及索引等,例如MySQL中的redo log,redo log是一种基于磁盘的数据结构,用于在系统挂掉的时候保证数据的正确性、完整性,也叫预写日志,例如在一个事物的执行过程中,首先会写redo log,然后才会应用实际的更改,这样当系统crash后恢复时就能够根据redo log进行重放从而恢复数据(在初始化的过程中,这个时候不会还没有客户端的连接)。日志也可以用于数据库主从之间的同步,因为本质上,数据库所有的操作记录都已经写入到了日志中,我们只要将日志同步到slave,并在slave重放就能够实现主从同步,这里也可以实现很多其他需要的组件,我们可以通过订阅redo log 从而拿到数据库所有的变更,从而实现个性化的业务逻辑,例如审计、缓存同步等等。日志在分布式系统中的应用分布式系统服务本质上就是关于状态的变更,这里可以理解为状态机,两个独立的进程(不依赖于外部环境,例如系统时钟、外部接口等)给定一致的输入将会产生一致的输出并最终保持一致的状态,而日志由于其固有的顺序性并不依赖系统时钟,正好可以用来解决变更有序性的问题。我们利用这个特性实现解决分布式系统中遇到的很多问题。例如RocketMQ中的备节点,主broker接收客户端的请求,并记录日志,然后实时同步到salve中,slave在本地重放,当master挂掉的时候,slave可以继续处理请求,例如拒绝写请求并继续处理读请求。日志中不仅仅可以记录数据,也可以直接记录操作,例如SQL语句。日志是解决一致性问题的关键数据结构,日志就像是操作序列,每一条记录代表一条指令,例如应用广泛的Paxos、Raft协议,都是基于日志构建起来的一致性协议。日志在Message Queue中的应用日志可以很方便的用于处理数据之间的流入流出,每一个数据源都可以产生自己的日志,这里数据源可以来自各个方面,例如某个事件流(页面点击、缓存刷新提醒、数据库binlog变更),我们可以将日志集中存储到一个集群中,订阅者可以根据offset来读取日志的每条记录,根据每条记录中的数据、操作应用自己的变更。在此我向大家推荐一个架构学习交流群。交流学习群号:897889510 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多这里的日志可以理解为消息队列,消息队列可以起到异步解耦、限流的作用。为什么说解耦呢?因为对于消费者、生产者来说,两个角色的职责都很清晰,就负责生产消息、消费消息,而不用关心下游、上游是谁,不管是来数据库的变更日志、某个事件也好,对于某一方来说我根本不需要关心,我只需要关注自己感兴趣的日志以及日志中的每条记录。我们知道数据库的QPS是一定的,而上层应用一般可以横向扩容,这个时候如果到了双11这种请求突然的场景,数据库会吃不消,那么我们就可以引入消息队列,将每个队数据库的操作写到日志中,由另外一个应用专门负责消费这些日志记录并应用到数据库中,而且就算数据库挂了,当恢复的时候也可以从上次消息的位置继续处理(RocketMQ和Kafka都支持Exactly Once语义),这里即使生产者的速度异于消费者的速度也不会有影响,日志在这里起到了缓冲的作用,它可以将所有的记录存储到日志中,并定时同步到slave节点,这样消息的积压能力能够得到很好的提升,因为写日志都是有master节点处理,读请求这里分为两种,一种是tail-read,就是说消费速度能够跟得上写入速度的,这种读可以直接走缓存,而另一种也就是落后于写入请求的消费者,这种可以从slave节点读取,这样通过IO隔离以及操作系统自带的一些文件策略,例如pagecache、缓存预读等,性能可以得到很大的提升。在此我向大家推荐一个架构学习交流群。交流学习群号:575745314 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。分布式系统中可横向扩展是一个相当重要的特性,加机器能解决的问题都不是问题。那么如何实现一个能够实现横向扩展的消息队列呢? 假如我们有一个单机的消息队列,随着topic数目的上升,IO、CPU、带宽等都会逐渐成为瓶颈,性能会慢慢下降,那么这里如何进行性能优化呢?1.topic/日志分片,本质上topic写入的消息就是日志的记录,那么随着写入的数量越多,单机会慢慢的成为瓶颈,这个时候我们可以将单个topic分为多个子topic,并将每个topic分配到不同的机器上,通过这种方式,对于那些消息量极大的topic就可以通过加机器解决,而对于一些消息量较少的可以分到到同一台机器或不进行分区2.group commit,例如Kafka的producer客户端,写入消息的时候,是先写入一个本地内存队列,然后将消息按照每个分区、节点汇总,进行批量提交,对于服务器端或者broker端,也可以利用这种方式,先写入pagecache,再定时刷盘,刷盘的方式可以根据业务决定,例如金融业务可能会采取同步刷盘的方式。3.规避无用的数据拷贝4.IO隔离结语日志在分布式系统中扮演了很重要的角色,是理解分布式系统各个组件的关键,随着理解的深入,我们发现很多分布式中间件都是基于日志进行构建的,例如Zookeeper、HDFS、Kafka、RocketMQ、Google Spanner等等,甚至于数据库,例如Redis、MySQL等等,其master-slave都是基于日志同步的方式,依赖共享的日志系统,我们可以实现很多系统: 节点间数据同步、并发更新数据顺序问题(一致性问题)、持久性(系统crash时能够通过其他节点继续提供服务)、分布式锁服务等等,相信慢慢的通过实践、以及大量的论文阅读之后,一定会有更深层次的理解。

September 30, 2018 · 1 min · jiezi

消息中间件—简谈Kafka中的NIO网络通信模型

摘要:很多人喜欢把RocketMQ与Kafka做对比,其实这两款消息队列的网络通信层还是比较相似的,本文就为大家简要地介绍下Kafka的NIO网络通信模型,通过对Kafka源码的分析来简述其Reactor的多线程网络通信模型和总体框架结构,同时简要介绍Kafka网络通信层的设计与具体实现。一、Kafka网络通信模型的整体框架概述Kafka的网络通信模型是基于NIO的Reactor多线程模型来设计的。这里先引用Kafka源码中注释的一段话:相信大家看了上面的这段引文注释后,大致可以了解到Kafka的网络通信层模型,主要采用了 1(1个Acceptor线程)+N(N个Processor线程)+M(M个业务处理线程) 。下面的表格简要的列举了下(这里先简单的看下后面还会详细说明):线程数线程名线程具体说明1kafka-socket-acceptor_%xAcceptor线程,负责监听Client端发起的请求Nkafka-network-thread_%dProcessor线程,负责对Socket进行读写Mkafka-request-handler-_%dWorker线程,处理具体的业务逻辑并生成Response返回Kafka网络通信层的完整框架图如下图所示:Kafka消息队列的通信层模型—1+N+M模型.png刚开始看到上面的这个框架图可能会有一些不太理解,并不要紧,这里可以先对Kafka的网络通信层框架结构有一个大致了解。本文后面会结合Kafka的部分重要源码来详细阐述上面的过程。这里可以简单总结一下其网络通信模型中的几个重要概念:(1), Acceptor :1个接收线程,负责监听新的连接请求,同时注册OP_ACCEPT 事件,将新的连接按照 “round robin” 方式交给对应的 Processor 线程处理;(2), Processor :N个处理器线程,其中每个 Processor 都有自己的 selector,它会向 Acceptor 分配的 SocketChannel 注册相应的 OP_READ 事件,N 的大小由 “num.networker.threads” 决定;(3), KafkaRequestHandler :M个请求处理线程,包含在线程池—KafkaRequestHandlerPool内部,从RequestChannel的全局请求队列—requestQueue中获取请求数据并交给KafkaApis处理,M的大小由 “num.io.threads” 决定;(4), RequestChannel :其为Kafka服务端的请求通道,该数据结构中包含了一个全局的请求队列 requestQueue和多个与Processor处理器相对应的响应队列responseQueue,提供给Processor与请求处理线程KafkaRequestHandler和KafkaApis交换数据的地方。(5), NetworkClient :其底层是对 Java NIO 进行相应的封装,位于Kafka的网络接口层。Kafka消息生产者对象—KafkaProducer的send方法主要调用NetworkClient完成消息发送;(6), SocketServer :其是一个NIO的服务,它同时启动一个Acceptor接收线程和多个Processor处理器线程。提供了一种典型的Reactor多线程模式,将接收客户端请求和处理请求相分离;(7), KafkaServer :代表了一个Kafka Broker的实例;其startup方法为实例启动的入口;(8), KafkaApis :Kafka的业务逻辑处理Api,负责处理不同类型的请求;比如 “发送消息”、 “获取消息偏移量—offset” 和 “处理心跳请求” 等;二、Kafka网络通信层的设计与具体实现这一节将结合Kafka网络通信层的源码来分析其设计与实现,这里主要详细介绍网络通信层的几个重要元素—SocketServer、Acceptor、Processor、RequestChannel和KafkaRequestHandler。本文分析的源码部分均基于Kafka的0.11.0版本。1、SocketServerSocketServer是接收客户端Socket请求连接、处理请求并返回处理结果的核心类,Acceptor及Processor的初始化、处理逻辑都是在这里实现的。在KafkaServer实例启动时会调用其startup的初始化方法,会初始化1个 Acceptor和N个Processor线程(每个EndPoint都会初始化,一般来说一个Server只会设置一个端口),其实现如下:2、AcceptorAcceptor是一个继承自抽象类AbstractServerThread的线程类。Acceptor的主要任务是监听并且接收客户端的请求,同时建立数据传输通道—SocketChannel,然后以轮询的方式交给一个后端的Processor线程处理(具体的方式是添加socketChannel至并发队列并唤醒Processor线程处理)。在该线程类中主要可以关注以下两个重要的变量:(1), nioSelector :通过NSelector.open()方法创建的变量,封装了JAVA NIO Selector的相关操作;(2), serverChannel :用于监听端口的服务端Socket套接字对象;下面来看下Acceptor主要的run方法的源码:在上面源码中可以看到,Acceptor线程启动后,首先会向用于监听端口的服务端套接字对象—ServerSocketChannel上注册OP_ACCEPT 事件。然后以轮询的方式等待所关注的事件发生。如果该事件发生,则调用accept()方法对OP_ACCEPT事件进行处理。这里,Processor是通过 round robin 方法选择的,这样可以保证后面多个Processor线程的负载基本均匀。Acceptor的accept()方法的作用主要如下:(1)通过SelectionKey取得与之对应的serverSocketChannel实例,并调用它的accept()方法与客户端建立连接;(2)调用connectionQuotas.inc()方法增加连接统计计数;并同时设置第(1)步中创建返回的socketChannel属性(如sendBufferSize、KeepAlive、TcpNoDelay、configureBlocking等)(3)将socketChannel交给processor.accept()方法进行处理。这里主要是将socketChannel加入Processor处理器的并发队列newConnections队列中,然后唤醒Processor线程从队列中获取socketChannel并处理。其中,newConnections会被Acceptor线程和Processor线程并发访问操作,所以newConnections是ConcurrentLinkedQueue队列(一个基于链接节点的无界线程安全队列)3、ProcessorProcessor同Acceptor一样,也是一个线程类,继承了抽象类AbstractServerThread。其主要是从客户端的请求中读取数据和将KafkaRequestHandler处理完响应结果返回给客户端。在该线程类中主要关注以下几个重要的变量:(1), newConnections :在上面的 Acceptor 一节中已经提到过,它是一种ConcurrentLinkedQueue[SocketChannel]类型的队列,用于保存新连接交由Processor处理的socketChannel;(2), inflightResponses :是一个Map[String, RequestChannel.Response]类型的集合,用于记录尚未发送的响应;(3), selector :是一个类型为KSelector变量,用于管理网络连接;下面先给出Processor处理器线程run方法执行的流程图:Kafk_Processor线程的处理流程图.png从上面的流程图中能够可以看出Processor处理器线程在其主流程中主要完成了这样子几步操作:(1), 处理newConnections队列中的socketChannel 。遍历取出队列中的每个socketChannel并将其在selector上注册OP_READ事件;(2), 处理RequestChannel中与当前Processor对应响应队列中的Response 。在这一步中会根据responseAction的类型(NoOpAction/SendAction/CloseConnectionAction)进行判断,若为“NoOpAction”,表示该连接对应的请求无需响应;若为“SendAction”,表示该Response需要发送给客户端,则会通过“selector.send”注册OP_WRITE事件,并且将该Response从responseQueue响应队列中移至inflightResponses集合中;“CloseConnectionAction”,表示该连接是要关闭的;(3), 调用selector.poll()方法进行处理 。该方法底层即为调用nioSelector.select()方法进行处理。(4), 处理已接受完成的数据包队列—completedReceives 。在processCompletedReceives方法中调用“requestChannel.sendRequest”方法将请求Request添加至requestChannel的全局请求队列—requestQueue中,等待KafkaRequestHandler来处理。同时,调用“selector.mute”方法取消与该请求对应的连接通道上的OP_READ事件;(5), 处理已发送完的队列—completedSends 。当已经完成将response发送给客户端,则将其从inflightResponses移除,同时通过调用“selector.unmute”方法为对应的连接通道重新注册OP_READ事件;(6), 处理断开连接的队列 。将该response从inflightResponses集合中移除,同时将connectionQuotas统计计数减1;4、RequestChannel在Kafka的网络通信层中,RequestChannel为Processor处理器线程与KafkaRequestHandler线程之间的数据交换提供了一个数据缓冲区,是通信过程中Request和Response缓存的地方。因此,其作用就是在通信中起到了一个数据缓冲队列的作用。Processor线程将读取到的请求添加至RequestChannel的全局请求队列—requestQueue中;KafkaRequestHandler线程从请求队列中获取并处理,处理完以后将Response添加至RequestChannel的响应队列—responseQueue中,并通过responseListeners唤醒对应的Processor线程,最后Processor线程从响应队列中取出后发送至客户端。5、KafkaRequestHandlerKafkaRequestHandler也是一种线程类,在KafkaServer实例启动时候会实例化一个线程池—KafkaRequestHandlerPool对象(包含了若干个KafkaRequestHandler线程),这些线程以守护线程的方式在后台运行。在KafkaRequestHandler的run方法中会循环地从RequestChannel中阻塞式读取request,读取后再交由KafkaApis来具体处理。6、KafkaApisKafkaApis是用于处理对通信网络传输过来的业务消息请求的中心转发组件。该组件反映出Kafka Broker Server可以提供哪些服务。三、总结仔细阅读Kafka的NIO网络通信层的源码过程中还是可以收获不少关于NIO网络通信模块的关键技术。Apache的任何一款开源中间件都有其设计独到之处,值得借鉴和学习。对于任何一位使用Kafka这款分布式消息队列的同学来说,如果能够在一定实践的基础上,再通过阅读其源码能起到更为深入理解的效果,对于大规模Kafka集群的性能调优和问题定位都大有裨益。对于刚接触Kafka的同学来说,想要自己掌握其NIO网络通信层模型的关键设计,还需要不断地使用本地环境进行debug调试和阅读源码反复思考。 ...

September 29, 2018 · 1 min · jiezi

dubbo负载均衡策略及对应源码分析

在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。我们还可以扩展自己的负责均衡策略,前提是你已经从一个小白变成了大牛,嘻嘻1、Random LoadBalance1.1 随机,按权重设置随机概率。1.2 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。1.3 源码分析 package com.alibaba.dubbo.rpc.cluster.loadbalance; import java.util.List; import java.util.Random; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; /** * random load balance. * * @author qianlei * @author william.liangf / public class RandomLoadBalance extends AbstractLoadBalance { public static final String NAME = “random”; private final Random random = new Random(); protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int totalWeight = 0; // 总权重 boolean sameWeight = true; // 权重是否都一样 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); totalWeight += weight; // 累计总权重 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; // 计算所有权重是否一样 } } if (totalWeight > 0 && ! sameWeight) { // 如果权重不相同且权重大于0则按总权重数随机 int offset = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < length; i++) { offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { return invokers.get(i); } } } // 如果权重相同或权重为0则均等随机 return invokers.get(random.nextInt(length));}}说明:从源码可以看出随机负载均衡的策略分为两种情况a. 如果总权重大于0并且权重不相同,就生成一个1totalWeight(总权重数)的随机数,然后再把随机数和所有的权重值一一相减得到一个新的随机数,直到随机 数小于0,那么此时访问的服务器就是使得随机数小于0的权重所在的机器b. 如果权重相同或者总权重数为0,就生成一个1length(权重的总个数)的随机数,此时所访问的机器就是这个随机数对应的权重所在的机器2、RoundRobin LoadBalance2.1 轮循,按公约后的权重设置轮循比率。2.2 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。2.3 源码分析 package com.alibaba.dubbo.rpc.cluster.loadbalance; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.common.utils.AtomicPositiveInteger; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; /* * Round robin load balance. * * @author qian.lei * @author william.liangf / public class RoundRobinLoadBalance extends AbstractLoadBalance {public static final String NAME = “roundrobin”; private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();private final ConcurrentMap<String, AtomicPositiveInteger> weightSequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String key = invokers.get(0).getUrl().getServiceKey() + “.” + invocation.getMethodName(); int length = invokers.size(); // 总个数 int maxWeight = 0; // 最大权重 int minWeight = Integer.MAX_VALUE; // 最小权重 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); maxWeight = Math.max(maxWeight, weight); // 累计最大权重 minWeight = Math.min(minWeight, weight); // 累计最小权重 } if (maxWeight > 0 && minWeight < maxWeight) { // 权重不一样 AtomicPositiveInteger weightSequence = weightSequences.get(key); if (weightSequence == null) { weightSequences.putIfAbsent(key, new AtomicPositiveInteger()); weightSequence = weightSequences.get(key); } int currentWeight = weightSequence.getAndIncrement() % maxWeight; List<Invoker<T>> weightInvokers = new ArrayList<Invoker<T>>(); for (Invoker<T> invoker : invokers) { // 筛选权重大于当前权重基数的Invoker if (getWeight(invoker, invocation) > currentWeight) { weightInvokers.add(invoker); } } int weightLength = weightInvokers.size(); if (weightLength == 1) { return weightInvokers.get(0); } else if (weightLength > 1) { invokers = weightInvokers; length = invokers.size(); } } AtomicPositiveInteger sequence = sequences.get(key); if (sequence == null) { sequences.putIfAbsent(key, new AtomicPositiveInteger()); sequence = sequences.get(key); } // 取模轮循 return invokers.get(sequence.getAndIncrement() % length);}}说明:从源码可以看出轮循负载均衡的算法是:a. 如果权重不一样时,获取一个当前的权重基数,然后从权重集合中筛选权重大于当前权重基数的集合,如果筛选出的集合的长度为1,此时所访问的机 器就是集合里面的权重对应的机器b. 如果权重一样时就取模轮循3、LeastActive LoadBalance3.1 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差(调用前的时刻减去响应后的时刻的值)。3.2 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大3.3 对应的源码package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.List;import java.util.Random;import com.alibaba.dubbo.common.Constants;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;import com.alibaba.dubbo.rpc.RpcStatus;/** LeastActiveLoadBalance* * @author william.liangf*/public class LeastActiveLoadBalance extends AbstractLoadBalance {public static final String NAME = “leastactive”;private final Random random = new Random();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int leastActive = -1; // 最小的活跃数 int leastCount = 0; // 相同最小活跃数的个数 int[] leastIndexs = new int[length]; // 相同最小活跃数的下标 int totalWeight = 0; // 总权重 int firstWeight = 0; // 第一个权重,用于于计算是否相同 boolean sameWeight = true; // 是否所有权重相同 for (int i = 0; i < length; i++) { Invoker<T> invoker = invokers.get(i); int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活跃数 int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重 if (leastActive == -1 || active < leastActive) { // 发现更小的活跃数,重新开始 leastActive = active; // 记录最小活跃数 leastCount = 1; // 重新统计相同最小活跃数的个数 leastIndexs[0] = i; // 重新记录最小活跃数下标 totalWeight = weight; // 重新累计总权重 firstWeight = weight; // 记录第一个权重 sameWeight = true; // 还原权重相同标识 } else if (active == leastActive) { // 累计相同最小的活跃数 leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标 totalWeight += weight; // 累计总权重 // 判断所有权重是否一样 if (sameWeight && i > 0 && weight != firstWeight) { sameWeight = false; } } } // assert(leastCount > 0) if (leastCount == 1) { // 如果只有一个最小则直接返回 return invokers.get(leastIndexs[0]); } if (! sameWeight && totalWeight > 0) { // 如果权重不相同且权重大于0则按总权重数随机 int offsetWeight = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; offsetWeight -= getWeight(invokers.get(leastIndex), invocation); if (offsetWeight <= 0) return invokers.get(leastIndex); } } // 如果权重相同或权重为0则均等随机 return invokers.get(leastIndexs[random.nextInt(leastCount)]);}}说明:源码里面的注释已经很清晰了,大致的意思就是活跃数越小的的机器分配到的请求越多4、ConsistentHash LoadBalance4.1 一致性 Hash,相同参数的请求总是发到同一提供者。4.2 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。4.3 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key=“hash.arguments” value=“0,1” />4.4 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=“hash.nodes” value=“320” />4.5 源码分析暂时还没有弄懂,后面弄懂了再补充进来,有兴趣的小伙伴可以自己去看一下源码,然后一起交流一下心得5、dubbo官方的文档的负载均衡配置示例服务端服务级别 <dubbo:service interface="…" loadbalance=“roundrobin” /> 客户端服务级别 <dubbo:reference interface="…" loadbalance=“roundrobin” /> 服务端方法级别 <dubbo:service interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:service> 客户端方法级别 <dubbo:reference interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:reference> 大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. 原文连接:https://www.cnblogs.com/leeSm… ...

September 29, 2018 · 4 min · jiezi

前后端分离项目 — SpringSocial 绑定与解绑社交账号如微信、QQ

1、准备工作 申请QQ、微信相关ClientId和AppSecret,这些大家自己到QQ互联和微信开发平台 去申请吧 还有java后台要引入相关的jar包,如下:&lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.security.oauth.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-security-oauth2-autoconfigure&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.security.oauth&lt;/groupId&gt; &lt;artifactId&gt;spring-security-oauth2&lt;/artifactId&gt; &lt;version&gt;2.3.3.RELEASE&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;!--&lt;dependency&gt;--&gt; &lt;!--&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;--&gt; &lt;!--&lt;artifactId&gt;spring-cloud-starter-security&lt;/artifactId&gt;--&gt; &lt;!--&lt;/dependency&gt;--&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt; &lt;artifactId&gt;spring-cloud-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt; &lt;artifactId&gt;spring-cloud-starter-oauth2&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;mysql&lt;/groupId&gt; &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.social&lt;/groupId&gt; &lt;artifactId&gt;spring-social-config&lt;/artifactId&gt; &lt;version&gt;1.1.6.RELEASE&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.social&lt;/groupId&gt; &lt;artifactId&gt;spring-social-core&lt;/artifactId&gt; &lt;version&gt;1.1.6.RELEASE&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.social&lt;/groupId&gt; &lt;artifactId&gt;spring-social-security&lt;/artifactId&gt; &lt;version&gt;1.1.6.RELEASE&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.social&lt;/groupId&gt; &lt;artifactId&gt;spring-social-web&lt;/artifactId&gt; &lt;version&gt;1.1.6.RELEASE&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt; &lt;artifactId&gt;jjwt&lt;/artifactId&gt; &lt;version&gt;0.9.1&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.commons&lt;/groupId&gt; &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt; &lt;version&gt;3.7&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.commons&lt;/groupId&gt; &lt;artifactId&gt;commons-collections4&lt;/artifactId&gt; &lt;version&gt;4.2&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;commons-beanutils&lt;/groupId&gt; &lt;artifactId&gt;commons-beanutils&lt;/artifactId&gt; &lt;version&gt;1.9.3&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.data&lt;/groupId&gt; &lt;artifactId&gt;spring-data-mongodb&lt;/artifactId&gt; &lt;version&gt;2.0.9.RELEASE&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-mongodb&lt;/artifactId&gt; &lt;version&gt;2.0.4.RELEASE&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt; &lt;artifactId&gt;jackson-core&lt;/artifactId&gt; &lt;version&gt;2.9.6&lt;/version&gt; &lt;/dependency&gt;然后在application.properties里面设置相关配置,如redis、mysql等设置,如下:spring.datasource.url=spring.datasource.username=spring.datasource.password=spring.datasource.driverClassName=com.mysql.jdbc.Driverspring.redis.host=127.0.0.1spring.redis.password=your_pwdspring.redis.port=6379spring.redis.timeout=30000ssb.security.social.register-url=/social/signUpssb.security.social.filter-processes-url=/social-loginssb.security.social.bind-url=https://website/social-bind/qqssb.security.social.callback-url=https://website/social-loginssb.security.social.connect-url=https://website/social-connect//QQ授权ssb.security.social.qq.app-id=ssb.security.social.qq.app-secret=ssb.security.social.qq.provider-id=qq//WeChat授权ssb.security.social.wechat.app-id=ssb.security.social.wechat.app-secret=ssb.security.social.wechat.provider-id=wechat2、准备工作做好之后,现在我们开始分析社交绑定,其实spring-social框架里已经自带了spring-social-web,这个jar包里面有个ConnectController.java类,这个类已经帮我们实现了相关绑定与解绑实现方法,问题在于它是基于Session的,所以如果是前后端分离项目使用Session当然应有问题,所以我们要结合Redis来使用,把相关变量都存在Redis中,所以我们上面已经配置好了Redis,我们再来看看Redis配置代码:@Configurationpublic class RestTemplateConfig { @Bean public RestTemplate restTemplate(ClientHttpRequestFactory factory){ return new RestTemplate(factory); } @Bean public ClientHttpRequestFactory simpleClientHttpRequestFactory(){ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setReadTimeout(50000);//单位为ms factory.setConnectTimeout(50000);//单位为ms return factory; }}3、获取系统当前用户所有社交账号绑定情况 设置好之后,我们来分析一下spring-social-web这个jar包获取社交账号绑定情况,它的请求地址是/connect,代码如下:@Controller@RequestMapping({"/connect"})public class ConnectController implements InitializingBean { private static final Log logger = LogFactory.getLog(ConnectController.class); private final ConnectionFactoryLocator connectionFactoryLocator; private final ConnectionRepository connectionRepository; private final MultiValueMap<Class<?>, ConnectInterceptor<?>> connectInterceptors = new LinkedMultiValueMap(); private final MultiValueMap<Class<?>, DisconnectInterceptor<?>> disconnectInterceptors = new LinkedMultiValueMap(); private ConnectSupport connectSupport; private final UrlPathHelper urlPathHelper = new UrlPathHelper(); private String viewPath = “connect/”; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); private String applicationUrl = null; protected static final String DUPLICATE_CONNECTION_ATTRIBUTE = “social_addConnection_duplicate”; protected static final String PROVIDER_ERROR_ATTRIBUTE = “social_provider_error”; protected static final String AUTHORIZATION_ERROR_ATTRIBUTE = “social_authorization_error”; @Inject public ConnectController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) { this.connectionFactoryLocator = connectionFactoryLocator; this.connectionRepository = connectionRepository; } /** @deprecated */ @Deprecated public void setInterceptors(List<ConnectInterceptor<?>> interceptors) { this.setConnectInterceptors(interceptors); } public void setConnectInterceptors(List<ConnectInterceptor<?>> interceptors) { Iterator var2 = interceptors.iterator(); while(var2.hasNext()) { ConnectInterceptor<?> interceptor = (ConnectInterceptor)var2.next(); this.addInterceptor(interceptor); } } public void setDisconnectInterceptors(List<DisconnectInterceptor<?>> interceptors) { Iterator var2 = interceptors.iterator(); while(var2.hasNext()) { DisconnectInterceptor<?> interceptor = (DisconnectInterceptor)var2.next(); this.addDisconnectInterceptor(interceptor); } } public void setApplicationUrl(String applicationUrl) { this.applicationUrl = applicationUrl; } public void setViewPath(String viewPath) { this.viewPath = viewPath; } public void setSessionStrategy(SessionStrategy sessionStrategy) { this.sessionStrategy = sessionStrategy; } public void addInterceptor(ConnectInterceptor<?> interceptor) { Class<?> serviceApiType = GenericTypeResolver.resolveTypeArgument(interceptor.getClass(), ConnectInterceptor.class); this.connectInterceptors.add(serviceApiType, interceptor); } public void addDisconnectInterceptor(DisconnectInterceptor<?> interceptor) { Class<?> serviceApiType = GenericTypeResolver.resolveTypeArgument(interceptor.getClass(), DisconnectInterceptor.class); this.disconnectInterceptors.add(serviceApiType, interceptor); } @RequestMapping( method = {RequestMethod.GET} ) public String connectionStatus(NativeWebRequest request, Model model) { this.setNoCache(request); this.processFlash(request, model); Map<String, List<Connection<?>>> connections = this.connectionRepository.findAllConnections(); model.addAttribute(“providerIds”, this.connectionFactoryLocator.registeredProviderIds()); model.addAttribute(“connectionMap”, connections); return this.connectView(); } @RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.GET} ) public String connectionStatus(@PathVariable String providerId, NativeWebRequest request, Model model) { this.setNoCache(request); this.processFlash(request, model); List<Connection<?>> connections = this.connectionRepository.findConnections(providerId); this.setNoCache(request); if(connections.isEmpty()) { return this.connectView(providerId); } else { model.addAttribute(“connections”, connections); return this.connectedView(providerId); } } @RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.POST} ) public RedirectView connect(@PathVariable String providerId, NativeWebRequest request) { ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId); MultiValueMap<String, String> parameters = new LinkedMultiValueMap(); this.preConnect(connectionFactory, parameters, request); try { return new RedirectView(this.connectSupport.buildOAuthUrl(connectionFactory, request, parameters)); } catch (Exception var6) { this.sessionStrategy.setAttribute(request, “social_provider_error”, var6); return this.connectionStatusRedirect(providerId, request); } } @RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.GET}, params = {“oauth_token”} ) public RedirectView oauth1Callback(@PathVariable String providerId, NativeWebRequest request) { try { OAuth1ConnectionFactory<?> connectionFactory = (OAuth1ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId); Connection<?> connection = this.connectSupport.completeConnection(connectionFactory, request); this.addConnection(connection, connectionFactory, request); } catch (Exception var5) { this.sessionStrategy.setAttribute(request, “social_provider_error”, var5); logger.warn(“Exception while handling OAuth1 callback (” + var5.getMessage() + “). Redirecting to " + providerId + " connection status page.”); } return this.connectionStatusRedirect(providerId, request); } @RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.GET}, params = {“code”} ) public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) { try { OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId); Connection<?> connection = this.connectSupport.completeConnection(connectionFactory, request); this.addConnection(connection, connectionFactory, request); } catch (Exception var5) { this.sessionStrategy.setAttribute(request, “social_provider_error”, var5); logger.warn(“Exception while handling OAuth2 callback (” + var5.getMessage() + “). Redirecting to " + providerId + " connection status page.”); } return this.connectionStatusRedirect(providerId, request); } @RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.GET}, params = {“error”} ) public RedirectView oauth2ErrorCallback(@PathVariable String providerId, @RequestParam(“error”) String error, @RequestParam(value = “error_description”,required = false) String errorDescription, @RequestParam(value = “error_uri”,required = false) String errorUri, NativeWebRequest request) { Map<String, String> errorMap = new HashMap(); errorMap.put(“error”, error); if(errorDescription != null) { errorMap.put(“errorDescription”, errorDescription); } if(errorUri != null) { errorMap.put(“errorUri”, errorUri); } this.sessionStrategy.setAttribute(request, “social_authorization_error”, errorMap); return this.connectionStatusRedirect(providerId, request); } @RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.DELETE} ) public RedirectView removeConnections(@PathVariable String providerId, NativeWebRequest request) { ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId); this.preDisconnect(connectionFactory, request); this.connectionRepository.removeConnections(providerId); this.postDisconnect(connectionFactory, request); return this.connectionStatusRedirect(providerId, request); } @RequestMapping( value = {"/{providerId}/{providerUserId}"}, method = {RequestMethod.DELETE} ) public RedirectView removeConnection(@PathVariable String providerId, @PathVariable String providerUserId, NativeWebRequest request) { ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId); this.preDisconnect(connectionFactory, request); this.connectionRepository.removeConnection(new ConnectionKey(providerId, providerUserId)); this.postDisconnect(connectionFactory, request); return this.connectionStatusRedirect(providerId, request); } protected String connectView() { return this.getViewPath() + “status”; } protected String connectView(String providerId) { return this.getViewPath() + providerId + “Connect”; } protected String connectedView(String providerId) { return this.getViewPath() + providerId + “Connected”; } protected RedirectView connectionStatusRedirect(String providerId, NativeWebRequest request) { HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class); String path = “/connect/” + providerId + this.getPathExtension(servletRequest); if(this.prependServletPath(servletRequest)) { path = servletRequest.getServletPath() + path; } return new RedirectView(path, true); } public void afterPropertiesSet() throws Exception { this.connectSupport = new ConnectSupport(this.sessionStrategy); if(this.applicationUrl != null) { this.connectSupport.setApplicationUrl(this.applicationUrl); } } private boolean prependServletPath(HttpServletRequest request) { return !this.urlPathHelper.getPathWithinServletMapping(request).equals(""); } private String getPathExtension(HttpServletRequest request) { String fileName = this.extractFullFilenameFromUrlPath(request.getRequestURI()); String extension = StringUtils.getFilenameExtension(fileName); return extension != null?"." + extension:""; } private String extractFullFilenameFromUrlPath(String urlPath) { int end = urlPath.indexOf(63); if(end == -1) { end = urlPath.indexOf(35); if(end == -1) { end = urlPath.length(); } } int begin = urlPath.lastIndexOf(47, end) + 1; int paramIndex = urlPath.indexOf(59, begin); end = paramIndex != -1 && paramIndex < end?paramIndex:end; return urlPath.substring(begin, end); } private String getViewPath() { return this.viewPath; } private void addConnection(Connection<?> connection, ConnectionFactory<?> connectionFactory, WebRequest request) { try { this.connectionRepository.addConnection(connection); this.postConnect(connectionFactory, connection, request); } catch (DuplicateConnectionException var5) { this.sessionStrategy.setAttribute(request, “social_addConnection_duplicate”, var5); } } private void preConnect(ConnectionFactory<?> connectionFactory, MultiValueMap<String, String> parameters, WebRequest request) { Iterator var4 = this.interceptingConnectionsTo(connectionFactory).iterator(); while(var4.hasNext()) { ConnectInterceptor interceptor = (ConnectInterceptor)var4.next(); interceptor.preConnect(connectionFactory, parameters, request); } } private void postConnect(ConnectionFactory<?> connectionFactory, Connection<?> connection, WebRequest request) { Iterator var4 = this.interceptingConnectionsTo(connectionFactory).iterator(); while(var4.hasNext()) { ConnectInterceptor interceptor = (ConnectInterceptor)var4.next(); interceptor.postConnect(connection, request); } } private void preDisconnect(ConnectionFactory<?> connectionFactory, WebRequest request) { Iterator var3 = this.interceptingDisconnectionsTo(connectionFactory).iterator(); while(var3.hasNext()) { DisconnectInterceptor interceptor = (DisconnectInterceptor)var3.next(); interceptor.preDisconnect(connectionFactory, request); } } private void postDisconnect(ConnectionFactory<?> connectionFactory, WebRequest request) { Iterator var3 = this.interceptingDisconnectionsTo(connectionFactory).iterator(); while(var3.hasNext()) { DisconnectInterceptor interceptor = (DisconnectInterceptor)var3.next(); interceptor.postDisconnect(connectionFactory, request); } } private List<ConnectInterceptor<?>> interceptingConnectionsTo(ConnectionFactory<?> connectionFactory) { Class<?> serviceType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class); List<ConnectInterceptor<?>> typedInterceptors = (List)this.connectInterceptors.get(serviceType); if(typedInterceptors == null) { typedInterceptors = Collections.emptyList(); } return typedInterceptors; } private List<DisconnectInterceptor<?>> interceptingDisconnectionsTo(ConnectionFactory<?> connectionFactory) { Class<?> serviceType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class); List<DisconnectInterceptor<?>> typedInterceptors = (List)this.disconnectInterceptors.get(serviceType); if(typedInterceptors == null) { typedInterceptors = Collections.emptyList(); } return typedInterceptors; } private void processFlash(WebRequest request, Model model) { this.convertSessionAttributeToModelAttribute(“social_addConnection_duplicate”, request, model); this.convertSessionAttributeToModelAttribute(“social_provider_error”, request, model); model.addAttribute(“social_authorization_error”, this.sessionStrategy.getAttribute(request, “social_authorization_error”)); this.sessionStrategy.removeAttribute(request, “social_authorization_error”); } private void convertSessionAttributeToModelAttribute(String attributeName, WebRequest request, Model model) { if(this.sessionStrategy.getAttribute(request, attributeName) != null) { model.addAttribute(attributeName, Boolean.TRUE); this.sessionStrategy.removeAttribute(request, attributeName); } } private void setNoCache(NativeWebRequest request) { HttpServletResponse response = (HttpServletResponse)request.getNativeResponse(HttpServletResponse.class); if(response != null) { response.setHeader(“Pragma”, “no-cache”); response.setDateHeader(“Expires”, 1L); response.setHeader(“Cache-Control”, “no-cache”); response.addHeader(“Cache-Control”, “no-store”); } }}上面就是ConnectController的源码了,我们现在分析一下获取当前用户社交绑定情况的方法: @RequestMapping( method = {RequestMethod.GET})public String connectionStatus(NativeWebRequest request, Model model) { this.setNoCache(request); this.processFlash(request, model); Map&lt;String, List&lt;Connection&lt;?&gt;&gt;&gt; connections = this.connectionRepository.findAllConnections(); model.addAttribute("providerIds", this.connectionFactoryLocator.registeredProviderIds()); model.addAttribute("connectionMap", connections); return this.connectView();}@RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.GET})public String connectionStatus(@PathVariable String providerId, NativeWebRequest request, Model model) { this.setNoCache(request); this.processFlash(request, model); List&lt;Connection&lt;?&gt;&gt; connections = this.connectionRepository.findConnections(providerId); this.setNoCache(request); if(connections.isEmpty()) { return this.connectView(providerId); } else { model.addAttribute("connections", connections); return this.connectedView(providerId); }}对了,就是这两个方法,前面第一个方法请求的地址是:/connect(需要用户登录) 这个地址是获取当前用户所有社交账号绑定情况,第二个方法请求的地址是:/connect/{providerId}(需要用户登录) 这个地址是获取某个社交账号绑定情况,如/connect/qq,所以我们要获取当前用户绑定的所有社交账号绑定情况,使用的是第一个方法,但是现在有个问题,获取完之后 它是直接跳转页面到/connect/status,当然这不是我们想要的,我们要修改这个类,比如地址换成/socialConnect,这个换成自己的就好,然后我们来改下这个方法,如下:@RequestMapping( method = {RequestMethod.GET})public ResponseEntity&lt;?&gt; connectionStatus(NativeWebRequest request, Model model) throws JsonProcessingException { this.setNoCache(request); this.processFlash(request, model); Map&lt;String, List&lt;Connection&lt;?&gt;&gt;&gt; connections = this.connectionRepository.findAllConnections(); model.addAttribute("providerIds", this.connectionFactoryLocator.registeredProviderIds()); model.addAttribute("connectionMap", connections); Map&lt;String,Boolean&gt; result = new HashMap&lt;String, Boolean&gt;(); for (String key : connections.keySet()){ result.put(key, org.apache.commons.collections.CollectionUtils.isNotEmpty(connections.get(key))); } return ResponseEntity.ok(objectMapper.writeValueAsString(result));}改好的代码直接返回Json数据给前端,而不是跳转页面,完美解决了前后端分离项目问题,好了,我们使用postman发送请求测试看看:如图所示,我们成功获取当前登录用户所有社交账号绑定情况了(为什么这里只有qq和微信?社交账号的类型是你application.proterties里面配置的)。4、绑定社交账号好了,我们来看看绑定社交账号的方法:@RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.POST})public RedirectView connect(@PathVariable String providerId, NativeWebRequest request) { ConnectionFactory&lt;?&gt; connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId); MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap(); this.preConnect(connectionFactory, parameters, request); try { return new RedirectView(this.connectSupport.buildOAuthUrl(connectionFactory, request, parameters)); } catch (Exception var6) { this.sessionStrategy.setAttribute(request, "social_provider_error", var6); return this.connectionStatusRedirect(providerId, request); }}@RequestMapping( value = {"/{providerId}"}, method = {RequestMethod.GET}, params = {"code"})public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) { try { OAuth2ConnectionFactory&lt;?&gt; connectionFactory = (OAuth2ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId); Connection&lt;?&gt; connection = this.connectSupport.completeConnection(connectionFactory, request); this.addConnection(connection, connectionFactory, request); } catch (Exception var5) { this.sessionStrategy.setAttribute(request, "social_provider_error", var5); logger.warn("Exception while handling OAuth2 callback (" + var5.getMessage() + "). Redirecting to " + providerId + " connection status page."); } return this.connectionStatusRedirect(providerId, request);}现在来分析 下这两个 方法的作用,第一个方法请求的地址是:POST /connect/{providerId} ,第二个方法请求地址是:GET /connect/{providerId}?code=&state=。第一个方法是…未完待续5、解绑社交账号 ...

September 28, 2018 · 7 min · jiezi

Spring Boot中如何扩展XML请求和响应的支持

在之前的所有Spring Boot教程中,我们都只提到和用到了针对HTML和JSON格式的请求与响应处理。那么对于XML格式的请求要如何快速的在Controller中包装成对象,以及如何以XML的格式返回一个对象呢?实现原理:消息转换器(Message Converter)在扩展上述问题之前,我们先要知道Spring Boot中处理HTTP请求的实现是采用的Spring MVC。而在Spring MVC中有一个消息转换器这个概念,它主要负责处理各种不同格式的请求数据进行处理,并包转换成对象,以提供更好的编程体验。在Spring MVC中定义了HttpMessageConverter接口,抽象了消息转换器对类型的判断、对读写的判断与操作,具体可见如下定义:public interface HttpMessageConverter<T> { boolean canRead(Class<?> clazz, @Nullable MediaType mediaType); boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType); List<MediaType> getSupportedMediaTypes(); T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;}众所周知,HTTP请求的Content-Type有各种不同格式定义,如果要支持Xml格式的消息转换,就必须要使用对应的转换器。Spring MVC中默认已经有一套采用Jackson实现的转换器MappingJackson2XmlHttpMessageConverter。扩展实现第一步:引入Xml消息转换器在传统Spring应用中,我们可以通过如下配置加入对Xml格式数据的消息转换实现:@Configurationpublic class MessageConverterConfig1 extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml(); builder.indentOutput(true); converters.add(new MappingJackson2XmlHttpMessageConverter(builder.build())); }}在Spring Boot应用不用像上面这么麻烦,只需要加入jackson-dataformat-xml依赖,Spring Boot就会自动引入MappingJackson2XmlHttpMessageConverter的实现:<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId></dependency>同时,为了配置Xml数据与维护对象属性的关系所要使用的注解也在上述依赖中,所以这个依赖也是必须的。第二步:定义对象与Xml的关系做好了基础扩展之后,下面就可以定义Xml内容对应的Java对象了,比如:@Data@NoArgsConstructor@AllArgsConstructor@JacksonXmlRootElement(localName = “User”)public class User { @JacksonXmlProperty(localName = “name”) private String name; @JacksonXmlProperty(localName = “age”) private Integer age;}其中:@Data、@NoArgsConstructor、@AllArgsConstructor是lombok简化代码的注解,主要用于生成get、set以及构造函数。@JacksonXmlRootElement、@JacksonXmlProperty注解是用来维护对象属性在xml中的对应关系。上述配置的User对象,其可以映射的Xml样例如下(后续可以使用上述xml来请求接口):<User> <name>aaaa</name> <age>10</age></User>第三步:创建接收xml请求的接口完成了要转换的对象之后,可以编写一个接口来接收xml并返回xml,比如:@Controllerpublic class UserController { @PostMapping(value = “/user”, consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE) @ResponseBody public User create(@RequestBody User user) { user.setName(“didispace.com : " + user.getName()); user.setAge(user.getAge() + 100); return user; }}最后,启动Spring Boot应用,通过POSTMAN等请求工具,尝试一下这个接口,可以看到请求Xml,并且返回了经过处理后的Xml内容。案例代码可以通过下面两个仓库中查阅chapter3-1-8目录:- Github- Gitee ...

September 27, 2018 · 1 min · jiezi

IOC 之深入理解 Spring IoC

IOC 理论IoC 全称为 Inversion of Control,翻译为 “控制反转”,它还有一个别名为 DI(Dependency Injection),即依赖注入。如何理解“控制反转”好呢?理解好它的关键在于我们需要回答如下四个问题:谁控制谁控制什么为何是反转哪些方面反转了在回答这四个问题之前,我们先看 IOC 的定义:所谓 IOC ,就是由 Spring IOC 容器来负责对象的生命周期和对象之间的关系上面这句话是整个 IoC 理论的核心。如何来理解这句话?我们引用一个例子来走阐述(看完该例子上面四个问题也就不是问题了)。已找女朋友为例(对于程序猿来说这个值得探究的问题)。一般情况下我们是如何来找女朋友的呢?首先我们需要根据自己的需求(漂亮、身材好、性格好)找一个妹子,然后到处打听她的兴趣爱好、微信、电话号码,然后各种投其所好送其所要,最后追到手。如下:/*** 年轻小伙子*/public class YoungMan {private BeautifulGirl beautifulGirl;YoungMan(){// 可能你比较牛逼,指腹为婚// beautifulGirl = new BeautifulGirl();}public void setBeautifulGirl(BeautifulGirl beautifulGirl) {this.beautifulGirl = beautifulGirl;}public static void main(String[] args){YoungMan you = new YoungMan();BeautifulGirl beautifulGirl = new BeautifulGirl(“你的各种条件”);beautifulGirl.setxxx(“各种投其所好”);// 然后你有女票了you.setBeautifulGirl(beautifulGirl);}}这就是我们通常做事的方式,如果我们需要某个对象,一般都是采用这种直接创建的方式(new BeautifulGirl()),这个过程复杂而又繁琐,而且我们必须要面对每个环节,同时使用完成之后我们还要负责销毁它,在这种情况下我们的对象与它所依赖的对象耦合在一起。其实我们需要思考一个问题?我们每次用到自己依赖的对象真的需要自己去创建吗?我们知道,我们依赖对象其实并不是依赖该对象本身,而是依赖它所提供的服务,只要在我们需要它的时候,它能够及时提供服务即可,至于它是我们主动去创建的还是别人送给我们的,其实并不是那么重要。再说了,相比于自己千辛万苦去创建它还要管理、善后而言,直接有人送过来是不是显得更加好呢?这个给我们送东西的“人” 就是 IoC,在上面的例子中,它就相当于一个婚介公司,作为一个婚介公司它管理着很多男男女女的资料,当我们需要一个女朋友的时候,直接跟婚介公司提出我们的需求,婚介公司则会根据我们的需求提供一个妹子给我们,我们只需要负责谈恋爱,生猴子就行了。你看,这样是不是很简单明了。诚然,作为婚介公司的 IoC 帮我们省略了找女朋友的繁杂过程,将原来的主动寻找变成了现在的被动接受(符合我们的要求),更加简洁轻便。你想啊,原来你还得鞍马前后,各种巴结,什么东西都需要自己去亲力亲为,现在好了,直接有人把现成的送过来,多么美妙的事情啊。所以,简单点说,IoC 的理念就是让别人为你服务,如下图(摘自Spring揭秘):在没有引入 IoC 的时候,被注入的对象直接依赖于被依赖的对象,有了 IoC 后,两者及其他们的关系都是通过 Ioc Service Provider 来统一管理维护的。被注入的对象需要什么,直接跟 IoC Service Provider 打声招呼,后者就会把相应的被依赖对象注入到被注入的对象中,从而达到 IOC Service Provider 为被注入对象服务的目的。所以 IoC 就是这么简单!原来是需要什么东西自己去拿,现在是需要什么东西让别人(IOC Service Provider)送过来现在在看上面那四个问题,答案就显得非常明显了:谁控制谁:在传统的开发模式下,我们都是采用直接 new 一个对象的方式来创建对象,也就是说你依赖的对象直接由你自己控制,但是有了 IOC 容器后,则直接由 IoC 容器来控制。所以“谁控制谁”,当然是 IoC 容器控制对象。控制什么:控制对象。为何是反转:没有 IoC 的时候我们都是在自己对象中主动去创建被依赖的对象,这是正转。但是有了 IoC 后,所依赖的对象直接由 IoC 容器创建后注入到被注入的对象中,依赖的对象由原来的主动获取变成被动接受,所以是反转。哪些方面反转了:所依赖对象的获取被反转了。妹子有了,但是如何拥有妹子呢?这也是一门学问。可能你比较牛逼,刚刚出生的时候就指腹为婚了。大多数情况我们还是会考虑自己想要什么样的妹子,所以还是需要向婚介公司打招呼的。还有一种情况就是,你根本就不知道自己想要什么样的妹子,直接跟婚介公司说,我就要一个这样的妹子。所以,IOC Service Provider 为被注入对象提供被依赖对象也有如下几种方式:构造方法注入、stter方法注入、接口注入。构造器注入构造器注入,顾名思义就是被注入的对象通过在其构造方法中声明依赖对象的参数列表,让外部知道它需要哪些依赖对象。YoungMan(BeautifulGirl beautifulGirl){this.beautifulGirl = beautifulGirl;}构造器注入方式比较直观,对象构造完毕后就可以直接使用,这就好比你出生你家里就给你指定了你媳妇。setter 方法注入对于 JavaBean 对象而言,我们一般都是通过 getter 和 setter 方法来访问和设置对象的属性。所以,当前对象只需要为其所依赖的对象提供相对应的 setter 方法,就可以通过该方法将相应的依赖对象设置到被注入对象中。如下:public class YoungMan {private BeautifulGirl beautifulGirl;public void setBeautifulGirl(BeautifulGirl beautifulGirl) {this.beautifulGirl = beautifulGirl;}}相比于构造器注入,setter 方式注入会显得比较宽松灵活些,它可以在任何时候进行注入(当然是在使用依赖对象之前),这就好比你可以先把自己想要的妹子想好了,然后再跟婚介公司打招呼,你可以要林志玲款式的,赵丽颖款式的,甚至凤姐哪款的,随意性较强。接口方式注入接口方式注入显得比较霸道,因为它需要被依赖的对象实现不必要的接口,带有侵入性。一般都不推荐这种方式。各个组件该图为 ClassPathXmlApplicationContext 的类继承体系结构,虽然只有一部分,但是它基本上包含了 IOC 体系中大部分的核心类和接口。下面我们就针对这个图进行简单的拆分和补充说明。Resource体系Resource,对资源的抽象,它的每一个实现类都代表了一种资源的访问策略,如ClasspathResource 、 URLResource ,FileSystemResource 等。有了资源,就应该有资源加载,Spring 利用 ResourceLoader 来进行统一资源加载,类图如下:BeanFactory 体系BeanFactory 是一个非常纯粹的 bean 容器,它是 IOC 必备的数据结构,其中 BeanDefinition 是她的基本结构,它内部维护着一个 BeanDefinition map ,并可根据 BeanDefinition 的描述进行 bean 的创建和管理。BeanFacoty 有三个直接子类 ListableBeanFactory、HierarchicalBeanFactory 和 AutowireCapableBeanFactory,DefaultListableBeanFactory 为最终默认实现,它实现了所有接口。Beandefinition 体系BeanDefinition 用来描述 Spring 中的 Bean 对象。BeandefinitionReader体系BeanDefinitionReader 的作用是读取 Spring 的配置文件的内容,并将其转换成 Ioc 容器内部的数据结构:BeanDefinition。ApplicationContext体系这个就是大名鼎鼎的 Spring 容器,它叫做应用上下文,与我们应用息息相关,她继承 BeanFactory,所以它是 BeanFactory 的扩展升级版,如果BeanFactory 是屌丝的话,那么 ApplicationContext 则是名副其实的高富帅。由于 ApplicationContext 的结构就决定了它与 BeanFactory 的不同,其主要区别有:继承 MessageSource,提供国际化的标准访问策略。继承 ApplicationEventPublisher ,提供强大的事件机制。扩展 ResourceLoader,可以用来加载多个 Resource,可以灵活访问不同的资源。对 Web 应用的支持。上面五个体系可以说是 Spring IoC 中最核心的部分,后面博文也是针对这五个部分进行源码分析。其实 IoC 咋一看还是挺简单的,无非就是将配置文件(暂且认为是 xml 文件)进行解析(分析 xml 谁不会啊),然后放到一个 Map 里面就差不多了,初看有道理,其实要面临的问题还是有很多的,下面就劳烦各位看客跟着 LZ 博客来一步一步揭开 Spring IoC 的神秘面纱。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..原文:http://www.uml.org.cn/j2ee/20… ...

September 27, 2018 · 1 min · jiezi

微服务写的最全的一篇文章

今年有人提出了2018年微服务将疯狂至死,可见微服务的争论从未停止过。在这我将自己对微服务的理解整理了一下,希望对大家有所帮助。1.什么是微服务1)一组小的服务(大小没有特别的标准,只要同一团队的工程师理解服务的标识一致即可)2)独立的进程(java的tomcat,nodejs等)3)轻量级的通信(不是soap,是http协议)4)基于业务能力(类似用户服务,商品服务等等)5)独立部署(迭代速度快)6)无集中式管理(无须统一技术栈,可以根据不同的服务或者团队进行灵活选择)ps:微服务的先行者Netflix公司,开源了一些好的微服务框架,后续会有介绍。2.怎么权衡微服务的利于弊利:强模块边界 。(模块化的演化过程:类–>组件/类库(sdk)–>服务(service),方式越来越灵活)可独立部署。技术多样性。弊:分布式复杂性。最终一致性。(各个服务的团队,数据也是分散式治理,会出现不一致的问题)运维复杂性。测试复杂性。3.企业在什么时候考虑引入微服务从生产力和系统的复杂性这两个方面来看。公司一开始的时候,业务复杂性不高,这时候是验证商业模式的时候,业务简单,用单体服务反而生产力很高。随着公司的发展,业务复杂性慢慢提高,这时候就可以采用微服务来提升生产力了。至于这个转化的点,需要团队的架构师来进行各方面衡量,就个人经验而言,团队发展到百人以上,采用微服务就很有必要了。有些架构师是具有微服务架构能力,所以设计系统时就直接设计成了微服务,而不是通过单服务慢慢演化发展成微服务。在这里我并不推荐这种做法,因为一开始对业务领域并不是很了解,并且业务模式还没有得到验证,这时候上微服务风险比较高,很有可能失败。所以建议大家在单服务的应用成熟时,并且对业务领域比较熟悉的时候,如果发现单服务无法适应业务发展时,再考虑微服务的设计和架构。4.微服务的组织架构如上图左边,传统的企业中,团队是按职能划分的。开发一个项目时,会从不同的职能团队找人进行开发,开发完成后,再各自回到自己的职能团队,这种模式实践证明,效率还是比较低的。如上图右边,围绕每个业务线或产品,按服务划分团队。团队成员从架构到运维,形成一个完整的闭环。一直围绕在产品周围,进行不断的迭代。不会像传统的团队一样离开。这样开发效率会比较高。至于这种团队的规模,建议按照亚马逊的两个披萨原则,大概10人左右比较好。5:怎么理解中台战略和微服务中台战略的由来:马云2015年去欧洲的一家公司supersell参观,发现这个公司的创新能力非常强,团队的规模很小,但是开发效率很高。他们就是采用中台战略。马云感触很深,回国后就在集团内部推出了中台战略。简单的理解就是把传统的前后台体系中的后台进行了细分。阿里巴巴提出了大中台小前台的战略。就是强化业务和技术中台,把前端的应用变得更小更灵活。当中台越强大,能力就越强,越能更好的快速响应前台的业务需求。打个比喻,就是土壤越肥沃,越适合生长不同的生物,打造好的生态系统。6:服务分层每个公司的服务分层都不相同,有的公司服务没有分层,有的怎分层很多。目前业界没有统一的标准。下面推荐一个比较容易理解的两层结构。1:基础服务: 比如一个电商网站,商品服务和订单服务就属于基础服务(核心领域服务)。缓存服务,监控服务,消息队列等也属于基础服务(公共服务)2:聚合服务 :例如网关服务就算一种聚合服务(适配服务)。这是一种逻辑划分,不是物理划分,实际设计的东西很多很复杂。7:微服务的技术架构体系下图是一个成型的互联网微服务的架构体系:1:接入层 负载均衡作用,运维团队负责2:网关层 反向路由,安全验证,限流等3:业务服务层 基础服务和领域服务4:支撑服务层5:平台服务6:基础设施层 运维团队负责。(或者阿里云)8:微服务的服务发现的三种方式第一种:如下图所示,传统的服务发现(大部分公司的做法)。服务上线后,通知运维,申请域名,配置路由。调用方通过dns域名解析,经过负载均衡路由,进行服务访问。缺点: LB的单点风险,服务穿透LB,性能也不是太好第二种:也叫客户端发现方式。如下图所示。通过服务注册的方式,服务提供者先注册服务。消费者通过注册中心获取相应服务。并且把LB的功能移动到了消费者的进程内,消费者根据自身路由去获取相应服务。优点是,没有了LB单点问题,也没有了LB的中间一跳,性能也比较好。但是这种方式有一个非常明显的缺点就是具有非常强的耦合性。针对不同的语言,每个服务的客户端都得实现一套服务发现的功能。第三种:也叫服务端发现方式,如下图所示。和第二种很相似。但是LB功能独立进程单独部署,所以解决了客户端多语言开发的问题。唯一的缺点就是运维成比较高,每个节点都得部署一个LB的代理,例如nginx。9.微服务网关网关就好比一个公司的门卫。屏蔽内部细节,统一对外服务接口。下图是一个网关所处位置的示例图。10.Netflix Zuul网关介绍核心就是一个servlet,通过filter机制实现的。主要分为三类过滤器:前置过滤器,过滤器和后置过滤器。主要特色是,这些过滤器可以动态插拔,就是如果需要增加减少过滤器,可以不用重启,直接生效。原理就是:通过一个db维护过滤器(上图蓝色部分),如果增加过滤器,就将新过滤器编译完成后push到db中,有线程会定期扫描db,发现新的过滤器后,会上传到网关的相应文件目录下,并通知过滤器loader进行加载相应的过滤器。整个网关调用的流程上图从左变http Request开始经过三类过滤器,最终到最右边的Http Response,这就是Zull网关的整个调用流程。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多11:微服务的路由发现体系整个微服务的路由发现体系,一般由服务注册中心和网关两部分组成。以NetFlix为例子,Eureka和Zull这两个组件支撑了netFlix整个的路由发现体系。如下图所示,首先外部请求发送到网关,网关去服务注册中心获取相应的服务,进行调用。其次内部服务间的调用,也通过服务注册中心进行的12.微服务配置中心目前大部分公司都是把配置写到配置文件中,遇到修改配置的情况,成本很高。并且没有修改配置的记录,出问题很难追溯。配置中心就接解决了以上的问题。可配置内容:数据库连接,业务参数等等配置中心就是一个web服务,配置人员通过后台页面修改配置,各个服务就会得到新的配置参数。实现方式主要有两种,一种是push,另一种是pull。两张方式各有优缺点。push实时性较好,但是遇到网络抖动,会丢失消息。pull不会丢失消息但是实时性差一些。大家可以同时两种方式使用,实现一个比较好的效果。如下图所示,这是一个国内知名互联网公司的配置中心架构图。开源地址:http://github.com/ctripcorp/a…13:RPC遇到了REST内部一些核心服务,性能要求比较高的可以采用RPC,对外服务的一般可以采用rest。14:服务框架和治理微服务很多的时候,就需要有治理了。一个好的微服务框架一般分为以下14个部分。如下图所示。这就是开篇所说的,微服务涉及的东西很多,有些初创公司和业务不成熟的产品是不太适合的,成本比较高。目前国内比较好的微服务框架就是阿里巴巴的DUBBO了,国外的就是spring cloud,大家可以去研究一下.15:监控体系监控是微服务治理的重要环节。一般分为以下四层。如下图所示。监控的内容分为五个部分:日志监控,Metrics监控(服务调用情况),调用链监控,告警系统和健康检查。日志监控,国内常用的就是ELK+KAFKA来实现。健康检查和Metrics,像spring boot会自带。Nagios也是一个很好的开源监控框架。16:Trace调用链监控调用链监控是用来追踪微服务之前依赖的路径和问题定位。例如阿里的鹰眼系统。主要原理就是子节点会记录父节点的id信息。下图是目前比较流行的调用链监控框架。17:微服务的限流熔断假设服务A依赖服务B和服务C,而B服务和C服务有可能继续依赖其他的服务,继续下去会使得调用链路过长。如果在A的链路上某个或几个被调用的子服务不可用或延迟较高,则会导致调用A服务的请求被堵住,堵住的请求会消耗占用掉系统的线程、io等资源,当该类请求越来越多,占用的计算机资源越来越多的时候,会导致系统瓶颈出现,造成其他的请求同样不可用,最终导致业务系统崩溃。一般情况对于服务依赖的保护主要有两种方式:熔断和限流。目前最流行的就是Hystrix的熔断框架。下图是Hystrix的断路器原理图:限流方式可以采用zuul的API限流方法。18.Docker 容器部署技术&持续交付流水线随着微服务的流行,容器技术也相应的被大家重视起来。容器技术主要解决了以下两个问题:1:环境一致性问题。例如java的jar/war包部署会依赖于环境的问题(操着系统的版本,jdk版本问题)。2:镜像部署问题。例如java,rubby,nodejs等等的发布系统是不一样的,每个环境都得很麻烦的部署一遍,采用docker镜像,就屏蔽了这类问题。下图是Docker容器部署的一个完整过程。更重要的是,拥有如此多服务的集群环境迁移、复制也非常轻松,只需选择好各服务对应的Docker服务镜像、配置好相互之间访问地址就能很快搭建出一份完全一样的新集群。19.容器调度和发布体系目前基于容器的调度平台有Kubernetes,mesos,omega。下图是mesos的一个简单架构示意图。下图是一个完整的容器发布体系 大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..

September 27, 2018 · 1 min · jiezi

分布式系统消息中间件——RibbitMQ的使用基础篇

前言我是在解决分布式事务的一致性问题时了解到RabbitMQ的,当时主要是要基于RabbitMQ来实现我们分布式系统之间对有事务可靠性要求的系统间通信的。关于分布式事务一致性问题及其常见的解决方案,可以看我另一篇博客。提到RabbitMQ,不难想到的几个关键字:消息中间件、消息队列。而消息队列不由让我想到,当时在大学学习操作系统这门课,消息队列不难想到生产者消费者模式。(PS:操作系统这门课程真的很好也很重要,其中的一些思想在我工作的很长一段一时间内给了我很大帮助和启发,给我提供了许多解决问题的思路。强烈建议每一个程序员都去学一学操作系统!)一 消息中间件1.1 简介消息中间件也可以称消息队列,是指用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息队列模型,可以在分布式环境下扩展进程的通信。当下主流的消息中间件有RabbitMQ、Kafka、ActiveMQ、RocketMQ等。其能在不同平台之间进行通信,常用来屏蔽各种平台协议之间的特性,实现应用程序之间的协同。其优点在于能够在客户端和服务器之间进行同步和异步的连接,并且在任何时刻都可以将消息进行传送和转发。是分布式系统中非常重要的组件,主要用来解决应用耦合、异步通信、流量削峰等问题。1.2 作用消息中间件几大主要作用如下:解耦冗余(存储)扩展性削峰可恢复性顺序保证缓冲异步通信1.3 消息中间件的两种模式1.3.1 P2P模式P2P模式包含三个角色:消息队列(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。P2P的特点:每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行它不会影响到消息被发送到队列接收者在成功接收消息之后需向队列应答成功如果希望发送的每个消息都会被成功处理的话,那么需要P2P模式1.3.2 Pub/Sub模式Pub/Sub模式包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 。多个发布者将消息发送到Topic,系统将这些消息传递给多个订阅者。Pub/Sub的特点每个消息可以有多个消费者发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。为了消费消息,订阅者必须保持运行的状态。如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型。1.4 常用中间件介绍与对比Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache定级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。RabbitMQ比Kafka可靠,kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。二 RabbitMQ了解2.1 简介RabbitMQ是流行的开源消息队列系统。RabbitMQ是AMQP(高级消息队列协议)的标准实现。支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,持久化。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了一个Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)或者数据持久化都有很好的支持。其主要特点如下:可靠性灵活的路由扩展性高可用性多种协议多语言客户端管理界面插件机制2.2 概念RabbitMQ从整体上来看是一个典型的生产者消费者模型,主要负责接收、存储和转发消息。其整体模型架构如下图所示:我们先来看一个RabbitMQ的运转流程,稍后会对这个流程中所涉及到的一些概念进行详细的解释。生产者:(1)生产者连接到RabbitMQ Broker,建立一个连接( Connection)开启一个信道(Channel)(2)生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等(3)生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等(4)生产者通过路由键将交换器和队列绑定起来(5)生产者发送消息至RabbitMQ Broker,其中包含路由键、交换器等信息。(6)相应的交换器根据接收到的路由键查找相匹配的队列。(7)如果找到,则将从生产者发送过来的消息存入相应的队列中。(8)如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者(9)关闭信道。(10)关闭连接。消费者:(1)消费者连接到RabbitMQ Broker ,建立一个连接(Connection),开启一个信道(Channel) 。(2)消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,(3)等待RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息。(4)消费者确认(ack) 接收到的消息。(5)RabbitMQ 从队列中删除相应己经被确认的消息。(6)关闭信道。(7)关闭连接。2.2.1 信道这里我们主要讨论两个问题:为何要有信道?主要原因还是在于TCP连接的"昂贵"性。无论是生产者还是消费者,都需要和RabbitMQ Broker 建立连接,这个连接就是一条TCP 连接。而操作系统对于TCP连接的创建于销毁是非常昂贵的开销。假设消费者要消费消息,并根据服务需求合理调度线程,若只进行TCP连接,那么当高并发的时候,每秒可能都有成千上万的TCP连接,不仅仅是对TCP连接的浪费,也很快会超过操作系统每秒所能建立连接的数量。如果能在一条TCP连接上操作,又能保证各个线程之间的私密性就完美了,于是信道的概念出现了。信道为何?信道是建立在Connection 之上的虚拟连接。当应用程序与Rabbit Broker建立TCP连接的时候,客户端紧接着可以创建一个AMQP 信道(Channel) ,每个信道都会被指派一个唯一的D。RabbitMQ 处理的每条AMQP 指令都是通过信道完成的。信道就像电缆里的光纤束。一条电缆内含有许多光纤束,允许所有的连接通过多条光线束进行传输和接收。2.2.2 生产者消费者关于生产者消费者我们需要了解几个概念:Producer:生产者,即消息投递者一方。消息:消息一般分两个部分:消息体(payload)和标签。标签用来描述这条消息,如:一个交换器的名称或者一个路由Key,Rabbit通过解析标签来确定消息的去向,payload是消息内容可以使一个json,数组等等。Consumer:消费者,就是接收消息的一方。消费者订阅RabbitMQ的队列,当消费者消费一条消息时,只是消费消息的消息体。在消息路由的过程中,会丢弃标签,存入到队列中的只有消息体。Broker:消息中间件的服务节点。2.2.3 队列、交换器、路由key、绑定从RabbitMQ的运转流程我们可以知道生产者的消息是发布到交换器上的。而消费者则是从队列上获取消息的。那么消息到底是如何从交换器到队列的呢?我们先具体了解一下这几个概念。Queue:队列,是RabbitMQ的内部对象,用于存储消息。RabbitMQ中消息只能存储在队列中。生产者投递消息到队列,消费者从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(轮询)给多个消费者进行消费,而不是每个消费者都收到所有的消息进行消费。(注意:RabbitMQ不支持队列层面的广播消费,如果需要广播消费,可以采用一个交换器通过路由Key绑定多个队列,由多个消费者来订阅这些队列的方式。)Exchange:交换器。在RabbitMQ中,生产者并非直接将消息投递到队列中。真实情况是,生产者将消息发送到Exchange(交换器),由交换器将消息路由到一个或多个队列中。如果路由不到,或返回给生产者,或直接丢弃,或做其它处理。RoutingKey:路由Key。生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则。这个路由Key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。在交换器类型和绑定键固定的情况下,生产者可以在发送消息给交换器时通过指定RoutingKey来决定消息流向哪里。Binding:RabbitMQ通过绑定将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键,这样RabbitMQ就可以指定如何正确的路由到队列了。从这里我们可以看到在RabbitMQ中交换器和队列实际上可以是一对多,也可以是多对多关系。交换器和队列就像我们关系数据库中的两张表。他们同归BindingKey做关联(多对多关系表)。在我们投递消息时,可以通过Exchange和RoutingKey(对应BindingKey)就可以找到相对应的队列。RabbitMQ主要有四种类型的交换器:fanout:扇形交换器,它会把发送到该交换器的消息路由到所有与该交换器绑定的队列中。如果使用扇形交换器,则不会匹配路由Key。direct:direct交换器,会把消息路由到RoutingKey与BindingKey完全匹配的队列中。topic:完全匹配BindingKey和RoutingKey的direct交换器 有些时候并不能满足实际业务的需求。topic 类型的交换器在匹配规则上进行了扩展,它与direct 类型的交换器相似,也是将消息路由到BindingKey 和RoutingKey相匹配的队列中,但这里的匹配规则有些不同,它约定:1:RoutingKey 为一个点号".“分隔的字符串(被点号”.“分隔开的每一段独立的字符串称为一个单词),如"hs.rabbitmq.client”,“com.rabbit.client"等。2:BindingKey 和RoutingKey 一样也是点号”.“分隔的字符串;3:BindingKey 中可以存在两种特殊字符串"“和”#",用于做模糊匹配,其中"“用于匹配一个单词,”#“用于匹配多规格单词(可以是零个)。如图: · 路由键为” apple.rabbit.client” 的消息会同时路由到Queuel 和Queue2; · 路由键为" orange.mq.client" 的消息只会路由到Queue2 中: · 路由键为" apple.mq.demo" 的消息只会路由到Queue2 中: · 路由键为" banana.rabbit.demo" 的消息只会路由到Queuel 中: · 路由键为" orange.apple.banana" 的消息将会被丢弃或者返回给生产者因为它没有匹配任何路由键。header:headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中 的headers属性进行匹配。在绑定队列和交换器时制定一组键值对, 当发送消息到交换器时, RabbitMQ 会获取到该消息的headers(也是一个键值对的形式) ,对比其中的键值对是否完全 匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。(注:该交换器类型性能较差且不实用,因此一般不会用到)。了解了上面的概念,我们再来思考消息是如何从交换器到队列的。首先Rabbit在接收到消息时,会解析消息的标签从而得到消息的交换器与路由key信息。然后根据交换器的类型、路由key以及该交换器和队列的绑定关系来决定消息最终投递到哪个队列里面。三 RabbitMQ使用3.1 RabbitMQ安装这里我们基于docker来安装。3.1.1 拉取镜像docker pull rabbitmq:management3.1.2 启动容器docker run -d –name rabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq:management3.2 RabbitMQ 客户端开发使用这里我们以dotnet平台下RabbitMQ.Client3.6.9(可以从nuget中下载)为示例,简单介绍dotnet平台下对RabbitMQ的简单操作。更详细的内容可以从nuget中下载源码和文档进行查看。3.2.1 连接Rabbit ConnectionFactory factory = new ConnectionFactory(); factory.UserName = “admin”;//用户名 factory.Password = “admin”;//密码 factory.HostName = “192.168.17.205”;//主机名 factory.VirtualHost = “”;//虚拟主机(这个暂时不需要,稍后的文章里会介绍虚拟主机 的概念) factory.Port = 15672;//端口 IConnection conn = factory.CreateConnection();//创建连接 3.2.2 创建信道IModel channel = conn.CreateModel();说明:Connection 可以用来创建多个Channel 实例,但是Channel 实例不能在线程问共享,应用程序应该为每一个线程开辟一个Channel 。某些情况下Channel 的操作可以并发运行,但是在其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响友送方确认( publisherconfrrm)机制的运行,所以多线程问共享Channel实例是非线程安全的。3.2.3 交换器、队列和绑定 channel.ExchangeDeclare(“exchangeName”, “direct”, true); String queueName = channel.QueueDeclare().QueueName; channel.QueueBind(queueName, “exchangeName”, “routingKey”);如上创建了一个持久化的、非自动删除的、绑定类型为direct 的交换器,同时也创建了一个非持久化的、排他的、自动删除的队列(此队列的名称由RabbitMQ 自动生成)。这里的交换器和队列也都没有设置特殊的参数。上面的代码也展示了如何使用路由键将队列和交换器绑定起来。上面声明的队列具备如下特性: 只对当前应用中同一个Connection 层面可用,同一个Connection 的不同Channel可共用,并且也会在应用连接断开时自动删除。上述方法根据参数不同,可以有不同的重载形式,根据自身的需要进行调用。ExchangeDeclare方法详解:ExchangeDeclare有多个重载方法,这些重载方法都是由下面这个方法中缺省的某些参数构成的。void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete, IDictionary<string, object> arguments);exchange : 交换器的名称。type : 交换器的类型,常见的如fanout、direct 、topicdurable: 设置是否持久化。durab l e 设置为true 表示持久化, 反之是非持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。autoDelete : 设置是否自动删除。autoDelete 设置为true 则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为:“当与此交换器连接的客户端都断开时,RabbitMQ 会自动删除本交换器”。internal : 设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。argument : 其他一些结构化参数,比如alternate - exchange。QueueDeclare方法详解:QueueDeclare只有两个重载。QueueDeclareOk QueueDeclare();QueueDeclareOk QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary<string, object> arguments);不带任何参数的queueDeclare 方法默认创建一个由RabbitMQ 命名的(类似这种amq.gen-LhQzlgv3GhDOv8PIDabOXA 名称,这种队列也称之为匿名队列〉、排他的、自动删除的、非持久化的队列。queue : 队列的名称。durable: 设置是否持久化。为true 则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。exclusive : 设置是否排他。为true 则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接(Connection) 可见的,同一个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;“首次"是指如果一个连接己经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同:即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。autoDelete: 设置是否自动删除。为true则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。不能把这个参数错误地理解为:当连接到此队列的所有客户端断开时,这个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。argurnents:设置队列的其他一些参数,如x-rnessage-ttl、x-expires、x-rnax-length、x-rnax-length-bytes、x-dead-letter-exchange、x-deadletter-routing-key,x-rnax-priority等。注意:生产者和消费者都能够使用queueDeclare来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道直为"传输"模式,之后才能声明队列。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多QueueBind 方法详解:将队列和交换器绑定的方法如下:void QueueBind(string queue, string exchange, string routingKey, IDictionary<string, object> arguments);queue: 队列名称:exchange: 交换器的名称:routingKey: 用来绑定队列和交换器的路由键;argument: 定义绑定的一些参数。将队列与交换器解绑的方法如下: QueueUnbind(string queue, string exchange, string routingKey, IDictionary<string, object> arguments);其参数与绑定意义相同。注:除队列可以绑定交换器外,交换器同样可以绑定队列。即:ExchangeBind方法,其使用方式与队列绑定相似。3.2.4 发送消息发送消息可以使用BasicPublish方法。void BasicPublish(string exchange, string routingKey, bool mandatory,IBasicProperties basicProperties, byte[] body);exchange: 交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到RabbitMQ 默认的交换器中。routingKey : 路由键,交换器根据路由键将消息存储到相应的队列之中。basicProperties: 消息的基本属性集。body : 消息体( pay1oad ),真正需要发送的消息。mandatory: 是否将消息返回给生产者(会在后续的文章中介绍这个参数).3.2.5 消费消息RabbitMQ 的消费模式分两种: 推(Push)模式和拉(Pull)模式。推模式采用BasicConsume进行消费,而拉模式则是调用BasicGet进行消费。推模式: EventingBasicConsumer consumer = new EventingBasicConsumer(channel);//定义消费者 对象consumer.Received += (model, ea) => { //do someting; channel.BasicAck(ea.DeliveryTag, multiple: false);//确认 }; channel.BasicConsume(queue: “queueName”, noAck: false, consumer: consumer);//订阅消息 string BasicConsume(string queue, bool noAck, string consumerTag, bool noLocal, bool exclusive, IDictionary<string, object> arguments, IBasicConsumer consumer);queue : 队列的名称:noAck : 设置是否需要确认,false为需要确认。consumerTag: 消费者标签,用来区分多个消费者:noLocal : 设置为true 则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者:exclusive : 设置是否排他arguments : 设置消费者的其他参数consumer: 指定处理消息的消费者对象。拉模式BasicGetResult result = channel.BasicGet(“queueName”, noAck: false);//获取消息channel.BasicAck(result.DeliveryTag, multiple: false);//确认3.2.6 关闭连接在应用程序使用完之后,需要关闭连接,释放资源:channel.close();conn.close() ;显式地关闭Channel 是个好习惯,但这不是必须的,在Connection 关闭的时候,Channel 也会自动关闭。结束语以上简单介绍了分布式系统中消息中间件的概念与作用,以及RabbitMQ的一些基本概念与简单使用。下一篇文章将继续针对RabbitMQ进行总结。主要内容包括何时创建队列、RabbitMQ的确认机制、过期时间的使用、死信队列、以及利用RabbitMQ实现延迟队列……大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. 原文连接:https://www.cnblogs.com/hunte… ...

September 26, 2018 · 2 min · jiezi

良好的RPC接口设计,需要注意这些方面

RPC 框架的讨论一直是各个技术交流群中的热点话题,阿里的 dubbo,新浪微博的 motan,谷歌的 grpc,以及不久前蚂蚁金服开源的 sofa,都是比较出名的 RPC 框架。RPC 框架,或者一部分人习惯称之为服务治理框架,更多的讨论是存在于其技术架构,比如 RPC 的实现原理,RPC 各个分层的意义,具体 RPC 框架的源码分析…但却并没有太多话题和“如何设计 RPC 接口”这样的业务架构相关。可能很多小公司程序员还是比较关心这个问题的,这篇文章主要分享下一些个人眼中 RPC 接口设计的最佳实践。初识 RPC 接口设计由于 RPC 中的术语每个程序员的理解可能不同,所以文章开始,先统一下 RPC 术语,方便后续阐述。大家都知道共享接口是 RPC 最典型的一个特点,每个服务对外暴露自己的接口,该模块一般称之为 api;外部模块想要实现对该模块的远程调用,则需要依赖其 api;每个服务都需要有一个应用来负责实现自己的 api,一般体现为一个独立的进程,该模块一般称之为 app。api 和 app 是构建微服务项目的最简单组成部分,如果使用 maven 的多 module 组织代码,则体现为如下的形式。serviceA 服务serviceA/pom.xml 定义父 pom 文件 <modules> <module>serviceA-api</module> <module>serviceA-app</module></modules><packaging>pom</packaging><groupId>moe.cnkirito</groupId><artifactId>serviceA</artifactId><version>1.0.0-SNAPSHOT</version>serviceA/serviceA-api/pom.xml 定义对外暴露的接口,最终会被打成 jar 包供外部服务依赖 <parent> <artifactId>serviceA</artifactId> <groupId>moe.cnkirito</groupId> <version>1.0.0-SNAPSHOT</version></parent><packaging>jar</packaging><artifactId>serviceA-api</artifactId>serviceA/serviceA-app/pom.xml 定义了服务的实现,一般是 springboot 应用,所以下面的配置文件中,我配置了 springboot 应用打包的插件,最终会被打成 jar 包,作为独立的进程运行。 <parent> <artifactId>serviceA</artifactId> <groupId>moe.cnkirito</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <packaging>jar</packaging> <artifactId>serviceA-app</artifactId> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>麻雀虽小,五脏俱全,这样一个微服务模块就实现了。旧 RPC 接口的痛点统一好术语,这一节来描述下我曾经遭遇过的 RPC 接口设计的痛点,相信不少人有过相同的遭遇。查询接口过多各种 findBy 方法,加上各自的重载,几乎占据了一个接口 80% 的代码量。这也符合一般人的开发习惯,因为页面需要各式各样的数据格式,加上查询条件差异很大,便造成了:一个查询条件,一个方法的尴尬场景。这样会导致另外一个问题,需要使用某个查询方法时,直接新增了方法,但实际上可能这个方法已经出现过了,隐藏在了令人眼花缭乱的方法中。难以扩展接口的任何改动,比如新增一个入参,都会导致调用者被迫升级,这也通常是 RPC 设计被诟病的一点,不合理的 RPC 接口设计会放大这个缺点。升级困难在之前的 “初识 RPC 接口设计”一节中,版本管理的粒度是 project,而不是 module,这意味着:api 即使没有发生变化,app 版本演进,也会造成 api 的被迫升级,因为 project 是一个整体。问题又和上一条一样了,api 一旦发生变化,调用者也得被迫升级,牵一发而动全身。难以测试接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护。特别是对于那些有良好习惯编写单元测试的程序员而言,简直是噩梦,用例也得跟着改。异常设计不合理在既往的工作经历中曾经有一次会议,就 RPC 调用中的异常设计引发了争议,一派人觉得需要有一个业务 CommonResponse,封装异常,每次调用后,优先判断调用结果是否 success,在进行业务逻辑处理;另一派人觉得这比较麻烦,由于 RPC 框架是可以封装异常调用的,所以应当直接 try catch 异常,不需要进行业务包裹。在没有明确规范时,这两种风格的代码同时存在于项目中,十分难看!单参数接口如果你使用过 springcloud ,可能会不适应 http 通信的限制,因为 @RequestBody 只能使用单一的参数,也就意味着,springcloud 构建的微服务架构下,接口天然是单参数的。而 RPC 方法入参的个数在语法层面是不会受到限制的,但如果强制要求入参为单参数,会解决一部分的痛点。使用 Specification 模式解决查询接口过多的问题public interface StudentApi{Student findByName(String name);List<Student> findAllByName(String name);Student findByNameAndNo(String name,String no);Student findByIdcard(String Idcard);}如上的多个查询方法目的都是同一个:根据条件查询出 Student,只不过查询条件有所差异。试想一下,Student 对象假设有 10 个属性,最坏的情况下它们的排列组合都可能作为查询条件,这便是查询接口过多的根源。public interface StudentApi{Student findBySpec(StudentSpec spec);List<Student> findListBySpec(StudentListSpec spec);Page<Student> findPageBySpec(StudentPageSpec spec);}上述接口便是最通用的单参接口,三个方法几乎囊括了 99% 的查询条件。所有的查询条件都被封装在了 StudentSpec,StudentListSpec,StudentPageSpec 之中,分别满足了单对象查询,批量查询,分页查询的需求。如果你了解领域驱动设计,会发现这里借鉴了其中 Specification 模式的思想。单参数易于做统一管理 public interface SomeProvider {void opA(ARequest request);void opB(BRequest request);CommonResponse<C> opC(CRequest request); }入参中的入参虽然形态各异,但由于是单个入参,所以可以统一继承 AbstractBaseRequest,即上述的 ARequest,BRequest,CRequest 都是 AbstractBaseRequest 的子类。在千米内部项目中,AbstractBaseRequest 定义了 traceId、clientIp、clientType、operationType 等公共入参,减少了重复命名,我们一致认为,这更加的 OO。有了 AbstractBaseRequest,我们可以更加轻松地在其之上做 AOP,千米的实践中,大概做了如下的操作:请求入参统一校验(request.checkParam(); param.checkParam();)实体变更统一加锁,降低锁粒度请求分类统一处理(if (request instanceof XxxRequest))请求报文统一记日志(log.setRequest(JsonUtil.getJsonString(request)))操作成功统一发消息如果不遵守单参数的约定,上述这些功能也并不是无法实现,但所需花费的精力远大于单参数,一个简单的约定带来的优势,我们认为是值得的。单参数入参兼容性强还记得前面的小节中,我提到了 SpringCloud,在 SpringCloud Feign 中,接口的入参通常会被 @RequestBody 修饰,强制做单参数的限制。千米内部使用了 Dubbo 作为 Rpc 框架,一般而言,为 Dubbo 服务设计的接口是不能直接用作 Feign 接口的(主要是因为 @RequestBody 的限制),但有了单参数的限制,便使之成为了可能。为什么我好端端的 Dubbo 接口需要兼容 Feign 接口?可能会有人发出这样的疑问,莫急,这样做的初衷当然不是为了单纯做接口兼容,而是想充分利用 HTTP 丰富的技术栈以及一些自动化工具。自动生成 HTTP 接口实现(让服务端同时支持 Dubbo 和 HTTP 两种服务接口)看过我之前文章的朋友应该了解过一个设计:千米内部支持的是 Dubbo 协议和 HTTP 协议族(如 JSON RPC 协议,Restful 协议),这并不意味着程序员需要写两份代码,我们可以通过 Dubbo 接口自动生成 HTTP 接口,体现了单参数设计的兼容性之强。通过 Swagger UI 实现对 Dubbo 接口的可视化便捷测试又是一个兼容 HTTP 技术栈带来的便利,在 Restful 接口的测试中,Swagger 一直是备受青睐的一个工具,但可惜的是其无法对 Dubbo 接口进行测试。兼容 HTTP 后,我们只需要做一些微小的工作,便可以实现 Swagger 对 Dubbo 接口的可视化测试。有利于 TestNg 集成测试自动生成 TestNG 集成测试代码和缺省测试用例,这使得服务端接口集成测试变得异常简单,程序员更能集中精力设计业务用例,结合缺省用例、JPA 自动建表和 PowerMock 模拟外部依赖接口实现本机环境。这块涉及到了公司内部的代码,只做下简单介绍,我们一般通过内部项目 com.qianmi.codegenerator:api-dubbo-2-restful ,com.qianmi.codegenerator:api-request-json 生成自动化的测试用例,方便测试。而这些自动化工具中大量使用了反射,而由于单参数的设计,反射用起来比较方便。接口异常设计首先肯定一点,RPC 框架是可以封装异常的,Exception 也是返回值的一部分。在 go 语言中可能更习惯于返回 err,res 的组合,但 JAVA 中我个人更偏向于 try catch 的方法捕获异常。RPC 接口设计中的异常设计也是一个注意点。初始方案 public interface ModuleAProvider { void opA(ARequest request); void opB(BRequest request); CommonResponse<C> opC(CRequest request); }我们假设模块 A 存在上述的 ModuleAProvider 接口,ModuleAProvider 的实现中或多或少都会出现异常,例如可能存在的异常 ModuleAException,调用者实际上并不知道 ModuleAException 的存在,只有当出现异常时,才会知晓。对于 ModuleAException 这种业务异常,我们更希望调用方能够显示的处理,所以 ModuleAException 应该被设计成 Checked Excepition。正确的异常设计姿势public interface ModuleAProvider {void opA(ARequest request) throws ModuleAException;void opB(BRequest request) throws ModuleAException;CommonResponse<C> opC(CRequest request) throws ModuleAException;}上述接口中定义的异常实际上也是一种契约,契约的好处便是不需要叙述,调用方自然会想到要去处理 Checked Exception,否则连编译都过不了。调用方的处理方式在 ModuleB 中,应当如下处理异常: public class ModuleBService implements ModuleBProvider {@ReferenceModuleAProvider moduleAProvider;@Overridepublic void someOp() throws ModuleBexception{ try{ moduleAProvider.opA(…); }catch(ModuleAException e){ throw new ModuleBException(e.getMessage()); }}@Overridepublic void anotherOp(){ try{ moduleAProvider.opB(…); }catch(ModuleAException e){ // 业务逻辑处理 }}}someOp 演示了一个异常流的传递,ModuleB 暴露出去的异常应当是 ModuleB 的 api 模块中异常类,虽然其依赖了 ModuleA ,但需要将异常进行转换,或者对于那些意料之中的业务异常可以像 anotherOp() 一样进行处理,不再传递。这时如果新增 ModuleC 依赖 ModuleB,那么 ModuleC 完全不需要关心 ModuleA 的异常。异常与熔断作为系统设计者,我们应该认识到一点: RPC 调用,失败是常态。通常我们需要对 RPC 接口做熔断处理,比如千米内部便集成了 Netflix 提供的熔断组件 Hystrix。Hystrix 需要知道什么样的异常需要进行熔断,什么样的异常不能够进行熔断。在没有上述的异常设计之前,回答这个问题可能还有些难度,但有了 Checked Exception 的契约,一切都变得明了清晰了。public class ModuleAProviderProxy {@Referenceprivate ModuleAProvider moduleAProvider;@HystrixCommand(ignoreExceptions = {ModuleAException.class})public void opA(ARequest request) throws ModuleAException { moduleAProvider.opA(request);}@HystrixCommand(ignoreExceptions = {ModuleAException.class})public void opB(BRequest request) throws ModuleAException { moduleAProvider.oBB(request);}@HystrixCommand(ignoreExceptions = {ModuleAException.class})public CommonResponse<C> opC(CRequest request) throws ModuleAException { return moduleAProvider.opC(request);}}如服务不可用等原因引发的多次接口调用超时异常,会触发 Hystrix 的熔断;而对于业务异常,我们则认为不需要进行熔断,因为对于接口 throws 出的业务异常,我们也认为是正常响应的一部分,只不过借助于 JAVA 的异常机制来表达。实际上,和生成自动化测试类的工具一样,我们使用了另一套自动化的工具,可以由 Dubbo 接口自动生成对应的 Hystrix Proxy。我们坚定的认为开发体验和用户体验一样重要,所以公司内部会有非常多的自动化工具。API 版本单独演进引用一段公司内部的真实对话:A:我下载了你们的代码库怎么编译不通过啊,依赖中 xxx-api-1.1.3 版本的 jar 包找不到了,那可都是 RELEASE 版本啊。B:你不知道我们 nexus 容量有限,只能保存最新的 20 个 RELEASE 版本吗?那个 API 现在最新的版本是 1.1.31 啦。A:啊,这才几个月就几十个 RELEASE 版本啦?这接口太不稳定啦。B: 其实接口一行代码没改,我们业务分析是很牛逼的,一直很稳定。但是这个 API是和我们项目一起打包的,我们需求更新一次,就发布一次,API 就被迫一起升级版本。发生这种事,大家都不想的。在单体式架构中,版本演进的单位是整个项目。微服务解决的一个关键的痛点便是其做到了每个服务的单独演进,这大大降低了服务间的耦合。正如我文章开始时举得那个例子一样:serviceA 是一个演进的单位,serviceA-api 和 serviceA-app 这两个 Module 从属于 serviceA,这意味着 app 的一次升级,将会引发 api 的升级,因为他们是共生的!而从微服务的使用角度来看,调用者关心的是 api 的结构,而对其实现压根不在乎。所以对于 api 定义未发生变化,其 app 发生变化的那些升级,其实可以做到对调用者无感知。在实践中也是如此api 版本的演进应该是缓慢的,而 app 版本的演进应该是频繁的。所以,对于这两个演进速度不一致的模块,我们应该单独做版本管理,他们有自己的版本号。问题回归查询接口过多各种 findBy 方法,加上各自的重载,几乎占据了一个接口 80% 的代码量。这也符合一般人的开发习惯,因为页面需要各式各样的数据格式,加上查询条件差异很大,便造成了:一个查询条件,一个方法的尴尬场景。这样会导致另外一个问题,需要使用某个查询方法时,直接新增了方法,但实际上可能这个方法已经出现过了,隐藏在了令人眼花缭乱的方法中。解决方案:使用单参+Specification 模式,降低重复的查询方法,大大降低接口中的方法数量。难以扩展接口的任何改动,比如新增一个入参,都会导致调用者被迫升级,这也通常是 RPC 设计被诟病的一点,不合理的 RPC 接口设计会放大这个缺点。解决方案:单参设计其实无形中包含了所有的查询条件的排列组合,可以直接在 app 实现逻辑的新增,而不需要对 api 进行改动(如果是参数的新增则必须进行 api 的升级,参数的废弃可以用 @Deprecated 标准)。升级困难在之前的 “初识 RPC 接口设计”一节中,版本管理的粒度是 project,而不是 module,这意味着:api 即使没有发生变化,app 版本演进,也会造成 api 的被迫升级,因为 project 是一个整体。问题又和上一条一样了,api 一旦发生变化,调用者也得被迫升级,牵一发而动全身。解决方案:以 module 为版本演进的粒度。api 和 app 单独演进,减少调用者的不必要升级次数。难以测试接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护。特别是对于那些有良好习惯编写单元测试的程序员而言,简直是噩梦,用例也得跟着改。解决方案:单参数设计+自动化测试工具,打造良好的开发体验。异常设计不合理在既往的工作经历中曾经有一次会议,就 RPC 调用中的异常设计引发了争议,一派人觉得需要有一个业务 CommonResponse,封装异常,每次调用后,优先判断调用结果是否 success,在进行业务逻辑处理;另一派人觉得这比较麻烦,由于 RPC 框架是可以封装异常调用的,所以应当直接 try catch 异常,不需要进行业务包裹。在没有明确规范时,这两种风格的代码同时存在于项目中,十分难看!解决方案:Checked Exception+正确异常处理姿势,使得代码更加优雅,降低了调用方不处理异常带来的风险。原文出处:https://www.jianshu.com/p/dca…作者:占小狼 ...

September 24, 2018 · 3 min · jiezi

分库分表后如何部署上线?

引言我们先来讲一个段子面试官:“有并发的经验没?”应聘者:“有一点。”面试官:“那你们为了处理并发,做了哪些优化?”应聘者:“前后端分离啊,限流啊,分库分表啊。。”面试官:“谈谈分库分表吧?“应聘者:“bala。bala。bala。。”面试官心理活动:这个仁兄讲的怎么这么像网上的博客抄的,容我再问问。面试官:“你们分库分表后,如何部署上线的?”应聘者:“这!!!!!!”不要惊讶,写这篇文章前,我特意去网上看了下分库分表的文章,很神奇的是,都在讲怎么进行分库分表,却不说分完以后,怎么部署上线的。这样在面试的时候就比较尴尬了。你们自己摸着良心想一下,如果你真的做过分库分表,你会不知道如何部署的么?因此我们来学习一下如何部署吧。ps: 我发现一个很神奇的现象。因为很多公司用的技术比较low,那么一些求职者为了提高自己的竞争力,就会将一些高大上的技术写进自己的low项目中。然后呢,他出去面试害怕碰到从这个公司出来的人,毕竟从这个公司出来的人,一定知道自己以前公司的项目情形。因此为了圆谎,他就会说:“他们从事的是这个公司的老项目改造工作,用了很多新技术进去!”那么,请你好好思考一下,你们的老系统是如何平滑升级为新系统的!如何部署停机部署法大致思路就是,挂一个公告,半夜停机升级,然后半夜把服务停了,跑数据迁移程序,进行数据迁移。步骤如下:(1)出一个公告,比如“今晚00:00~6:00进行停机维护,暂停服务”(2)写一个迁移程序,读 db-old 数据库,通过中间件写入新库 db-new1 和 db-new2 ,具体如下图所示(3)校验迁移前后一致性,没问题就切该部分业务到新库。顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。评价:大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。另外,如果面试官的问题是你们怎么进行分库分表的?这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。不过,很多面试官为了卖弄自己的技术,喜欢这么问分表有哪些策略啊?你们用哪种啊?ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案?那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:“这个小伙子真的做过分库分表。“如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。双写部署法(一)这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。然后迁移流程如下(1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。(2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍!原因有二:(1)只有写请求的sql对恢复数据才有用。(2)系统中,绝大部分的业务需求是读请求,写请求比较少。注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重!(3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。上面步骤(1)~步骤(3)的过程如下(3)校验迁移前后一致性,没问题就切该部分业务到新库。顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。评价:大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。另外,如果面试官的问题是你们怎么进行分库分表的?这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。不过,很多面试官为了卖弄自己的技术,喜欢这么问分表有哪些策略啊?你们用哪种啊?ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案?那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:“这个小伙子真的做过分库分表。“如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。双写部署法(一)这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。然后迁移流程如下(1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。(2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍!原因有二:(1)只有写请求的sql对恢复数据才有用。(2)系统中,绝大部分的业务需求是读请求,写请求比较少。注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重!(3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。上面步骤(1)~步骤(3)的过程如下等到 db-old 中的历史数据迁移完毕,则开始迁移增量数据,也就是在消息队列里的数据。(4)将迁移程序下线,写一段订阅程序订阅消息队列中的数据(5)订阅程序将订阅到到数据,通过中间件写入新库(6)新老库一致性验证,去除代码中的双写代码,将涉及到 test_tb 表的读写操作,指向新库。上面步骤(4)~步骤(6)的过程如下这里大家可能会有一个问题,在步骤(1)~步骤(3),系统对历史数据进行操作,会造成不一致的问题么?OK,不会。这里我们对 delete 操作和 update 操作做分析,因为只有这两个操作才会造成历史数据变动, insert 进去的数据都是属于增量数据。(1)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据还未删除,就被迁移程序给迁走了。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,可以进行删除。(2)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据已经删除,迁移程序迁不走该行数据。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,再执行一次 delete ,并不会对一致性有影响。对 update 的操作类似,不赘述。双写部署法(二)上面的方法有一个硬伤,注意我有一句话(2)在代码中,与test_tb有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。大家想一下,这么做,是不是造成了严重的代码入侵。将非业务代码嵌入业务代码,这么做,后期删代码的时候特别累。有没什么方法,可以避免这个问题的?有的,订阅 binlog 日志。关于 binlog 日志,我尽量下周写一篇《研发应该掌握的binlog知识》,这边我就介绍一下作用记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。binlog不会记录SELECT和SHOW这类操作,因为这类操作对据本身并没有修改。还记得我们在 双写部署法(一) 里介绍的,往消息队列里发的消息,都是写操作的消息。而 binlog 日志记录的也是写操作。所以订阅该日志,也能满足我们的需求。于是步骤如下(1)打开binlog日志,系统正常上线就好(2)还是写一个迁移程序,迁移历史数据。步骤和上面类似,不啰嗦了。步骤(1)~步骤(2)流程图如下(3)写一个订阅程序,订阅binlog(mysql中有 canal 。至于oracle中,大家就随缘自己写吧)。然后将订阅到到数据通过中间件,写入新库。(4)检验一致性,没问题就切库。步骤(3)~步骤(4)流程图如下怎么验数据一致性这里大概介绍一下吧,这篇的篇幅太长了,大家心里有底就行。(1)先验数量是否一致,因为验数量比较快。至于验具体的字段,有两种方法:(2.1)有一种方法是,只验关键性的几个字段是否一致。(2.2)还有一种是 ,一次取50条(不一定50条,具体自己定,我只是举例),然后像拼字符串一样,拼在一起。用md5进行加密,得到一串数值。新库一样如法炮制,也得到一串数值,比较两串数值是否一致。如果一致,继续比较下50条数据。如果发现不一致,用二分法确定不一致的数据在0-25条,还是26条-50条。以此类推,找出不一致的数据,进行记录即可。合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代! ...

September 20, 2018 · 1 min · jiezi

我是这样手写 Spring 的(麻雀虽小五脏俱全)

人见人爱的 Spring 已然不仅仅只是一个框架了。如今,Spring 已然成为了一个生态。但深入了解 Spring 的却寥寥无几。这里,我带大家一起来看看,我是如何手写 Spring 的。我将结合对 Spring 十多年的研究经验,用不到 400 行代码来描述 Spring IOC、DI、MVC 的精华设计思想,并保证基本功能完整。首先,我们先来介绍一下 Spring 的三个阶段,配置阶段、初始化阶段和运行阶段(如图):配置阶段:主要是完成 application.xml 配置和 Annotation 配置。初始化阶段:主要是加载并解析配置信息,然后,初始化 IOC 容器,完成容器的 DI 操作,已经完成 HandlerMapping 的初始化。运行阶段:主要是完成 Spring 容器启动以后,完成用户请求的内部调度,并返回响应结果。先来看看我们的项目结构 (如下图)一、配置阶段我采用的是 maven 管理项目。先来看 pom.xml 文件中的配置,我只引用了 servlet-api 的依赖。然后,创建 GPDispatcherServlet 类并继承 HttpServlet,重写 init、doGet 和 doPost 方法。在 web.xml 文件中配置以下信息:在中,我们配置了一个初始化加载的 Spring 主配置文件路径,在原生框架中,我们应该配置的是 classpath:application.xml。在这里,我们为了简化操作,用 properties 文件代替 xml 文件。以下是 properties 文件中的内容:接下来,我们要配置注解。现在,我们不使用 Spring 的一针一线,所有注解全部自己手写。创建 GPController 注解:创建 GPRequestMapping 注解:创建 GPService 注解:创建 GPAutowired 注解:创建 GPRequestParam 注释:使用自定义注解进行配置:到此,我们把配置阶段的代码全部手写完成。二、初始化阶段先在 GPDispatcherServlet 中声明几个成员变量:当 Servlet 容器启动时,会调用 GPDispatcherServlet 的 init方法,从 init 方法的参数中,我们可以拿到主配置文件的路径,从能够读取到配置文件中的信息。前面我们已经介绍了 Spring 的三个阶段,现在来完成初始化阶段的代码。在 init 方法中,定义好执行步骤,如下:doLoadConfig 方法的实现,将文件读取到 Properties 对象中:doScanner 方法,递归扫描出所有的 Class 文件doInstance 方法,初始化所有相关的类,并放入到 IOC 容器之中。IOC 容器的 key 默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。因此,要先写一个针对类名首字母处理的工具方法。然后,再处理相关的类。doAutowired 方法,将初始化到 IOC 容器中的类,需要赋值的字段进行赋值initHandlerMapping 方法,将 GPRequestMapping 中配置的信息和 Method 进行关联,并保存这些关系。到此,初始化阶段的所有代码全部写完。三、运行阶段来到运行阶段,当用户发送请求被 Servlet 接受时,都会统一调用 doPost 方法,我先在 doPost 方法中再调用 doDispach 方法,代码如下:doDispatch 方法是这样写的:到此,我们完成了一个 mini 版本的 Spring,麻雀虽小,五脏俱全。我们把服务发布到 web 容器中,然后,在浏览器输入:http://localhost:8080/demo/query.json?name=Tom,就会得到下面的结果:当然,真正的 Spring 要复杂很多,但核心设计思路基本如此。例如:Spring 中真正的 HandlerMapping 是这样的:我在网络上也有现场直播手写 Spring,欢迎大家关注。如果在练习过程中有任何疑问,可以加我的架构群:586446657。欢迎工作一到五年的 Java 的工程师朋友们加入进来本群提供免费的学习指导架构资料以及免费的解答不懂得问题都可以在本群提出来之后还会有职业生涯规划以及面试指导 ...

September 20, 2018 · 1 min · jiezi

金色九月,那些曾经难倒你的面试真题

前言九月旺季已经来临,曾经何时,在17年的九月,经过再三考虑,我决定跳出一份干了四年且比较安逸的开发工作,当时下定决心,选择跳槽。最后跟着诸多农码一起涌进了一段为期两个月的面试高峰期。但是在选择跳出后,好多人都问我:“阿光,现在不是做的好好的么,怎么突然间就选择离职了,再说了,现在外面的工作也不好找啊,而且在这里薪资也还不错,为啥要走?”当时,面对这样的问题,我只用了几个字给回复他们了:“我是做开发的,但现在的工作不是我想像的那种,我不要在每天做哪些业务代码了,我要跳出舒适区,我要迎接新的挑战。”在这狂热的九月里,我的简历投了无数个,也面试好几家不错的一线互联网公司,但最后都没有面上,最后,还是无意间通过一个群的渠道,认识了几位大牛,最后报名,获取内推机会,最后经过努力,成功面入狗厂。算起来我还是挺幸运的。现在也是九月,现在回味起来,那时候面试的路上也是蛮艰辛的。最近这几天我抽空整理了一下17年的面试经验,相信这些面试经验对那些想跳出舒适区,近期想换工作的码农们,这些面试经验,希望对你们有所帮助。——后面我还总结了我的工作心得。源码分析真题你有没有用过Spring的AOP? 是用来干嘛的? 大概会怎么使用?说说你对Java注解的理解什么是依赖注入?什么是控制反转(IOC)? 在 Spring 中,有几种依赖注入方式?工厂模式你知道哪几种?你用过哪几种?每一种的用法知道么?在 Spring 中,有几种配置 Bean 的方式?SpringMVC中RequestMapping可以指定GET, POST用法么?怎么指定?SpringMVC如果希望把输出的Object(例如XXResult或者XXResponse)这 种包装为JSON输出, 应该怎么处理?分布式真题1、说说HashMap和Hashtable的区别 2、说一下实现一个保证迭代顺序的HashMap3、说一说排序算法,稳定性,复杂度4、说一说GC5、JVM如何加载一个类的过程,双亲委派模型中有哪些方法?6、 TCP如何保证可靠传输?三次握手过程?7、你们用什么Redis客户端? Redis高性能的原因大概可以讲一些?8、你熟悉哪些Redis的数据结构? zset是干什么的? 和set有什么区别?微服务真题1、什么是微服务?你知道有哪些框架?用过哪些框架?2、springCloud和dubbo 有哪些区别?3、什么是微服务熔断?什么是服务降级?4、springboot和springcloud,请你谈谈对他们的理解?5、你所知道的微服务技术栈有哪些?请列举一二6、说说 RPC的实现原理7、说说 Dubbo的实现原理并发编程真题1、并发了解么?说说看你对并发的理解2、什么是线程?线程和进程有什么区别?如何在Java中实现线程?3、死锁与活锁的区别,死锁与饥饿的区别?4、你知道在java中守护线程和本地线程区别么,说说你的看法?5、Java中用到的线程调度算法是什么?性能优化真题1、JVM内存分哪几个区,每个区的作用是什么?2、如和判断一个对象是否存活?(或者GC对象的判定方法)3、简述java垃圾回收机制?4、java中垃圾收集的方法有哪些?5、如何自定义一个类加载器?你使用过哪些或者你在什么场景下需要一个自定义的类加载器吗?6、做gc时,一个对象在内存各个Space中被移动的顺序是什么?7、你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理 过程中有哪些收获?上面就是我的面试笔记记录,这几个面试点应该是面试比较喜欢问的了,特别是分布式和JVM这些,如果想进大厂且薪资高的话,这两个知识点是必学要弄清楚的。还有就是你的个人见解了,这些都是非常重要的了。废话不多说,大家往下看我的近期总结学习心得,继续往下看干货………..(如果想和作者一样系统化的学习面试真题后面的架构体系图,大家可以加文章末尾的群号,里面有学习资料,加群备注即可领取。我只能帮你们到这里了。我也在群里,希望和大家共同学习进步。)程序员应有的几个阶段第一阶段—-三年我认为三年对于程序员来说是第一个门槛,这个阶段将会淘汰掉一批不适合写代码的人。这一阶段,我们走出校园,迈入社会,成为一名程序员,正式从书本上的内容迈向真正的企业级开发。我们知道如何团队协作、如何使用项目管理工具、项目版本如何控制、我们写的代码如何测试如何在线上运行等等,积累了一定的开发经验,也对代码有了一定深入的认识,是一个比较纯粹的Coder的阶段。第二阶段—-五年五年又是区分程序员的第二个门槛。有些人在三年里,除了完成工作,在空余时间基本不会研究别的东西,这些人永远就是个Coder,年纪大一些势必被更年轻的人给顶替;有些人在三年里,除了写代码之外,还热衷于研究各种技术实现细节、看了N多好书、写一些博客、在Github上分享技术,这些人在五年后必然具备在技术上独当一面的能力并且清楚自己未来的发展方向,从一个Coder逐步走向系统分析师或是架构师,成为项目组中不可或缺的人物。第三阶段—-十年十年又是另一个门槛了,转行或是继续做一名程序员就在这个节点上。如果在前几年就抱定不转行的思路并且为之努力的话,那么在十年的这个节点上,有些人必然成长为一名对行业有着深入认识、对技术有着深入认识、能从零开始对一个产品进行分析的程序员,这样的人在公司基本担任的都是CTO、技术专家、首席架构师等最关键的职位,这对于自己绝对是一件荣耀的事,当然老板在经济上也绝不会亏待你。我认为,随着你工作年限的增长、对生活对生命认识的深入,应当不断思考三个问题:我到底适不适合当一名程序员?我到底应不应该一辈子以程序员为职业?我对编程到底持有的是一种什么样的态度,是够用就好呢还是不断研究?最终,明确自己的职业规划,对自己的规划负责并为之努力。如何成为一名优秀的程序员1.愿意学习新技术随着技术的不断进步,我们学到和实施的技术会很快地过时。所以,作为一个程序员,你就需要更新你的技能,保持与时俱进。市场上出现的所谓新技术通常由一些进程和语法变化而构成,但逻辑是相通的,所以你可以很快地掌握它。2.调试技巧程序员不但需要创建代码,而且当软件不按预期方式工作时,程序员还必须能够快速而有效地解决问题。因此,与其更改所有代码,还不如在创建程序时制作适当的流程文档,以便你可以快速检查代码并尽快找到问题。制作正确的文档可以方便你快速调试程序,而不浪费时间。3.解决问题的技巧当一名普通的程序员获得项目/模块时,他们会直接写代码。但一名成功的程序员会试图找出代码出现问题的根源,并通知团队领导或项目负责人。因为有时在文档化项目需求并启动项目后,或者甚至在完成项目后,我们才会在项目中遇到一些问题。所以最好在出现问题之前先找到问题。此外,快速地找到解决问题的方法。4.对工作的热情我们的工作时间大多为朝九晚五,但是当你对工作迸发激情时,那么不要到了休息日就将工作束之高阁。一直工作直到完成它。当然,我的意思不是说如果你没有解决方案,还得成天垂头丧气对着电脑,我的意思是如果你已经接近于完成的时候,那么索性一鼓作气搞定吧。你的经理或高层将会欣赏你的工作,并且当你有一个良好的环境时,你的激情将会越发高昂。另外,在没有工作的时候也不要无所事事。不妨尝试构建一些新的应用程序,如游戏,拼图,聊天应用程序等,这将有助于使你更加热爱你的工作。5.懒惰…我的意思是更高效!当有很多任务并且快没有时间来完成项目的时候,这时只有懒惰的程序员才能找到更好和最快的解决方案,因为他非常了解如何才能事半功倍。如果你想找到做事的最好方式,那么就去问懒惰的人;很多时候这些所谓的懒人会找到最好、最快和最有效的方法,因为他们总在试图寻找更高效的路径方法。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..

September 7, 2018 · 1 min · jiezi

细数那些不懂Spring底层原理带来的伤与痛

什么是spring?Spring 是个Java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程模型促进良好的编程习惯。2. 使用Spring框架的好处是什么?轻量:Spring 是轻量的,基本的版本大约2MB。控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。容器:Spring 包含并管理应用中对象的生命周期和配置。MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。3. Spring由哪些模块组成?以下是Spring 框架的基本模块:Core moduleBean moduleContext moduleExpression Language moduleJDBC moduleORM moduleOXM moduleJava Messaging Service(JMS) moduleTransaction moduleWeb moduleWeb-Servlet moduleWeb-Struts moduleWeb-Portlet module4. 核心容器(应用上下文) 模块。这是基本的Spring模块,提供spring 框架的基础功能,BeanFactory 是 任何以spring为基础的应用的核心。Spring 框架建立在此模块之上,它使Spring成为一个容器。5. BeanFactory – BeanFactory 实现举例。Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离。最常用的BeanFactory 实现是XmlBeanFactory 类。6. XMLBeanFactory最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ,它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。7. 解释AOP模块AOP模块用于发给我们的Spring应用做面向切面的开发, 很多支持由AOP联盟提供,这样就确保了Spring和其他AOP框架的共通性。这个模块将元数据编程引入Spring。8. 解释JDBC抽象和DAO模块。通过使用JDBC抽象和DAO模块,保证数据库代码的简洁,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上,提供了一个统一的异常访问层。它还利用Spring的AOP 模块给Spring应用中的对象提供事务管理服务。9. 解释对象/关系映射集成模块。Spring 通过提供ORM模块,支持我们在直接JDBC之上使用一个对象/关系映射映射(ORM)工具,Spring 支持集成主流的ORM框架,如Hiberate,JDO和 iBATIS SQL Maps。Spring的事务管理同样支持以上所有ORM框架及JDBC。10. 解释WEB 模块。Spring的WEB模块是构建在application context 模块基础之上,提供一个适合web应用的上下文。这个模块也包括支持多种面向web的任务,如透明地处理多个文件上传请求和程序级请求参数的绑定到你的业务对象。它也有对Jakarta Struts的支持。11. Spring配置文件Spring配置文件是个XML 文件,这个文件包含了类信息,描述了如何配置它们,以及如何相互调用。12. 什么是Spring IOC 容器?Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。13. IOC的优点是什么?IOC 或 依赖注入把应用的代码量降到最低。它使应用容易测试,单元测试不再需要单例和JNDI查找机制。最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载。15. ApplicationContext通常的实现是什么?FileSystemXmlApplicationContext :此容器从一个XML文件中加载beans的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。ClassPathXmlApplicationContext:此容器也从一个XML文件中加载beans的定义,这里,你需要正确设置classpath因为这个容器将在classpath里找bean配置。WebXmlApplicationContext:此容器加载一个XML文件,此文件定义了一个WEB应用的所有bean。16. Bean 工厂和 Application contexts 有什么区别?Application contexts提供一种方法处理文本消息,一个通常的做法是加载文件资源(比如镜像),它们可以向注册为监听器的bean发布事件。另外,在容器或容器内的对象上执行的那些不得不由bean工厂以程序化方式处理的操作,可以在Application contexts中以声明的方式处理。Application contexts实现了MessageSource接口,该接口的实现以可插拔的方式提供获取本地化消息的方法。17. 一个Spring的应用看起来象什么?一个定义了一些功能的接口。这实现包括属性,它的Setter , getter 方法和函数等。Spring AOP。Spring 的XML 配置文件。使用以上功能的客户端程序。依赖注入18. 什么是Spring的依赖注入?依赖注入,是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,之后一个容器(IOC容器)负责把他们组装起来。19. 有哪些不同类型的IOC(依赖注入)方式?构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。20. 哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?你两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。Spring Beans21.什么是Spring beans?Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中<bean/> 的形式定义。Spring 框架定义的beans都是单件beans。在bean tag中有个属性”singleton”,如果它被赋为TRUE,bean 就是单件,否则就是一个 prototype bean。默认是TRUE,所以所有在Spring框架中的beans 缺省都是单件。22. 一个 Spring Bean 定义 包含什么?一个Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。23. 如何给Spring 容器提供配置元数据?这里有三种重要的方法给Spring 容器提供配置元数据。XML配置文件。基于注解的配置。基于java的配置。24. 你怎样定义类的作用域?当定义一个<bean> 在Spring里,我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定义。如,当Spring要在需要的时候每次生产一个新的bean实例,bean的scope属性被指定为prototype。另一方面,一个bean每次使用的时候必须返回同一个实例,这个bean的scope 属性 必须设为 singleton。25. 解释Spring支持的几种bean的作用域。Spring框架支持以下五种bean的作用域:singleton : bean在每个Spring ioc 容器中只有一个实例。prototype:一个bean的定义可以有多个实例。request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。缺省的Spring bean 的作用域是Singleton.26. Spring框架中的单例bean是线程安全的吗?不,Spring框架中的单例bean不是线程安全的。27. 解释Spring框架中bean的生命周期。Spring容器 从XML 文件中读取bean的定义,并实例化bean。Spring根据bean的定义填充所有的属性。如果bean实现了BeanNameAware 接口,Spring 传递bean 的ID 到 setBeanName方法。如果Bean 实现了 BeanFactoryAware 接口, Spring传递beanfactory 给setBeanFactory 方法。如果有任何与bean相关联的BeanPostProcessors,Spring会在postProcesserBeforeInitialization()方法内调用它们。如果bean实现IntializingBean了,调用它的afterPropertySet方法,如果bean声明了初始化方法,调用此初始化方法。如果有BeanPostProcessors 和bean 关联,这些bean的postProcessAfterInitialization() 方法将被调用。如果bean实现了 DisposableBean,它将调用destroy()方法。28. 哪些是重要的bean生命周期方法? 你能重载它们吗?有两个重要的bean 生命周期方法,第一个是setup , 它是在容器加载bean的时候被调用。第二个方法是 teardown 它是在容器卸载类的时候被调用。The bean 标签有两个重要的属性(init-method和destroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct和@PreDestroy)。29. 什么是Spring的内部bean?当一个bean仅被用作另一个bean的属性时,它能被声明为一个内部bean,为了定义inner bean,在Spring 的 基于XML的 配置元数据中,可以在 <property/>或 <constructor-arg/> 元素内使用<bean/> 元素,内部bean通常是匿名的,它们的Scope一般是prototype。30. 在 Spring中如何注入一个java集合?Spring提供以下几种集合的配置元素:<list>类型用于注入一列值,允许有相同的值。<set> 类型用于注入一组值,不允许有相同的值。<map> 类型用于注入一组键值对,键和值都可以为任意类型。<props>类型用于注入一组键值对,键和值都只能为String类型。31. 什么是bean装配?装配,或bean 装配是指在Spring 容器中把bean组装到一起,前提是容器需要知道bean的依赖关系,如何通过依赖注入来把它们装配到一起。32. 什么是bean的自动装配?Spring 容器能够自动装配相互合作的bean,这意味着容器不需要<constructor-arg>和<property>配置,能通过Bean工厂自动处理bean之间的协作。33. 解释不同方式的自动装配 。有五种自动装配的方式,可以用来指导Spring容器用自动装配方式来进行依赖注入。no:默认的方式是不进行自动装配,通过显式设置ref 属性来进行装配。byName:通过参数名 自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。byType::通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。constructor:这个方式类似于byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。34.自动装配有哪些局限性 ?自动装配的局限性是:重写: 你仍需用 <constructor-arg>和 <property> 配置来定义依赖,意味着总要重写自动装配。基本数据类型:你不能自动装配简单的属性,如基本数据类型,String字符串,和类。模糊特性:自动装配不如显式装配精确,如果有可能,建议使用显式装配。35. 你可以在Spring中注入一个null 和一个空字符串吗?可以。Spring注解36. 什么是基于Java的Spring注解配置? 给一些注解的例子.基于Java的配置,允许你在少量的Java注解的帮助下,进行你的大部分Spring配置而非通过XML文件。以@Configuration 注解为例,它用来标记类可以当做一个bean的定义,被Spring IOC容器使用。另一个例子是@Bean注解,它表示此方法将要返回一个对象,作为一个bean注册进Spring应用上下文。37. 什么是基于注解的容器配置?相对于XML文件,注解型的配置依赖于通过字节码元数据装配组件,而非尖括号的声明。开发者通过在相应的类,方法或属性上使用注解的方式,直接组件类中进行配置,而不是使用xml表述bean的装配关系。38. 怎样开启注解装配?注解装配在默认情况下是不开启的,为了使用注解装配,我们必须在Spring配置文件中配置 <context:annotation-config/>元素。39. @Required 注解这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显式的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializationException。40. @Autowired 注解@Autowired 注解提供了更细粒度的控制,包括在何处以及如何完成自动装配。它的用法和@Required一样,修饰setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。41. @Qualifier 注解当有多个相同类型的bean却只有一个需要自动装配时,将@Qualifier 注解和@Autowire 注解结合使用以消除这种混淆,指定需要装配的确切的bean。Spring数据访问42.在Spring框架中如何更有效地使用JDBC?使用SpringJDBC 框架,资源管理和错误处理的代价都会被减轻。所以开发者只需写statements 和 queries从数据存取数据,JDBC也可以在Spring框架提供的模板类的帮助下更有效地被使用,这个模板叫JdbcTemplate (例子见这里here)43. JdbcTemplateJdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理。44. Spring对DAO的支持Spring对数据访问对象(DAO)的支持旨在简化它和数据访问技术如JDBC,Hibernate or JDO 结合使用。这使我们可以方便切换持久层。编码时也不用担心会捕获每种技术特有的异常。45. 使用Spring通过什么方式访问Hibernate?在Spring中有两种方式访问Hibernate:控制反转 Hibernate Template和 Callback。继承 HibernateDAOSupport提供一个AOP 拦截器。46. Spring支持的ORMSpring支持以下ORM:HibernateiBatisJPA (Java Persistence API)TopLinkJDO (Java Data Objects)47.如何通过HibernateDaoSupport将Spring和Hibernate结合起来?用Spring的 SessionFactory 调用 LocalSessionFactory。集成过程分三步:配置the Hibernate SessionFactory。继承HibernateDaoSupport实现一个DAO。在AOP支持的事务中装配。48. Spring支持的事务管理类型Spring支持两种类型的事务管理:编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。49. Spring框架的事务管理有哪些优点?它为不同的事务API 如 JTA,JDBC,Hibernate,JPA 和JDO,提供一个不变的编程模式。它为编程式事务管理提供了一套简单的API而不是一些复杂的事务API如它支持声明式事务管理。它和Spring各种数据访问抽象层很好得集成。50. 你更倾向用那种事务管理类型?大多数Spring框架的用户选择声明式事务管理,因为它对应用代码的影响最小,因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵活性。Spring面向切面编程(AOP)51. 解释AOP面向切面的编程,或AOP, 是一种编程技术,允许程序模块化横向切割关注点,或横切典型的责任划分,如日志和事务管理。52. Aspect 切面AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。53. 在Spring AOP 中,关注点和横切关注的区别是什么?关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。54. 连接点连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个应用程序执行Spring AOP的位置。55. 通知通知是个在方法执行前或执行后要做的动作,实际上是程序执行时要通过SpringAOP框架触发的代码段。Spring切面可以应用五种类型的通知:before:前置通知,在一个方法执行前被调用。after: 在方法执行之后调用的通知,无论方法执行是否成功。after-returning: 仅当方法成功完成后执行的通知。after-throwing: 在方法抛出异常退出时执行的通知。around: 在方法执行之前和之后调用的通知。56. 切点切入点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。57. 什么是引入?引入允许我们在已存在的类中增加新的方法和属性。58. 什么是目标对象?被一个或者多个切面所通知的对象。它通常是一个代理对象。也指被通知(advised)对象。59. 什么是代理?代理是通知目标对象后创建的对象。从客户端的角度看,代理对象和目标对象是一样的。60. 有几种不同类型的自动代理?BeanNameAutoProxyCreatorDefaultAdvisorAutoProxyCreatorMetadata autoproxying61. 什么是织入。什么是织入应用的不同点?织入是将切面和到其他应用类型或对象连接或创建一个被通知对象的过程。织入可以在编译时,加载时,或运行时完成。62. 解释基于XML Schema方式的切面实现。在这种情况下,切面由常规类以及基于XML的配置实现。63. 解释基于注解的切面实现在这种情况下(基于@AspectJ的实现),涉及到的切面声明的风格与带有java5标注的普通java类一致。Spring 的MVC64. 什么是Spring的MVC框架?Spring 配备构建Web 应用的全功能MVC框架。Spring可以很便捷地和其他MVC框架集成,如Struts,Spring 的MVC框架用控制反转把业务对象和控制逻辑清晰地隔离。它也允许以声明的方式把请求参数和业务对象绑定。65. DispatcherServletSpring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。66. WebApplicationContextWebApplicationContext 继承了ApplicationContext 并增加了一些WEB应用必备的特有功能,它不同于一般的ApplicationContext ,因为它能处理主题,并找到被关联的servlet。67. 什么是Spring MVC框架的控制器?控制器提供一个访问应用程序的行为,此行为通常通过服务接口实现。控制器解析用户输入并将其转换为一个由视图呈现给用户的模型。Spring用一个非常抽象的方式实现了一个控制层,允许用户创建多种用途的控制器。说到这里顺便给大家推荐一个Java架构方面的交流学习社群:650385180,里面不仅可以交流讨论,还有面试经验分享以及免费的资料下载,包括Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。相信对于已经工作和遇到技术瓶颈的码友,在这个群里会有你需要的内容。68. @Controller 注解该注解表明该类扮演控制器的角色,Spring不需要你继承任何其他控制器基类或引用Servlet API。69. @RequestMapping 注解该注解是用来映射一个URL到一个类或一个特定的方处理法上。by:老李

September 5, 2018 · 2 min · jiezi

Spring 事务管理

事务因Github自动化测试的原因,(最后找到的原因是getOneSavedDepartment时,这个Department没存上,所以ToMessage引用了一个未持久化的Department,就报错了),特此学习了一下事务。事务,基础概念就不说了。Spring为我们提供了对事务的支持,我们只需要很简单的注解或者XML配置即可实现。去网上找了好多篇关于Spring事务的博客,全是字,根本没有心情去看,更谈不上深入理解了,作者还在标题中自认为自己讲的比较好。如果你也不喜欢大段的文字,请继续向下看,我保证我画的图不会让你失望。org.springframework.transaction.annotation.Transactional@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface Transactional { @AliasFor(“transactionManager”) String value() default “”; @AliasFor(“value”) String transactionManager() default “”; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default -1; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {};}事务注解中有好多的属性,如果是简单场景的话,那我们只需要默认地配置就好了。但是如果应用发嵌套事务的复杂场景下,我们就需要研究研究这几个配置项了。这里我们深入学习一下事务的传播属性propagation。PropagationREQUIRED因为我们的事务传播级别就是REQUIRED,所以我们不配置propagation来测试REQUIRED。如果当前存在事务,则使用当前事务。如果不存在任何事务,则创建一个新的事务。内部方法开启默认REQUIRED级别的事务一个????,一个????,省略set、get方法。@Entitypublic class Cat { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name;}@Entitypublic class Dog { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name;}一个猫实现类,一个狗实现类,都有save方法,并且DogServiceImpl中还有一个保存并抛出异常的方法,用于测试回滚。省略依赖注入的代码。@Servicepublic class CatServiceImpl implements CatService { @Override @Transactional public void save(Cat cat) { catRepository.save(cat); }}@Servicepublic class DogServiceImpl implements DogService { @Override @Transactional public void save(Dog dog) { dogRepository.save(dog); } @Override @Transactional public void saveThrowException(Dog dog) { dogRepository.save(dog); throw new RuntimeException(); }}外部方法不开启事务public void test() { Cat cat = new Cat(); cat.setName(“Hello Kitty!”); catService.save(cat); Dog dog = new Dog(); dog.setName(“史努比”); dogService.save(dog);}没有任何异常抛出,猫存上了,狗也存上了。将保存狗的方法由save修改为saveThrowException,抛个异常,测试一下回滚。public void test() { Cat cat = new Cat(); cat.setName(“Hello Kitty!”); catService.save(cat); Dog dog = new Dog(); dog.setName(“史努比”); dogService.saveThrowException(dog);}猫存上了,狗没存上。因为同一事务的所有操作是同时成功,同时失败的。所以我们断定,猫和狗用的是两个事务。图解如果不存在任何事务,则创建一个新的事务。save猫和save狗都是默认的REQUIRED级别,test方法未开启事务,所以当前不存在任何事务。所以save猫创建了一个新的事务,save狗也创建了一个新的事务。外部方法开启事务为外部test方法添加事务。@Transactionalpublic void test() { Cat cat = new Cat(); cat.setName(“Hello Kitty!”); catService.save(cat); Dog dog = new Dog(); dog.setName(“史努比”); dogService.save(dog);}显然,未抛出异常,都存上了。方法修改为保存并抛出异常。@Transactionalpublic void test() { Cat cat = new Cat(); cat.setName(“Hello Kitty!”); catService.save(cat); Dog dog = new Dog(); dog.setName(“史努比”); dogService.saveThrowException(dog);}两者都没存上,所以断定save狗的方法抛出的异常对save猫是有影响的,猜测二者用的是同一个事务。如果当前存在事务,则使用当前事务。如果捕获抛出的RuntimeException,该方法仍然不能保存。Dog dog = new Dog();dog.setName(“史努比”);try { dogService.saveThrowException(dog);} catch (RuntimeException e) { System.out.println(“error”);}会抛出异常,该事务不能被提交。总结:REQUIRED修饰的内部方法会加入外部方法的事务中,其中有任一一个方法抛异常,事务都会回滚。SUPPORTSSUPPORTS与REQUIRED类似:如果当前存在事务,则使用当前事务。如果不存在任何事务,则不使用事务。MANDATORYMANDATORY与REQUIRED类似:如果当前存在事务,则使用当前事务。如果不存在任何事务,则抛出异常。REQUIRES_NEW如果当前存在事务,则挂起当前事务。创建一个新的事务。应该适合该操作相对独立的情况。就比如下单加支付,如果支付失败,但是这个订单应该还存在。如果将事务传播级别修改为这个的话,那save狗如果抛出异常就不影响save猫了。@Override@Transactionalpublic void test() { Cat cat = new Cat(); cat.setName(“Hello Kitty!”); catService.save(cat); Dog dog = new Dog(); dog.setName(“史努比”); dogService.saveThrowException(dog);}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void save(Cat cat) { catRepository.save(cat);}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void saveThrowException(Dog dog) { dogRepository.save(dog); throw new RuntimeException();}NOT_SUPPORTED如果当前存在事务,则挂起当前事务。同时以非事务方式运行。NEVER永远不要存在事务。如果当前存在事务,则抛出异常。NESTED如果当前存在事务,则在当前事务的一个嵌套事务中运行。该级别只对DataSourceTransactionManager事务管理器生效。本来想测试一下的,可惜。org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider’s capabilitiesNestedTransactionNotSupportedException,不支持嵌套事务。StackOverflow上的回答,Hibernate不支持嵌套事务。总结哥仨REQUIRED、SUPPORTS、MANDATORY,如果当前存在事务,则使用当前事务。哥仨REQUIRES_NEW、NOT_SUPPORTED、NEVER都不支持当前存在的事务。NESTED,嵌套事务,觉得这个应该是事务中最好用且最合理的,可惜Hibernate不支持。 ...

September 2, 2018 · 2 min · jiezi

Spring Cloud Finchley版中Consul多实例注册的问题处理

由于Spring Cloud对Etcd的支持一直没能从孵化器中出来,所以目前来说大多用户还在使用Eureka和Consul,之前又因为Eureka 2.0不在开源的消息,外加一些博眼球的标题党媒体使得Eureka的用户有所减少,所以,相信在选择Spring Cloud的用户群体中,应该有不少用户会选择Consul来做服务注册与发现。本文就来说一下,当我们使用Spring Cloud最新的Finchley版 + Consul 1.2.x时候最严重的一个坑:多实例注册的问题。问题解读问题:该问题可能在开发阶段不一定会发现,但是在线上部署多实例的时候,将会发现Consul中只有一个实例。原因:造成该问题的主要原因是Spring Cloud Consul在注册的时候实例名(InstanceId)采用了:“服务名-端口号”(即:{spring.application.name}-{server.port})的值,可以看到这个实例名如果不改变端口号的情况下,实例名都是相同的。如果熟悉Spring Cloud Consul的读者,可能会问老版本也是这个规则,怎么没有这个问题呢?。主要是由于Consul对实例唯一性的判断标准也有改变,在老版本的Consul中,对于实例名相同,但是服务地址不同,依然会认为是不同的实例。在Consul 1.2.x中,服务实例名成为了集群中的唯一标识,所以,也就导致了上述问题。解决方法既然知道了原因,那么我们要解决它就可以有的放矢了。下面就来介绍两个具体的解决方式:方法一:通过配置属性指定新的规则下面举个例子,通过spring.cloud.consul.discovery.instance-id参数直接来配置实例命名规则。这里比较粗暴的通过随机数来一起组织实例名。当然这样的组织方式并不好,因为随机数依然有冲突的可能,所以您还可以用更负责的规则来进行组织实例名。spring.cloud.consul.discovery.instance-id=${spring.application.name}-${random.int[10000,99999]}方法二:通过扩展ConsulServiceRegistry来重设实例名由于通过配置属性的方式对于定义实例名的能力有限,所以我们希望可以用更灵活的方式来定义。这时候我们就可以通过重写ConsulServiceRegistry的register方法来修改。比如下面的实现:public class MyConsulServiceRegistry extends ConsulServiceRegistry { public MyConsulServiceRegistry(ConsulClient client, ConsulDiscoveryProperties properties, TtlScheduler ttlScheduler, HeartbeatProperties heartbeatProperties) { super(client, properties, ttlScheduler, heartbeatProperties); } @Override public void register(ConsulRegistration reg) { reg.getService().setId(reg.getService().getName() + “-” + reg.getService().getAddress() + “-” + reg.getService().getPort()); super.register(reg); }}上面通过拼接“服务名”-“ip地址”-“端口号”的方式,构造了一个绝对唯一的实例名,这样就可以让每个服务实例都能正确的注册到Consul上了。

September 1, 2018 · 1 min · jiezi

简单理解:JVM为什么需要GC'

社区内有人发起了一个讨论,关于JVM是否一定需要GC?他们认为应用程序的回收目标是构建一个仅用来处理内存分配,而不执行任何真正的内存回收操作的 GC。即仅当可用的 Java 堆耗尽的时候,才进行顺序的 JVM 停顿操作。首先需要理解为什么需要GC。随着应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。社区的需求是尽量减少对应用程序的正常执行干扰,这也是业界目标。Oracle在JDK7时发布G1 GC的目的是为了减少应用程序停顿发生的可能性,让我们通过本文来了解G1 GC所做的工作。JVM发展历史简介还记得机器猫吗?他和康夫有一张书桌,书桌的抽屉其实是一个时空穿梭通道,让我们操作机器猫的时空机器,回到1998年。那年的12月8日,第二代Java平台的企业版J2EE正式对外发布。为了配合企业级应用落地,1999年4月27日,Java程序的舞台—Java HotSpot Virtual Machine(以下简称HotSpot )正式对外发布,并从这之后发布的JDK1.3版本开始,HotSpot成为Sun JDK的默认虚拟机。GC发展历史简介1999年随JDK1.3.1一起来的是串行方式的Serial GC ,它是第一款GC,并且这只是起点。此后,JDK1.4和J2SE1.3相继发布。2002年2月26日,J2SE1.4发布,Parallel GC 和Concurrent Mark Sweep (CMS)GC跟随JDK1.4.2一起发布,并且Parallel GC在JDK6之后成为HotSpot默认GC。HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?请记住以下口令:如果你想要最小化地使用内存和并行开销,请选Serial GC;如果你想要最大化应用程序的吞吐量,请选Parallel GC;如果你想要最小化GC的中断或停顿时间,请选CMS GC。那么问题来了,既然我们已经有了上面三个强大的GC,为什么还要发布Garbage First(G1)GC?原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。为什么名字叫做Garbage First(G1)呢?因为G1是一个并行回收器,它把堆内存分割为很多不相关的区间(Region),每个区间可以属于老年代或者年轻代,并且每个年龄代区间可以是物理上不连续的。老年代区间这个设计理念本身是为了服务于并行后台线程,这些线程的主要工作是寻找未被引用的对象。而这样就会产生一种现象,即某些区间的垃圾(未被引用对象)多于其他的区间。垃圾回收时实则都是需要停下应用程序的,不然就没有办法防治应用程序的干扰 ,然后G1 GC可以集中精力在垃圾最多的区间上,并且只会费一点点时间就可以清空这些区间里的垃圾,腾出完全空闲的区间。绕来绕去终于明白了,由于这种方式的侧重点在于处理垃圾最多的区间,所以我们给G1一个名字:垃圾优先(Garbage First)。G1 GC基本思想G1 GC是一个压缩收集器,它基于回收最大量的垃圾原理进行设计。G1 GC利用递增、并行、独占暂停这些属性,通过拷贝方式完成压缩目标。此外,它也借助并行、多阶段并行标记这些方式来帮助减少标记、重标记、清除暂停的停顿时间,让停顿时间最小化是它的设计目标之一。G1回收器是在JDK1.7中正式投入使用的全新的垃圾回收器,从长期目标来看,它是为了取代CMS 回收器。G1回收器拥有独特的垃圾回收策略,这和之前提到的回收器截然不同。从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区,但从堆的结构上看,它并不要求整个Eden区、年轻代或者老年代在物理上都是连续。综合来说,G1使用了全新的分区算法,其特点如下所示:并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力;并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况;分代GC:G1依然是一个分代收集器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。随着G1 GC的出现,GC从传统的连续堆内存布局设计,逐渐走向不连续内存块,这是通过引入Region概念实现,也就是说,由一堆不连续的Region组成了堆内存。其实也不能说是不连续的,只是它从传统的物理连续逐渐改变为逻辑上的连续,这是通过Region的动态分配方式实现的,我们可以把一个Region分配给Eden、Survivor、老年代、大对象区间、空闲区间等的任意一个,而不是固定它的作用,因为越是固定,越是呆板。G1 GC垃圾回收机制通过市场的力量,不断淘汰旧的行业,把有限的资源让给那些竞争力更强、利润率更高的企业。类似地,硅谷也在不断淘汰过时的人员,从全世界吸收新鲜血液。经过半个多世纪的发展,在硅谷地区便形成只有卓越才能生存的文化。本着这样的理念,GC承担了淘汰垃圾、保存优良资产的任务。G1 GC在回收暂停阶段会回收最大量的堆内区间(Region),这是它的设计目标,通过回收区间达到回收垃圾的目的。这里只有一个例外情况,这个例外发生在并行标记阶段的清除(Cleanup)步骤,如果G1 GC在清除步骤发现所有的区间都是由可回收垃圾组成的,那么它会立即回收这些区间,并且将这些区间插入到一个基于LinkedList实现的空闲区间队列里,以待后用。因此,释放这些区间并不需要等待下一个垃圾回收中断,它是实时执行的,即清除阶段起到了最后一道把控作用。这是G1 GC和之前的几代GC的一大差别。G1 GC的垃圾回收循环由三个主要类型组成:年轻代循环多步骤并行标记循环混合收集循环Full GC在年轻代回收期,G1 GC暂停应用程序线程,然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。G1的区间设计灵感为了加快GC的回收速度,HotSpot的历代GC都有自己的不同的设计方案,区间概念在软件设计、架构领域并不是一个新名词,关系型数据库、列式数据库最先使用这个概念提升数据存、取速度,软件架构设计时也广泛使用这样的分区概念加快数据交换、计算。为什么会有区间这个设计想法?大家一定看过电视剧《大宅门》吧?大宅门所描述的北京知名医术世家白家是这本电视剧的主角。白家有三兄弟,没有分家之前,由老爷子一手掌管全家,老爷子看似是个精明人,实质是个糊涂的人,否则也不会弄得后来白家家破人散。白家的三兄弟在没有分家之前,老大一家很老实,老二很懦弱,性格像女人,虽然肚子里明白道理,但是不敢出来做主。老三年轻时混蛋一个,每次出外采购药材都要私吞家里的银两,造成账目混乱。老大为了家庭和睦,一直在私下倒贴银两,让老爷子能够看到一本正常的账目。这样的一家子聚在一起,迟早家庭内部会出现问题,倒不如分家,你也不用算计家里的钱了,分给你,分给你的钱有本事守住,没本事就一直拮据下去吧。这就是最原始的分区(Region)概念。我们回到技术,看看HBase的RegionServer设计方式。在HBase内部,所有的用户数据以及元数据的请求,在经过Region的定位,最终会落在RegionServer上,并由RegionServer实现数据的读写操作。RegionServer是HBase集群运行在每个工作节点上的服务。它是整个HBase系统的关键所在,一方面它维护了Region的状态,提供了对于Region的管理和服务;另一方面,它与Master交互,上传Region的负载信息上传,参与Master的分布式协调管理。HRegionServer与HMaster以及Client之间采用RPC协议进行通信。HRegionServer向HMaster定期汇报节点的负载状况,包括RS内存使用状态、在线状态的Region等信息。在该过程中HRegionServer扮演了RPC客户端的角色,而HMaster扮演了RPC服务器端的角色。HRegionServer内置的RpcServer实现了数据更新、读取、删除的操作,以及Region涉及到Flush、Compaction、Open、Close、Load文件等功能性操作。Region是HBase数据存储和管理的基本单位。HBase使用RowKey将表水平切割成多个HRegion,从HMaster的角度,每个HRegion都纪录了它的StartKey和EndKey(第一个HRegion的StartKey为空,最后一个HRegion的EndKey为空),由于RowKey是排序的,因而Client可以通过HMaster快速的定位每个RowKey在哪个HRegion中。HRegion由HMaster分配到相应的HRegionServer中,然后由HRegionServer负责HRegion的启动和管理,和Client的通信,负责数据的读(使用HDFS)。每个HRegionServer可以同时管理1000个左右的HRegion。再来看看软件系统架构方面的分区设计。以任务调度为例,假设我们有一个中心调度服务,那么当数据量不断增多,这个中心调度服务一定会遇到性能瓶颈,因为所有的请求都会最终指向它。为了解决这个性能瓶颈,我们可以将任务调度拆分为多个服务,即这多个服务都可以处理任务调度工作,那么问题来了,每个任务调度服务处理的源数据是否需要完全一致?根据华为公司发布的专利发明,显示他们对于每一个任务调度服务有数据来源区分的操作,即按照任务调度数量对源数据进行划分,比如3个任务调度服务,那么源数据按照行号对3取余的方式划分,如果运行了一段时间之后,任务调度服务出现了数量上的增减,那么这个取余划分需要重新进行,要按照那个时候的任务调度数量重新划分区间。回到G1。在G1中,堆被平均分成若干个大小相等的区域(Region)。每个Region都有一个关联的Remembered Set(简称RS),RS的数据结构是Hash表,里面的数据是Card Table (堆中每512byte映射在card table 1byte)。简单的说RS里面存在的是Region中存活对象的指针。当Region中数据发生变化时,首先反映到Card Table中的一个或多个Card上,RS通过扫描内部的Card Table得知Region中内存使用情况和存活对象。在使用Region过程中,如果Region被填满了,分配内存的线程会重新选择一个新的Region,空闲Region被组织到一个基于链表的数据结构(LinkedList)里面,这样可以快速找到新的Region。总结没有GC机制的JVM是不能想象的,我们只能通过不断优化它的使用、不断调整自己的应用程序,避免出现大量垃圾,而不是一味认为GC造成了应用程序问题。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..

August 30, 2018 · 1 min · jiezi