在这里总结java研发过程中一些实用的工具/类/方法/特性,有时合理运用能够提升效率并且节省工作量,使得代码更丝滑更优雅。
MapStruct 是一个代码生成器(可以对比其他的一共12种如BeanUtils和Jmapper;地址zhuanlan.zhihu.com/p/460067093...[1] Java Bean 类型之间映射的实现。生成的映射代码使用纯方法调用,因此快速、类型安全且易于理解。 MapStruct 是一个 Java 注释处理器,适用于Java Bean 类型之间映射,多层应用程序通常需要在不同的对象模型(例如实体和 DTO)之间进行映射。与其他映射框架相比,MapStruct在编译时生成bean映射,这确保了高性能,允许快速的开发人员反馈和彻底的错误检查。遵循约定而不是配置方法,MapStruct使用合理的默认值,但在配置或实现特殊行为时会避开您的方式。当不同层级的类对象之间有公共的属性,需要进行两者间相互转换的时候,可以用mapstruct来构建两者的convert方法进行直接转换,不需要手动设置属性方法,减少工作量。
import org.mapstruct.factory.Mappers;
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
@DecoratedWith(IssueRecordConverterDecorator.class)
public interface IssueRecordConverter {
IssueRecordConverter INSTANCE = Mappers.getMapper(IssueRecordConverter.class);
@Mapping(target = "id", ignore = true)
IssueRecordPO convertToInsert(IssueRecord record);
}
至于最后生成的映射代码在哪里,其实是在target的生成文件包里面,为了方便查看,可以参考下面这个设置
操作过后,idea就能识别实现类,就可以点绿色快捷键进入实现类,方便检查mapstruct的结果 blog.csdn.net/Yzy9595/art...[2]
java8的新特性,Java中的Optional是一个容器对象,它可以包含一个非空值,也可以为空。它的主要作用是在编写代码时避免空指针异常。用了之后其实不需要后面加多余的 if(obj!=null) 的判断了
public List<ActivityConfig> queryActivityConfigList(Long areaId, ActivityTypeEnum activityType) {
List<ActivityConfigPO> configPOList = xxxConfig.getAreaAcvitityMap().get(areaId);
return Optional.ofNullable(configPOList)
.map(list -> list.stream()
.map(mapToConfig(areaId, activityType)
.collect(Collectors.toList()))
.orElse(new ArrayList<>());
}
java8的新特性,函数式接口,即只包含一个抽象方法的接口,也称为SAM接口,即Single Abstract Method interfaces,用@FunctionalInterface注解进行标注,常见的函数式接口如Comparator,Runnable,Callable。可以轻量级的将代码封装为数据,使代码简洁,易于理解。
通常在流式计算返回时候内部调用一下。
@NotNull
private static Function<ActivityConfigPO, ActivityConfig> mapToConfig(Long areaId, ActivityTypeEnum tag) {
return configPO -> ActivityConfig.builder()
.id(configPO.getId())
.areaId(areaId)
.desc(configPO.getDesc())
.activityType(tag)
.activityId(configPO.getActivityId())
.build();
}
java8新特性,简单返回对象映射转换的时候可以使用。上面示例返回map(list -> list.stream().map(mapToConfig(areaId, activityType).collect(Collectors.toList())));用到了
public static ActivityConfigPO findItem(List<ActivityConfigPO> list, Long Id) {
return list.stream().filter(item -> item.getId().equals(Id)).findFirst().orElse(null);
}
又或者java的基本数据和引用数据转换,将int[]数组转换成Integer[]数组: ArrayList.asList List.toArray Stream()->函数 boxed()拆装箱
import java.util.Arrays;
import java.util.stream.IntStream;
public class ArrayConvertExample {
public static void main(String[] args) {
Integer[] obj = new Integer[] {null, 1, 2, 3};
int[] newObj = Arrays.stream(obj).mapToInt(i -> i == null ? 0 : i.intValue()).toArray();
int[] obj2 = new int[] {4, 5, 6};
Integer[] newObj2 = Arrays.stream(obj2).boxed().toArray(Integer[]::new);
Integer[] newObj3 = IntStream.of(obj2).boxed().toArray(Integer[]::new);
}
}
注意下Stream.map()、Stream.filter()功能区别 map--映射,filter--匹配过滤 filter()方法用于根据条件过滤流中的元素; map()方法用于对流中的每个元素执行转换操作。 根据具体的需求,选择使用filter()方法还是map()方法。如果你想筛选满足特定条件的元素,使用filter();如果你想对元素进行转换,使用map()。 1.forEach----主要用于改变当前数组里面的值,forEach方法是Stream中的一个方法,它可以让我们对Stream中的元素进行遍历。forEach方法接收一个Consumer类型的参数,这个Consumer类型的参数其实就是一个函数式接口,它的参数为Stream中的元素,没有返回值。 2.filter----主要用于过滤当前数组,找出符合条件的元素,返回一个新的数组,不会改变原数组。filter()方法用于根据指定的条件筛选流中的元素,只保留符合条件的元素。它接受一个Predicate函数式接口作为参数,该接口定义了用于判断元素是否满足条件的方法。 3.map----则可以改变当前循环的值,返回一个新的被改变过值之后的数组,一般用来处理需要修改某一个数组的值。map()方法用于对流中的每个元素执行一个映射操作,将每个元素根据指定的映射规则转换为另一种类型。它接受一个Function函数式接口作为参数,该接口定义了将元素转换为另一种类型的方法。 参考博客blog.csdn.net/Whx123A/art...[3]
以往创建对象和属性Set赋值是分离的,或者通过构造器方式需要有对应的构造函数,整个过程不够直观和连贯。 因此我们可以通过@lombok.Builder注解,进行建造者模式的构建,能够清晰理解对象创建和属性注入需要的内容和具体构建过程。
@Data
@lombok.Builder
public class ActivityEntity {
}
private xxx builderActivity(ActivityRequest request) {
return ActivityEntity.builder()
.id(request.getActivityId())
.name(request.getName())
.status(request.getStatus())
.startTime(request.getStartTime())
.endTime(request.getEndTime())
.description(request.getDescription())
.build();
}
以前我们写胶水代码时候喜欢通过类似str1+str2的方式拼串,但是这种拼起来的结构看起来比较混乱,阅读起来不直观,容易出错。现在我们可以用String.format划分固定和可变部分进行格式化替换输出:
示例:客户端展示输出"满x元减y元优惠券"
String str = "满"+x+"元减"+y+"元优惠券";
private static final String COUPON_DESC = "满%s元减%s元优惠券";
String realCoupon = String.format(COUPON_DESC, x, y);
或者当我们想进行指定类型输出的时候,如汇率与金额的浮点运算结果需要保留特定有效位数:
log.fine((String.format("ExRate %.2f, 金额$%.2f)", x, y));
以前我们打印日志喜欢用的输出可能是这些:
System.out.println("xxxxxx");
System.out.printf("xxxxxx");
try{
}catch(exception e){
e.printStackTrace("xxxxxx");
}finally{
}
但是System.out和printStackTrace都是存在线程死锁风险的: 参考www.kdun.cn/ask/9230.ht...[4] System.out属于输出流,多线程争夺共享资源时候可能会发生阻塞导致死锁; printStackTrace占用系统资源,多线程争夺共享资源时候可能会死锁。并且可能会不断向上请求栈,直到异常被JVM捕获,如果一直没处理则也可能会导致死锁。 早期的java自带的log4j也可能死锁,还是推荐使用@slf4j,可以避免死锁问题。 业务常用的方式是@Slf4j带的日志,不会有上面的风险,先定义一个Logger,然后支持不同级别的输出如fine()/info()/error()等。
private static final Logger LOGGER = LoggerFactory.getLogger(xxx.class);
try {
} catch (Throwable e) {
LOGGER.error("创建订单异常:buildOrderEntity={}, buildOrderEntity, e);
throw new BizException(OrderErrorCode.ORDER_CREATE_ERROR);
}
据说是速度最快的JSON序列化工具(可以对比一些其他的比如Gson、java自带的Jackson等;地址blog.csdn.net/qinxun20080...[5]
Object obj = new Obj();
String jsonString = JSON.toJSONString(obj);
Object obj = JSON.parseObject(jsonString);
JSONobject jsonObj = JSON.toJSON(obj);
List<Object> objList = new ArrayList<>();
String jsonListString = JSON.toJSONString(objList);
List<Object> objList = JSON.parseArray(jsonListString);
if(content instanceof String) return content;
else return JSON.toJSONString(content);
但是要注意有一些坑,比如JSON.toJSONString里面传入String参数会报错,输入的时候先校验一下,if(content instanceof String) 就直接传输String类型的content就行。并且要区分是转成对象用parseObject,转成列表要用parseArray。
通常在Facade层或者Service入参调用真正的内部方法之前先加上一层参数校验,以防止对真正的业务逻辑造成注入干扰。Validator可以手动定义,也可以基于javax.validation包的默认方法校验,或者公司内部的FastValidator工具。
使用示例,调用Service注册方法之前对用户上传信息进行前置校验:
@Autowired
private UserInfoValidator userInfoValidator ;
@Override
public Boolean validUserInfo(Request request) {
return userInfoValidator.validUserInfo(request);
}
Boolean validUserInfo(){return xxxx;}
xxx validate = validator.validate(request);
对返回的结果进行包装,这个Result泛型类是可以自己定义的,把核心返回值作为泛型类Result的一个子属性,并且可以返回一些其他扩展属性比如自定义的model数据、响应码code和message等,方便定位分析排查执行信息和返回状态。
@Override
public Result<xxxxResponse> xxxQuery (Request request) {
return ResultWrapper.wrapper(() -> {
XxxxResponse xxxxResponse = new xxxxResponse();
response.setXXXX(xxxx);
response.setXXXX(xxxx);
response.setXXXX(xxxx);
return xxxxResponse;
});
}
可以理解成一种装饰器模式,通过wrapper方法对返回结果进行包装,支持传入一个函数式接口作为参数,并且返回结果可以用Result< xxxxResponse>去接收。
@Slf4j
public class ResultWrapper {
public static <T> Result<T> wrapper(Supplier<T> supplier) {
}
@Override
public Result<xxxxResponse> xxxQuery (Request request) {
return ResultWrapper.wrapper(() -> {
XxxxResponse xxxxResponse = new xxxxResponse();
response.setXXXX(xxxx);
response.setXXXX(xxxx);
response.setXXXX(xxxx);
return xxxxResponse;
});
}
很多场景Spring提供的BeanUtils性能不如MapStruct,但是还是介绍一下用法。
@ApiModelProperty(value = "Function_call")
private Function_call function_call;
BeanUtils.copyProperties(functionDTO.getProperties(), properties);
虽然Assembler/Converter是非常好用的对象,但是当业务复杂时,手写Assembler/Converter是一件耗时且容易出bug的事情,所以业界会有多种Bean Mapping的解决方案,从本质上分为动态和静态映射。 动态映射方案包括比较原始的 BeanUtils.copyProperties、能通过xml配置的Dozer等,其核心是在运行时根据反射动态赋值。动态方案的缺陷在于大量的反射调用,性能比较差,内存占用多,不适合特别高并发的应用场景。 所以在这里我给用Java的同学推荐一个库叫MapStruct(MapStruct官网)。MapStruct通过注解,在编译时静态生成映射代码,其最终编译出来的代码和手写的代码在性能上完全一致,且有强大的注解等能力。如果你的IDE支持,甚至可以在编译后看到编译出来的映射代码,用来做check。在这里我就不细讲MapStruct的用法了,具体细节请见官网。 用了MapStruct之后,会节省大量的成本,让代码变得简洁。 参考博客 blog.csdn.net/Taobaojishu...[6]
JDK1.5以后java提供了自动拆装箱的机制,但是有时候比如Stream计算时候,还是可能需要手动代码实现拆装箱的。比如Java中的IntStream boxed()方法,可以将int基本类型装箱成Integer。
import java.util.*;
import java.util.stream.Stream;
import java.util.stream.IntStream;
public class Demo {
public static void main(String[] args) {
IntStream intStream = IntStream.range(20, 30);
Stream<Integer> s = intStream.boxed();
s.forEach(System.out::println);
}
}
常用的场景是 Java Stream中List、Integer[]、int[] 的相互转换。参考博客 blog.csdn.net/studyday1/a...[7]
Integer[] vinteger = Arrays.stream(vint).boxed().toArray(Integer[]::new);
int[] vint = Arrays.stream(vinteger).mapToInt(Integer::valueOf).toArray();
还有一件事,List.toArray() Arrays.asList()方法底层可能也有坑的。 参考一下 blog.csdn.net/h4241778/ar...[8] 比如ArrayList.toArray() 传入String会报出类转换异常, 并且Arrays.asList()转换以后得list的size是不能更改的。
通常会用到可以选择的类型的时候,如果直接用String类型进行基于map映射后转到对应执行,有时候会有误操作增删导致字符串更改造成不匹配的风险,排查起来困难,并且把String定义成常量的实际意义不如定义成枚举更合理。
public enum AppEnum {
IOS("iOS"),
ANDROID("android");
@Getter
final private String platform;
AppEnum(String platform) {
this.platform = platform;
}
public static AppEnum of(String platform) {
return Arrays.stream(values())
.filter(item -> item.getPlatform().equalsIgnoreCase(platform))
.findFirst()
.orElse(null);
}
}
if (AppEnum.ANDROID.getPlatform().equals(type)) {
} else if (AppEnum.IOS.getPlatform().equals(type) {
}
但是注意枚举类在序列化时候可能会出现一些问题,Go语言就不支持枚举,转化之前可以先变成String,枚举类在业务类内部用一下还是比较方便的。
可以将Lambda表达式理解为一个匿名函数; Lambda表达式允许将一个函数作为另外一个函数的参数; 我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码作为实参),也可以理解为函数式编程,将一个函数作为参数进行传递。Lambda表达式能够让程序员的编程更加高效。 lambda表达式和方法引用使用前提:函数式接口 1.@FunctionalInterface 语法格式严格要求当前接口有且只能有一个尚未完成的缺省属性为 public abstract 修饰方法。 2.函数式接口一般用于方法的增强,直接作为方法的参数,实现函数式编程。 只有函数式接口的变量或者是函数式接口,才能够赋值为Lambda表达式。这个接口中,可以有默认方法,或者是静态方法。
@Override
public ActivityConfig queryActivityConfig(Long areaId, Long configId) {
return Optional.of(xxxConfig.getActivityConfig().getAreaAcvitityMap().get(areaId))
.map(list -> findItem(list, configId))
.map(mapToConfig(areaId, ActivityTypeEnum.XXX))
.orElse(null);
}
@NotNull
private static Function<ActivityConfigPO, ActivityConfig> mapToConfig(Long areaId, ActivityTypeEnum tag) {
return configPO -> ActivityConfig.builder()
.id(configPO.getId())
.areaId(areaId)
.desc(configPO.getDesc())
.activityType(tag)
.activityId(configPO.getActivityId())
.build();
}
其他一些场景,比如实现手动定制排序的时候,可以不同new Comparator()接口了,我们可以直接用lambda表达式,代码量会降低很多。
import java.util.*;
class Solution {
public String sort(Integer[] nums) {
Arrays.sort(nums, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
return Arrays.toString(nums);
}
}
public class Main {
public static void main(String[] args) {
Solution solution = new Solution();
Integer[] arr = {1, 2, 3, 4, 5};
System.out.println(solution.sort(arr));
}
}
其他内容参考博客 blog.csdn.net/qq_45944185...[9]
git发布相关,研发血泪经验总结 1.每次注意修改的是不是Feature分支 2.合并之前先git pull一下最新代码 3.合并master之前,feature分支先本地拉取master合并一下,预先更新包引用并解决冲突 4.按照紧急程度选择新功能放在一个分支还是两个分支,前后端发的时候商量好,先发后端 5.测试和预发公共环境合并commit以后先测一下,跑通了再push和发布,别污染公共环境。
最近项目也有一个执行脚本读取接口数据生成Excel的任务,于是首先想到了阿里的EasyExcel,对它的了解始于尚硅谷宋**的那个医院在线挂号项目。 用的比较多的功能,主要是文件的读取解析和数据写入文件。 参考blog.csdn.net/weixin_6850...[10] blog.csdn.net/wyr1235/art...[11] blog.csdn.net/weixin_7307...[12]
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import org.apache.poi.ss.formula.functions.T;
import java.util.LinkedList;
import java.util.List;
* @author wzx
*/
public class ExcelUtil {
* 封装Excel中的数据到指定的实体类中
* @param typeClass 指定的实体类的字节码类别
* @param readPath Excel的文件路径
* @return 指定的实体类对象的集合(每个对象代表每一条数据)
*/
public static List<T> getDataFromExcel(Class<T> typeClass , String readPath){
List<T> list = new LinkedList<>();
EasyExcel.read(readPath)
.head(typeClass)
.sheet()
.registerReadListener(new AnalysisEventListener<T>() {
@Override
public void invoke(T excelData, AnalysisContext analysisContext) {
list.add(excelData);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("数据读取完毕");
}
}).doRead();
return list;
}
* 将封装好的数据写入Excel中
* @param list 写入的数据集合
* @param writePath 写入的Excel文件的路径
* @param sheet excel表中生成的sheet表名
* @param excelType 插入的excel的类别,有xls、xlsx两种
*/
public static <T> void saveDataToExcel(List<T> list, String writePath, String sheet, ExcelTypeEnum excelType, Class<T> clazz) {
EasyExcel.write(writePath)
.head(clazz)
.excelType(excelType)
.sheet(sheet)
.doWrite(list);
}
}
有已经帮忙封装好工具类的,写文件比较简单,配置好对应的属性名称映射就行,文件会在指定的路径生成,读可能分成从磁盘路径读和从网络接收读取并解析。 读的方式可以分成同步读和监听器读取。 blog.csdn.net/m0_59084856...[13] 另外补充一些东西,ivoke返回LinkedHashMap类型的数据,如果我们想要获取表格头的属性,可以通过invokeHeadMap方法来获取。还有就是Controller层的文件的传输,参数类型是@RequestParam("file") MultipartFile file,然后通过InputStream inputStream = file.getInputStream()---调用 readExcel(inputStream,"xxxdatatype");进行输入流的读取。
private List<HashMap<String, String>> readExcel(InputStream inputStream) throws IOException {
HashMap<String,String> header = new HashMap<>();
List<HashMap<String, String>> data = new ArrayList<>();
EasyExcel.read(inputStream, new AnalysisEventListener<Object>() {
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context){
Integer currentRowNum = context.getCurrentRowNum();
for (Map.Entry<Integer, String> entry : headMap.entrySet()) {
header.put(String.valueOf(entry.getKey()),entry.getValue());
}
LOGGER.info("Parsed header: {}", header);
}
@Override
public void invoke(Object object, AnalysisContext context) {
Integer currentRowNum = context.getCurrentRowNum();
HashMap<String, String> row = new HashMap<>();
LinkedHashMap<Integer, String> dataMap = (LinkedHashMap<Integer, String>) object;
int index = 0;
for (Map.Entry<Integer, String> entry : dataMap.entrySet()) {
row.put(header.get(String.valueOf(index++)),entry.getValue());
if(index==header.keySet().size()) index = 0;
}
data.add(row);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}).sheet().doRead();
data.add(0, header);
return data;
}
另外注意文件上传的一些问题,网络请求的type类型是multipart/form-data,经过F12查看网络传输的内容可以看到文件传输的类型是二进制文件binary类型。
帮助你做分页数据处理的,比如chat对话记录的批量写入和批量读取,参考文章介绍: 官网github.com/pagehelper/...[14] blog.csdn.net/qq_25498677...[15] 可以顺便下一个maybatis-x的插件,红色和蓝色小鸟点击跳转切换着看挺方便的。
@Resource
private xxxMsgMapper xxxMsgMapper;
PageInfo<xxxMsg> pageInfo = PageHelper.startPage(1, limit, false).doSelectPageInfo(() -> xxxMsgMapper.selectByExample(example));
List<xxxMsg> xxxMsgs = pageInfo.getList();
这个上手起来感觉挺高大上的,线上排查很方便。线上机器部署多个集群的时候,流量大的时候一定能打到某个机器,访问量不多的时候,比如预发就两台机器pods,从webshell进入,这个时候请求负载均衡不确定路由到哪一台,可以两边都开着,然后监听访问,区分读还是写的请求,如果是读接口,可以多点几下,肯定有一些会路由到的。
操作步骤--- (1)机器的webShell进入终端,可能会要求VPN的6位动态口令单点登录(阿里郎或者飞连) (2)使用下面命令安装并启动进入Arthas 下载arthas-boot.jar,然后用java -jar的方式启动
curl -O https:
java -jar arthas-boot.jar
(3)选择进程 启动后首先选择attach的进程,Arthas会attach到目标进程上
(4)然后就可以用命令的形式,使用Arthas的功能 命令列表 arthas.aliyun.com/doc/command...[16] 常用的命令 dashboard-----当前系统的实时数据面板 sc----查看 JVM 已加载的类信息 trace----链路排查 watch---查看出入参 heapdump - dump java heap, 类似 jmap 命令的 heap dump 功能 jvm - 查看当前 JVM 的信息 stack - 输出当前方法被调用的调用路径 monitor - 方法执行监控,比如性能耗时等 thread----- 查看当前 JVM 的线程堆栈信息 还支持一些基础linux命令 推出就用quit/exit关闭当前会话 或者 直接stop完全退出
注意格式 有的命令需要类的全路径名称
sc com.xxx.generate*
trace com.xxx.GenerateImpl generatexxxData
watch com.xxx.GenerateImplxxxData -x 4
官方的手册 arthas.aliyun.com/doc/quick-s...[17] 一些额外的链接 blog.csdn.net/beiduofen20...[18] blog.51cto.com/u_12218/848...[19]
功能调试相关,MockMVC、PostMan、ApiPost、HTTP请求Client、okhttp-utils等 据说Apipost = Postman + Swagger + Mock + JMeter JSON查看工具 www.json.cn/[20] 可以看到内部有多少个对象
plantUML语雀里面操作起来很方便,语雀画个时序图 blog.csdn.net/asd13583550...[21]
内存泄漏排查 MAT分析 JProfile等 zhuanlan.zhihu.com/p/535069800...[22] 项目线上的GC频次和CPU、内存等的占用监控情况 可以看深堆区和浅堆区的比例 大对象的占用排序 线程调用stack 找到具体代码行数
forEach 增强for循环 blog.csdn.net/weixin_4599...[23] out 对比break/continue 终止多轮循环 clone方法 深/浅拷贝 @Value("${xxx.xxxxx}") private String xxxxx; 读取prop配置文件里面的内容进行动态赋值 分页查询 子查询 深分页问题 ORM框架 MybatisPlus 或者 基于JPA的Script标签形式
String字符串长度超过javac编译器运行的类型,可以更改类型选择Eclipse编译器。
在Java中,String类型的长度取决于系统内存的限制,因此在理论上没有固定的长度限制。 在Java中,String类型是使用Unicode字符集来存储字符串的,每个字符使用16位表示,因此单个字符串对象的最大长度是65535个字符(即2的16次方减1)。然而,这只是一个理论上的限制,因为实际上在Java中创建String类型的对象时,受限于内存大小和可用的虚拟机堆空间大小。 因此,String类型的长度在Java中受系统内存的限制,而不是固定的字符数限制。 参考 blog.csdn.net/weixin_4257...[24] www.jb51.net/article/273...[25]
在这里总结java研发过程中一些实用的工具/类/方法/特性,有时能提升效率并且节省工作量,使得代码更丝滑更优雅,用的时候回来查一下,结合业务实践真正理解它们的精髓和方便之处,更好地为实际工作服务。