超级实用开发技巧
最近经常逛github,看到里面一些好的项目和经验,最近看到了一个leader写的内容,对我很有启发,这半年看了不少源码,但是在实际开发中却很好使用起来,总结来说,总是想把人家的开发方式往我自己的业务开发上面套,只学到了皮毛,却没有理解别人为啥要这么做,导致实际场景中不能使用(因为自己的场景实在简单,只是些CURD),但是这篇作者的文章却给了我启发。原作者文章链接如下:(https://xwjie.github.io/rule/util.html#%E7%89%A9%E7%90%86%E4%B8%8A%E7%8B%AC%E7%AB%8B%E5%AD%98%E6%94%BE)
1.编码风格比技术更加重要
简单的删除controller如下:
@PostMapping("/delete")
public Map<String, Object> delete(long id, String lang) {
Map<String, Object> data = new HashMap<String, Object>();
boolean result = false;
try {
// 语言(中英文提示不同)
Locale local = "zh".equalsIgnoreCase(lang) ? Locale.CHINESE : Locale.ENGLISH;
result = configService.delete(id, local);
data.put("code", 0);
} catch (CheckException e) {
// 参数等校验出错,这类异常属于已知异常,不需要打印堆栈,返回码为-1
data.put("code", -1);
data.put("msg", e.getMessage());
} catch (Exception e) {
// 其他未知异常,需要打印堆栈分析用,返回码为99
log.error(e);
data.put("code", 99);
data.put("msg", e.toString());
}
data.put("result", result);
return data;
}
可以看到上面的代码写了很多和业务逻辑无关的内容,如返回值时Map,兼容了正常和错误的情况,处理了语言问题。如语言和异常处理的封装,完全可以抽出公共模块来做
大佬改成如下:
@PostMapping("/delete")
public ResultBean<Boolean> delete(long id) {
return new ResultBean<Boolean>(configService.delete(id));
}
很简单吧,用到的技术是AOP,也不高深,但是人家就能想到。技术无所谓高低,看你怎么用。
2.工具类编写
-
使用父类接口
举个例子,假设我们需要判断一个arrayList是否为空的函数,一开始是这样的。
public static boolean isEmpty(ArrayList<?> list) {
return list == null || list.size() == 0;
}
这个时候需要考虑一下,参数的类型能不能使用父类,我们看到只使用了size方法,我们可以知道size方法在list接口上有,于是我们修改成这样。
public static boolean isEmpty(List<?> list) {
return list == null || list.size() == 0;
}
后面发现,size方法在list的父类/接口Collection上也有,那么我们可以修改为最终这样
public static boolean isEmpty(Collection<?> list) {
return list == null || list.size() == 0;
}
到了这步,Collection没有父类/接口有size方法了,修改就可以结束了。最后我们需要把参数名字改一下,不要再使用list。改完后,所有实现了Collection的对象都可使用,最终版本如下:
public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.size() == 0;
} -
使用重载编写衍生接口
举个例子:现在需要编写一个方法,输入是一个utf-8格式的文件的文件名,把里面内容输出到一个list《 String》。我们刚刚开始编写的时候,是这个样子的
public static List readFile2List(String filename) throws IOException {
List list = new ArrayList();File file = new File(filename); FileInputStream fileInputStream = new FileInputStream(file); BufferedReader br = new BufferedReader(new InputStreamReader(fileInputStream, "UTF-8")); // XXX操作 return list; }
很显然,编码格式有可能被修改,可传的参数有File,FileInputStream等类型,所以我们需要都支持
package plm.common.utils;import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import org.apache.commons.io.IOUtils; /** * 工具类编写范例,使用重载编写不同参数类型的函数组 * * @author 晓风轻 https://github.com/xwjie/PLMCodeTemplate * */ public class FileUtil { private static final String DEFAULT_CHARSET = "UTF-8"; public static List<String> readFile2List(String filename) throws IOException { return readFile2List(filename, DEFAULT_CHARSET); } public static List<String> readFile2List(String filename, String charset) throws IOException { FileInputStream fileInputStream = new FileInputStream(filename); return readFile2List(fileInputStream, charset); } public static List<String> readFile2List(File file) throws IOException { return readFile2List(file, DEFAULT_CHARSET); } public static List<String> readFile2List(File file, String charset) throws IOException { FileInputStream fileInputStream = new FileInputStream(file); return readFile2List(fileInputStream, charset); } public static List<String> readFile2List(InputStream fileInputStream) throws IOException { return readFile2List(fileInputStream, DEFAULT_CHARSET); } public static List<String> readFile2List(InputStream inputStream, String charset) throws IOException { List<String> list = new ArrayList<String>(); BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(inputStream, charset)); String s = null; while ((s = br.readLine()) != null) { list.add(s); } } finally { IOUtils.closeQuietly(br); } return list; } }
多想一步,根据参数变化编写各种类型的入参函数,需要保证函数主要代码只有一份。
3.抽象接口定义
最开始我们写的代码如下
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
上面的代码很好的简化和优化了代码,但是他的语义是有问题的,我们需要体现一个转化过程
public interface DTOConvert<S,T> {
T convert(S s);
}
public class UserInputDTO {
private String username;
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User convertToUser(){
UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
User convert = userInputDTOConvert.convert(this);
return convert;
}
private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
}
调用方法如下:
User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);
比之前简洁了不少,再查工具类后,GUAUA实现了我们自定义的DTOConver接口
public abstract class Converter<A, B> implements Function<A, B> {
protected abstract B doForward(A a);
protected abstract A doBackward(B b);
//其他略
}
他可以支持正向转化和逆向转化,继续修改我们的转换代码
public class UserDTO {
private String username;
private int age;
//setget方法省略
public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convert(this);
return convert;
}
public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.reverse().convert(user);
return convert;
}
private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user) {
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
return userDTO;
}
}
}
最终调用方式如下:
@PostMapping
public UserDTO addUser(UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
4.巧用lombok
-
bean中的链式风格
@Accessors(chain = true)
@Setter
@Getter
public class Student {
private String name;
private int age;
}
调用方式(但是仅限在new的时候可以用,如果给实例赋值则不能使用)
Student student = new Student()
.setAge(24)
.setName(“zs”); -
bean的必输字段如name,一般方法是将name字段包装成一个构造方法,只有传入name这样的构造方法,才能创建一个Student对象。lombok可以用@RequiredArgsConstructor解决
@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = “ofName”)
public class Student {
@NonNull private String name;
private int age;
}
测试代码:
Student student = Student.ofName(“zs”); -
使用builder,builder模式大家都知道,lombok中有一个注解可以替代@Builder
一般的builder模式如下:
public class Student {
private String name;
private int age;public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public static Builder builder(){ return new Builder(); } public static class Builder{ private String name; private int age; public Builder name(String name){ this.name = name; return this; } public Builder age(int age){ this.age = age; return this; } public Student build(){ Student student = new Student(); student.setAge(age); student.setName(name); return student; } } }
调用方式:
Student student = Student.builder().name(“zs”).age(24).build();
这样的builder代码,实在是恶心难受,于是可以用lombok替代
@Builder
public class Student {
private String name;
private int age;
}
调用方式:
Student student = Student.builder().name(“zs”).age(24).build();
5.try-with-resource语句调用
JDK7之后,Java多了个新的语法:try-with-resources语句,
可以理解为是一个声明一个或多个资源的 try语句(用分号隔开),
一个资源作为一个对象,并且这个资源必须要在执行完关闭的,
try-with-resources语句确保在语句执行完毕后,每个资源都被自动关闭 。
任何实现了** java.lang.AutoCloseable的对象, 包括所有实现了 java.io.Closeable** 的对象
, 都可以用作一个资源。作者:程序鱼链接:https://www.jianshu.com/p/258c5ce1a2bd来源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
private void handle(String fileName) {
//括号里面的流会被自动关闭
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
...
}
} catch (Exception e) {
...
}
}
6.减少Null != XXX 的写法
尽所周知,好多变量都有可能为null,所有就写了满屏的null != xxx 的判断,这种判断多了,就很碍眼了,怎么能够减少这种写法,还能不影响业务逻辑
返回值为null有两种含义
- null代表着一种实际意义,这个值就存在有null的情况
- null值为错误值,后续操作不允许此值为null
如上两种情况,第一种情况,后续代码就是要兼容为null的情况,第二种情况就要想办法避免返回null值,或者是否需要抛出一个异常。
如
public class MyParser implements Parser {
private static Action DO_NOTHING = new Action() {
public void doSomething() { /* do nothing */ }
};
public Action findAction(String userInput) {
// ...
if ( /* we can't find any actions */ ) { //再找不到匹配的action的时候,我们返回默认的action,防止获取action的后续代码出现null的情况
return DO_NOTHING;
}
}}
7.正确使用equals方法
Objects的equals的方法经常报空指针异常,所以我们都知道需要使用常量或者确定的对象来调用equals方法。举个例子
// 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常
String str = null;
if (str.equals("SnailClimb")) {
...
} else {
..
}
运行上面的程序会抛出空指针异常,但是我们把第二行的条件判断语句改为下面这样的话,就不会抛出空指针异常,else 语句块得到执行。:
"SnailClimb".equals(str);// false
不过更推荐使用 java.util.Objects#equals(JDK7 引入的工具类)。
Objects.equals(null,"SnailClimb");// false
我们看一下java.util.Objects#equals的源码就知道原因了。
public static boolean equals(Object a, Object b) {
// 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。
return (a == b) || (a != null && a.equals(b));
}
注意:
- 每种原始类型都有默认值一样,如int默认值为 0,boolean 的默认值为 false,null 是任何引用类型的默认值,不严格的说是所有 Object 类型的默认值。
- 可以使用 == 或者 != 操作来比较null值,但是不能使用其他算法或者逻辑操作。在Java中null == null将返回true。
- 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常