Dubbo 自定义拦截器

写了 Spring AOP 实现自定义注解,打印日志之后,感觉在调用第三方 dubbo 接口的时候,依然会有同样的问题,然后看了一下 dubbo 的官方文档,决定下一个 filter,实现 dubbo 接口的日志拦截,以下是自己完的一个小例子,同样也是供需要的同学参考。 filter 具体实现如下: package cn.bridgeli.demo.filter; import com.alibaba.dubbo.rpc.Filter; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Result; import com.alibaba.dubbo.rpc.RpcException; import com.alibaba.dubbo.rpc.service.GenericService; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author bridgeli */ public class DubboServiceFilter implements Filter { private static final Logger LOGGER = LoggerFactory.getLogger(DubboServiceFilter.class); @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // 打印入参日志 String className = invocation.getInvoker().getInterface().getName(); String methodName = invocation.getMethodName(); String arguments = StringUtils.join(invocation.getArguments(), ";"); LOGGER.info("调用 dubbo 服务接口: " + className + "#" + methodName + ",参数:" + arguments); // 开始时间 long startTime = System.currentTimeMillis(); //执行接口调用逻辑 Result result = invoker.invoke(invocation); //调用耗时 long elapsed = System.currentTimeMillis() - startTime; //如果发生异常 则打印异常日志 if (result.hasException() && invoker.getInterface() != GenericService.class) { LOGGER.error("dubbo执行异常,接口:" + className + "#" + methodName + ",参数:" + arguments, result.getException()); } else { // 打印响应日志 LOGGER.info("dubbo服务响应成功:" + className + "#" + methodName + ",参数:" + arguments + ",返回值:" + result.getValue() + ",用时:" + elapsed); } //返回结果响应结果 return result; } } 在/src/main/resources/META-INF/dubbo目录下新增纯文本文件 com.alibaba.dubbo.rpc.Filter 内容为: dubboServiceFilter=cn.bridgeli.demo.filter.DubboServiceFilter 最后在服务提供者配置文件中添加配置使拦截器生效: <dubbo:provider filter="dubboServiceFilter"/> 或者 ...

March 22, 2020 · 1 分钟 · Bridge Li

Spring AOP 实现自定义注解

自工作后,除了一些小项目配置事务使用过 AOP,真正自己写 AOP 机会很少,另一方面在工作后还没有写过自定义注解,一直很好奇注解是怎么实现他想要的功能的,刚好做项目的时候,经常有人日志打得不够全,经常出现问题了,查日志的才发现忘记打了,所以趁此机会,搜了一些资料,用 AOP + 自定义注解,实现请求拦截,自定义打日志,玩一下这两个东西,以下是自己完的一个小例子,也供需要的同学参考。 注解如下: package cn.bridgeli.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author bridgeli */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog { /** * 方法描述 * * @return */ String desc() default ""; } 切面 package cn.bridgeli.demo.annotation; import cn.bridgeli.utils.AuthorizeUtil; import cn.bridgeli.entity.Principal; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * @author bridgeli * 1. 这是一个切面类 */ @Aspect @Component @Slf4j public class MyLogAspect { /** * 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名 * 切面最主要的就是切点,所有的故事都围绕切点发生 * logPointCut()代表切点名称 */ @Pointcut("@annotation(cn.bridgeli.demo.annotation.MyLog)") public void logPointCut() { } /** * 3. 环绕通知 * * @param joinPoint * @param myLog * @return */ @Around(value = "logPointCut() && @annotation(myLog)", argNames = "joinPoint,myLog") public Object logAround(ProceedingJoinPoint joinPoint, MyLog myLog) { // 获取方法名 String methodFullPathName = joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName(); // 获取参数 String params = StringUtils.join(joinPoint.getArgs(), ";"); Principal currentUser = AuthorizeUtil.getCurrentUser(); log.info("当前登陆用户:" + (null == currentUser ? "" : currentUser.toString()) + ",进入 [ " + methodFullPathName + " ] 方法, 方法的描述:" + myLog.desc() + ",参数为:" + params); // 继续执行方法 long startTime = System.currentTimeMillis(); Object result = null; try { result = joinPoint.proceed(); } catch (Throwable e) { log.error("切面执行报错,参数:" + params, e); } long elapsed = System.currentTimeMillis() - startTime; log.info("[ " + methodFullPathName + " ] 方法执行结束,返回值为:" + (null == result ? "" : result.toString()) + ",用时:" + elapsed); return result; } } 然后只需要在想使用的地方 @MyLog 就可以了,当然也可以加上 @MyLog(desc = “这是方法描述”),这样打出来的日志还会有方法是做什么的,别人看日志的时候能够一目了然。 ...

March 15, 2020 · 2 分钟 · Bridge Li

Java 使用 FFmpeg 处理视频文件示例

Java 使用 FFmpeg 处理视频文件示例 目前在公司做一个小东西,里面用到了 FFmpeg 简单处理音视频,感觉功能特别强大,在做之前我写了一个小例子,现在记录一下。 首先说明,我是在 https://ffmpeg.zeranoe.com/builds/ 这个地方下载的软件,Windows 和 Mac 解压之后即可使用。具体代码如下: package cn.bridgeli.demo; import org.junit.Test; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; /** * @author Bridge Li * @date 2020/2/29 15:40 */ public class FfmpegTest { private static final String OS = System.getProperty("os.name").toLowerCase(); private static final String FFMPEG_PATH = "/Users/bridgeli/ffmpeg-20200216-8578433-macos64-static/bin/ffmpeg"; @Test public void testFfmpeg() { String inputWavFile = "/Users/bridgeli/inputWavFile.wav"; String inputMp3File = "/Users/bridgeli/inputMp3File.mp3"; String inputMp4File = "/Users/bridgeli/inputMp4File.mp4"; String outMergeMp3File = "/Users/bridgeli/outMergeMp3File.mp3"; String outMergeMp3AndMp4File = "/Users/bridgeli/outMergeMp3AndMp4File.mp4"; String outConcatMp3File = "/Users/bridgeli/outConcatMp3File.mp3"; // 拼接 String command = null; if (OS.contains("mac") || OS.contains("linux")) { command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex \[0:0\]\[1:0\]concat=n=2:v=0:a=1[a] -map [a] " + outConcatMp3File; } else if (OS.contains("windows")) { command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex \"\[0:0\]\[1:0\]concat=n=2:v=0:a=1[a]\" -map \"[a]\" " + outConcatMp3File; } // 合并(视频和音频) // String command = FFMPEG_PATH + " -i " + inputMp4File + " -i " + outConcatMp3File + " -c:v copy -c:a aac -strict experimental " + outMergeMp3AndMp4File; // 合并 // String command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex amerge -ac 2 -c:a libmp3lame -q:a 4 " + outMergeMp3File; System.out.println(command); Process process = null; try { process = Runtime.getRuntime().exec(command); } catch (IOException e) { e.printStackTrace(); } if (null == process) { return; } try { process.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } try (InputStream errorStream = process.getErrorStream(); InputStreamReader inputStreamReader = new InputStreamReader(errorStream); BufferedReader br = new BufferedReader(inputStreamReader)) { String line = null; StringBuffer context = new StringBuffer(); while ((line = br.readLine()) != null) { context.append(line); } System.out.println("error message: " + context); } catch (IOException e) { e.printStackTrace(); } process.destroy(); } } 在我的认知中,完成任务是第一位的,所以按照这个简单处理一下音视频是没有问题的,具体更强大的语法,大家可以自己查询相关文档,也可以参考 https://blog.csdn.net/shshjj/article/details/98185454 这篇文中,其中我个人也在学习中。下面说两个在使用的过程中遇到的问题。 ...

February 29, 2020 · 2 分钟 · Bridge Li

日期中用 YYYY 一定会报错吗?

今年春节真是打破了 N 多传统,很多人都是在家连门都没出过,从今天开始也要开始在家远程办公了,因为和小伙伴合作开发一个功能,但是接口目前还没给到,然后记得今年元旦前后,关于 YYYY 报错的问题,突然火了,据说有 N 多程序员被火速召回公司改 bug,所以决定写篇小文章说说这个问题:YYYY 一定会报错吗? 首先需要说明的是:我用的 JDK 版本为:jdk-8u131-macosx-x64,所以具体表现为应该显示:2019-12-31,结果确是:2020-12-31,另外经过我的测试其实不仅 format 的时候报错,parse 的时候同样也会报错,我写了一段示例代码如下: package cn.bridgeli.demo; import org.joda.time.DateTime; import org.junit.Test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * Created by bridgeli on 2019/02/03. */ public class DateTest { @Test public void testSimpleDateFormat() throws ParseException { SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); DateTime dateTime1 = new DateTime(2019, 12, 31, 23, 59, 59); String date1 = simpleDateFormat1.format(dateTime1.toDate()); System.out.println("date1: " + date1); SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd"); DateTime dateTime2 = new DateTime(2020, 01, 01, 23, 59, 59); String date2 = simpleDateFormat2.format(dateTime2.toDate()); System.out.println("date2: " + date2); SimpleDateFormat simpleDateFormat3 = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); DateTime dateTime3 = new DateTime(2019, 12, 31, 23, 59, 59); String date3 = simpleDateFormat3.format(dateTime3.toDate()); System.out.println("date3: " + date3); SimpleDateFormat simpleDateFormat4 = new SimpleDateFormat("YYYY-MM-dd"); DateTime dateTime4 = new DateTime(2020, 01, 01, 23, 59, 59); String date4 = simpleDateFormat4.format(dateTime4.toDate()); System.out.println("date4: " + date4); SimpleDateFormat simpleDateFormat5 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date5 = simpleDateFormat5.parse("2019-12-31 23:59:59"); System.out.println("date5: " + date5); SimpleDateFormat simpleDateFormat6 = new SimpleDateFormat("yyyy-MM-dd"); Date date6 = simpleDateFormat6.parse("2020-01-01"); System.out.println("date6: " + date6); SimpleDateFormat simpleDateFormat7 = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); Date date7 = simpleDateFormat7.parse("2019-12-31 23:59:59"); System.out.println("date7: " + date7); SimpleDateFormat simpleDateFormat8 = new SimpleDateFormat("YYYY-MM-dd"); Date date8 = simpleDateFormat8.parse("2020-01-01"); System.out.println("date8: " + date8); } } 上面这段代码的输出结果为: ...

February 3, 2020 · 3 分钟 · Bridge Li

纠错:Java 内存模型(JMM)

当一个 Java 程序员工作一段时间之后,不可避免的要去了解 JVM,而了解 JVM 的时候,自然就会看到 Java 的内存模型,但是个人看过有太多的人概念不清不楚,有太多的人把 Java 内存结构,记得曾经看过一篇文章,一个同学去面试,面试官问他:简单聊聊 Java 的内存模型,他说完之后,面试官说:你说的不对啊,内存模型应该是堆、栈、常量池、方法区、程序计数器等等,然后他瞬间就不想去这家公司了,这不是让一个半吊子去当面试官吗,自己概念都没搞清楚就去说别人,个人也看过太多文章把这个搞错,所以今天就写了一篇小文章,说说自己对 JMM 的理解,当然我说的可能也不对,也不可能说透。 主内存和工作内存 Java 内存模型规定了所有的变量都存储在主内存,每个线程还有自己的工作内存,线程的工作内存中存储了被该线程使用到的变量的主内存存储拷贝,程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递均需要通过主内存来完成,主内存和工作内存之间的交互有 8 个人原子性的操作来实现,具体详细的可以再查资料。 重排序 重排序是编译器和处理器为了优化程序性能,而对指令顺序进行重新排序的一种手段。但 as-if-serial 语义的意思是:不管怎么重排序,单线程程序的执行结果都不能被改变,所以编译器和处理器都不会对存在数据依赖关系的操作做重排序。 happens-before 如果操作 1 happens-before 操作 2,那么第操作1的执行结果将对操作 2 可见,而且操作1的执行顺序排在第操作 2 之前。两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。另外 happens-before 具有传递性,由于这些规则的存在,Java 内存模型会保证重排序后的执行,在线程内看起来和串行的效果是一样的,这就是程序的顺序一致性。 对于 volatile 型变量的特殊规则 当一个变量被定义为 volatile 之后,他将具备两种特性,1. 保证此变量对所有线程的可见行,这里的可见行是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量做不到这一点。但是这个要多说一点,有些同学看到这可能认为:volatile 变量在各个线程中是一致的,所以基于 volatile 变量的运算在并发下是安全的,其实这句话的论据对,但结论是错的,因为 Java 中的运算并非原子的,volatile 的应用场景在于读多写少的地方:例如修饰一个 boolean 变量作为一个开关。2. 禁止指令重拍优化,这个在写单例的时候,我想大家都已经知道了,不再赘述。 final 被 final 修饰的字段,一旦完成了初始化,其他线程就能看到它,并且它也不会再变了。即只要不可变对象被正确的构建出来(没有发生 this 引用溢出),它就是线程安全的。

January 1, 2020 · 1 分钟 · Bridge Li

Apache Commons Codec — 加密与编码

明天就是十一假期了,公司也没多大事,刷知乎,看到有人吐槽曾经的一个合作伙伴连 md5 都写不对,告诉对方写错了,对方顾头不顾腚的修,还是没修对,然后测了一下自己写的想亏写对了,不然又遗留 bug 了,不过看下面评论,有人提到 Apache Commons Codec 里面都已经写好了,看了一下确实,反正也无聊,也写不了大文章,写着玩玩。 先看自己手写的 md5 // 今后大家不要这么写了,太傻了 package cn.bridgeli.demo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Created by bridgeli on 2019/7/12. */ public class EncryptUtils { private static Logger logger = LoggerFactory.getLogger(EncryptUtils.class); private EncryptUtils() { } public static String getMD5(String content) { if (null == content) { return ""; } MessageDigest messageDigest = null; try { messageDigest = MessageDigest.getInstance("md5"); } catch (NoSuchAlgorithmException e) { logger.error("md5 error", e); } if (null == messageDigest) { return ""; } messageDigest.update(content.getBytes()); byte[] bytes = messageDigest.digest(); StringBuilder stringBuilder = new StringBuilder(); for (byte b : bytes) { String str = Integer.toHexString(b & 0xFF); if (str.length() == 1) { stringBuilder.append("0"); } stringBuilder.append(str); } String result = stringBuilder.toString(); return result; } } 借助别人写好的工具类实现 先引入依赖 ...

September 30, 2019 · 2 分钟 · Bridge Li

关于 alibaba fastjson 的两个小知识点

json 转 JavaBean 大小写不敏感 在工作中,我个人经常使用的 json 的工具类是 Google 的 gson,前几天做一个需求的也自然而然的使用这个,但是在和其他部门联调的时候,发现他的属性全是小写,而不是刚开始约定的小驼峰,所以导致他传过来的字符串,我这边转不成 Java bean,在和他讨论的时候,他说他一直就这么写,而且别人使用的时候是没有问题的,然后就找到其他的使用的同学,他竟然说他没在意过这个问题,线上测试了一下,确实没有问题,然后就看一下怎么实现的,然后突然发现,他使用的是 alibaba 的 fastjson 转 Java bean,然后测试了一下,发现 alibaba fastjson 转 Java bean 竟然是大小写不敏感的,特此记录一下,测试代码入校: <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.37</version> </dependency> package cn.bridgeli.demo; import com.alibaba.fastjson.JSON; import org.junit.Test; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class FastJsonTest { @Test public void testFastJsonParseObject() { String jsonStr = "{\"USERNAME\":\"BridgeLi\"}"; User user = JSON.parseObject(jsonStr, User.class); System.out.println(user.getUsername()); } } class User implements Serializable { private static final long serialVersionUID = 4202834388700617773L; private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } } 我们看到测试的例子中 Java bean 的属性是小写的,json 字符串中的属性是全大写的,但转换一点问题都没有。 ...

August 25, 2019 · 1 分钟 · Bridge Li

关于 MySQL 和 MyBatis 易错的几个点

由于某些不可抗拒力原因,自从开博以来断更了一个月,昨天晚上突然发现竟然解封了,今天立即写一篇小文章感谢党感谢政府感谢人民。话说,这一周有一个实习的同学,在写一个小东西的时候,发现一个问题,排序没有生效,刚好之前我也看过另外一个问题,现在算是总结一下。 ORDER BY 不生效 代码大概就是: SELECT * FROM t ORDER BY "id DESC"; 我们其实可以很明显的看出来这个 SQL 有问题,ORDER BY 的后面多了引号,关键是这个 SQL 报错吗?如果 id 是随便写的一个不存在的列报错吗?答案是都不报错,排序也不生效,大家可以测试一些,这有时候就比较坑了,具体为什么会出现这个错误,后面再说,先说第二个。 分页问题 代码如下: /** * 开始页码 */ private String pageSize; /** * 分页量 */ private String offset; <if test="pageSize != null"> <if test="offset != null"> limit #{offset}, #{pageSize} </if> <if test="offset == null"> limit #{pageSize} </if> </if> 这个错误就比较明显了,一般人也不会犯,为什么要单独说一下呢?因为某同事说,分页用 # 会出错,但是我记得我一直用 # 没问题啊,刚好在遇到上面这个问题的时候,顺便测试了一些,还真有错,奇怪啊,其实原因很简单,就是分页的对象的属性有误,我们知道分页的属性,一般都是用 Integer,这个地方却写了 String,真是没事找事啊,一般谁会这么写?个人认为除非脑子有病。下面看第三个问题,也是一个比较诡异的问题。 UPDATE UPDATE t SET username = "BridgeLi" AND passwd = "BridgeLi" WHERE id = 1; 这个 SQL 有问题吗?会报错吗?这个问题有时候真不太看得出来,其实也很明显,SET 后面的各个列应该是 “,” 相连,而这个用的是 AND,有时候脑子一抽,还真有可能写错。但是会报错吗?这个问题还真不好答复,因为这个 SQL 这么写,相当于: ...

July 8, 2019 · 1 分钟 · Bridge Li

Redis GeoHash 的一个小示例

上周产品经理提了一个类似于 LBS 的应用,第一时间想到了忘记了之前什么时候看 Redis 的 API,发现 Redis 自 3.2 版本之后,新增了一类关于地理位置相关的 API,于是拿来测试一下,发现特别好用,写一个小例子作为笔记。 首先需要说明的是,由于我们公司的 JDK 的版本是 1.7,所以我采用的 spring-data-redis 的版本是:1.8.20.RELEASE,最新二点几的版本已经不支持 JDK 1.7,而一点几和二点几的版本的 API 有略微的差异(下面会说明,还有一点点我的小感悟),废话不多说,直接看例子: @Override public Long geoAdd(String key, List<Entity> entities) { redisTemplate.delete(key); GeoOperations geoOperations = redisTemplate.opsForGeo(); Map<String, Point> map = new HashMap<>(); Point point = null; for (Entity entity : entities) { point = new Point(entity.getLongitude(), entity.getLatitude()); map.put(gson.toJson(entity), point); } Long add = geoOperations.geoAdd(key, map); return add; } 参数就两个很简单,一个是 key,一个是数据集,我们将在这个集合中找出符合条件的数据,需要说明的是: 第一行我先调用了一个 delete 方法,是将上次放进去的数据删除,因为这个命令是 add,也就是新增,但是 redis 并没有提供直接删除这个 key 的命令(有一个 remove 的方法,但是需要传入删除哪些数据,也就是不能只给一个 key,把这个 key 对应的数据都删除,个人感觉不太好用),当然你也可以在计算后取得相应的数据之后删除,个人感觉都一样,不要忘记清理数据就行,另一个方法就是设置过期时间,也都行; 为什么要清理数据?因为这个 add,不同的情况,放进去的数据应该不同的,如果 entities 已经发生变更,而一直 add,那么数据将会乱掉,所以先把之前的数据删掉再说; 我个人采用的是把数据放到了 Map 中,其中 key 是对象序列化之后的 json 串,目的是为了下面找到对应的数据之后,直接反序列化成对象进行返回,当然也可以采用其他的方案,还有就是 add 还有一些其他的 API,这个大家可以自己看文档,选择合适的就行; 数据放进去之后就是计算了,我们的需求就是算一个人旁边几公里内有多少符合条件的数据,代码如下: ...

May 19, 2019 · 2 分钟 · Bridge Li

分享 Guava 的一些常见方法

前几天同事分享了一些关于 Guava 的一起基础用法,我之前没用过,感觉挺好的,所以记一些常见的方法。 一. 基础工具类,字符串相关的 其实这些在 apache commons-lang3,算是重复造轮子吧,简单说一下。 判断字符串是否为空,之前看到很多人自己定义,这些可能是一些老程序员吧,apache commons-lang3,Guava 的如下: boolean nullOrEmpty = Strings.isNullOrEmpty(""); 补全字符串(在前面补全和后面补全) String padStart0 = Strings.padStart("3", 2, 'a'); System.out.println("padStart0 = " + padStart0); String padStart1 = Strings.padStart("333", 2, 'a'); System.out.println("padStart1 = " + padStart1); 拆分和合并字符串 List<String> list = Splitter.on(",").splitToList("Denny,BridgeLi,CCC"); System.out.println("list = " + list); String join = Joiner.on(",").join(list); System.out.println("join = " + join); 对象相等判断和 ToStringHelper 类 boolean equal = Objects.equal("", ""); MoreObjects.toStringHelper(); 二. 集合类相关的,这些个人感觉还是非常常用的 不可变集合 @Test public void testJDK() { List<String> list = new ArrayList<>(); list.add("BridgeLi"); list.add("DennyLi"); List<String> unmodifiableList = Collections.unmodifiableList(list); System.out.println(unmodifiableList); unmodifiableList.add("Blog"); System.out.println(unmodifiableList); list.add("Blog"); System.out.println(unmodifiableList); } @Test public void testImmutableList() { List<String> list = new ArrayList<>(); list.add("BridgeLi"); list.add("DennyLi"); List<String> unmodifiableList = ImmutableList.copyOf(list); System.out.println(unmodifiableList); unmodifiableList.add("Blog"); System.out.println(unmodifiableList); list.add("Blog"); System.out.println(unmodifiableList); } 可重复集合,这个其实很常用可重复 set 可以用来基数,而可重复 Map 则可以实现 Map<K, List> 或者 Map<K, Set> 这样比较复杂的集合类型的数据结构 @Test public void testMultiset() { List<String> strings = Lists.newArrayList("aa", "bb", "aa", "cc"); Multiset<String> wordsMultiset = HashMultiset.create(); wordsMultiset.addAll(strings); for (String key : wordsMultiset.elementSet()) { System.out.println(key + " count:" + wordsMultiset.count(key)); } } @Test public void teststuScoreMultimap() { Multimap<String, StudentScore> scoreMultimap = ArrayListMultimap.create(); StudentScore studentScore = null; for (int i = 10; i < 20; i++) { studentScore = new StudentScore(); studentScore.courseId = 1001 + i; studentScore.score = 100 - i; scoreMultimap.put("peida", studentScore); } System.out.println("scoreMultimap:" + scoreMultimap.size()); System.out.println("scoreMultimap:" + scoreMultimap.keys()); Collection<StudentScore> studentScores = scoreMultimap.get("peida"); StudentScore studentScore1 = new StudentScore(); studentScore1.courseId = 1034; studentScore1.score = 67; studentScores.add(studentScore1); StudentScore studentScore2 = new StudentScore(); studentScore2.courseId = 1045; studentScore2.score = 56; scoreMultimap.put("jerry", studentScore2); System.out.println("scoreMultimap:" + scoreMultimap.size()); System.out.println("scoreMultimap:" + scoreMultimap.keys()); for (StudentScore stuScore : scoreMultimap.values()) { System.out.println("stuScore one:" + stuScore.courseId + " score:" + stuScore.score); } scoreMultimap.remove("jerry", studentScore2); System.out.println("scoreMultimap:" + scoreMultimap.size()); System.out.println("scoreMultimap:" + scoreMultimap.get("jerry")); scoreMultimap.put("harry", studentScore2); scoreMultimap.removeAll("harry"); System.out.println("scoreMultimap:" + scoreMultimap.size()); System.out.println("scoreMultimap:" + scoreMultimap.get("harry")); } class StudentScore{ int courseId; int score; } 双向 Map(BiMap),Map 是一种键值对映射,这个映射是键到值的映射,而 BiMap 既提供键到值的映射,也提供值到键的映射,所以它是双向 Map @Test public void testBimap() { BiMap<String, String> biMap = HashBiMap.create(); biMap.put("星期一", "Monday"); System.out.println("by key:" + biMap.get("星期一")); BiMap<String, String> biMap1 = biMap.inverse(); System.out.println("biMap1:" + biMap1.get("Monday")); } 双键 Map(Table),有些时候需要写 Map<String, Map<String, String» 这种格式的代码。但是这种阅读起来非常的不友好,Table 提供了新的思路:通过 rowKey + columnKey + value 来支持 @Test public void testTable() { Table<String, String, String> table = HashBasedTable.create(); table.put("IBM", "101", "A"); table.put("IBM", "102", "B"); table.put("IBM", "103", "C"); table.put("sun", "111", "D"); table.put("sun", "112", "E"); table.put("sun", "113", "F"); table.put("Google", "121", "G"); table.put("Google", "102", "H"); table.put("Google", "123", "L"); System.out.println(table.get("IBM", "102")); System.out.println(table.row("IBM")); System.out.println(table.column("102")); } 部分参考:https://www.cnblogs.com/peida/tag/Guava%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

April 30, 2019 · 2 分钟 · Bridge Li