代码设计与整洁总结
1. 使用Spring Validation 校验参数
- 改造前:Controller校验参数时,会写非常多的校验逻辑,且会与正常的业务代码糅合在一起,造成阅读代码的不适感。简单实例如下:
-
import lombok.Data; import lombok.ToString; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author anyu * @date 2021/8/11 12:10 */ @RestController @RequestMapping public class DemoController { @PostMapping("test") public void test(@RequestBody Student student) { if (student.getAge() == null || student.getAge() <= 0 ) { throw new RuntimeException("年龄参数不正确"); } if (StringUtils.isNotBlank(student.getName())){ throw new RuntimeException("年龄参数不正确"); } System.err.println(student); } } @Data @ToString class Student { private Integer age; private String name; }
-
- 改造后:使用Spring Validation 能够在POJO类中直接进行参数校验,使校验代码与业务代码分开,职责清晰,整洁度较好,代码示例如下:
-
import lombok.Data; import lombok.ToString; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; /** * @author anyu * @date 2021/8/11 12:10 */ @RestController @RequestMapping public class DemoController { @PostMapping("test") public void test(@RequestBody @Valid Student student) { System.err.println(student); } } @Data @ToString class Student { @Min(value = 1, message = "年龄参数不正确") private Integer age; @NotBlank(message = "年龄参数不正确") private String name; }
-
- 总结:关于Spring Validation 的使用,在对象前加上 @Valid 注解,然后在请求参数内使用 @Min、@Max、@NotNull、@NotEmpty、@Email等各种注解实现对不同参数的校验,具体用法可自行学习。
2. 枚举大有用处
- 枚举在我们的工作中经常会遇到。它有很多用处,比如创建单例对象(最佳单例的实现方式)、对状态值类型的映射、以及与其他的设计混合使用等等;
- 使用枚举创建单例模式并使用:
- 创建一个RespEntity类:
-
import lombok.Data; import java.io.Serializable; /** * @author anyu * @date 2021/7/27 10:50 */ @Data public class RespEntity<T> implements Serializable { private Integer code; private String message; private T data; public RespEntity(T data) { this.code = ResultCode.DefaultSuccess.getCode(); this.message = ResultCode.DefaultSuccess.getMessage(); this.data = data; } public RespEntity(ResultCode resultCode, T data) { this.code = resultCode.getCode(); this.message = resultCode.getMessage(); this.data = data; } public RespEntity(ResultCode resultCode) { this.code = resultCode.getCode(); this.message = resultCode.getMessage(); } public RespEntity(Integer code, String message){ this.code = code; this.message = message; } public RespEntity(Integer code, String message, T data) { this.code = code; this.message = message; this.data = data; } public RespEntity() { } public static RespEntity ok() { return OkR.INSTANCE.getInstance(); } public RespEntity ok(Integer code, String message) { return new RespEntity(code, message); } public RespEntity<T> ok(Integer code, String message, T data) { return new RespEntity<>(code, message, data); } public RespEntity<T> ok(T data) { return new RespEntity<>(ResultCode.DefaultSuccess, data); } public RespEntity fail() { return FailR.INSTANCE.getInstance(); } public RespEntity fail(String message) { return new RespEntity<>(ResultCode.DefaultFail.getCode(),message, ""); } public enum OkR { INSTANCE; private RespEntity instance; /** * JVM保证这个方法绝对只调用一次 */ OkR() { instance = new RespEntity<>(ResultCode.DefaultSuccess,""); } public RespEntity getInstance() { return instance; } } public enum FailR { INSTANCE; private RespEntity instance; /** * JVM保证这个方法绝对只调用一次 */ FailR() { instance = new RespEntity<>(ResultCode.DefaultFail,""); } public RespEntity getInstance() { return instance; } } }
里面的
OkR
、FailR
就是使用枚举创建的单例模式; - 使用RespEntity:
/** * @author anyu * @date 2021/8/11 12:10 */ @RestController @RequestMapping public class DemoController { @PostMapping("test") public RespEntity test() { return RespEntity.ok(); } }
- 使用枚举替代switch的功能:
-
改造前:
public static void main(String[] args) { String status = "N"; String statusDesc; switch (status) { case "N": statusDesc = "New"; break; case "D": statusDesc = "Inactive"; break; case "O": statusDesc = "Active"; break; case "S": statusDesc = "Suspended"; break; case "T": statusDesc = "Terminated"; break; case "C": statusDesc = "Closed"; break; default: statusDesc = "状态未知"; break; } System.err.println(statusDesc); }
-
改造后:
- 定义枚举类:
/** * @author anyu * @date 2021/8/11 10:16 */ public enum Status { N("New"), D("Inactive"), O("Active"), S("Suspended"), T("Terminated"), C("Closed"), Unknown("Unknown"); private String value; public String getValue() { return value; } Status(String value) { this.value = value; } public static Status forName(String status) { if (status == null) { return Status.Unknown; } String statusTrim = status.trim(); for (Status pc : values()) { if (pc.toString().equalsIgnoreCase(statusTrim)) { return pc; } } return Status.Unknown; } }
- 实现前面switch功能:
public static void main(String[] args) { String status = "N"; Status sellerStatus = Status.forName(status); System.err.println(sellerStatus.getValue()); }
代码在很大程度上被简洁了。
- 定义枚举类:
-
- 使用枚举的其他可行性:
- 我们可以去想一下枚举具有哪些特点,然后能够发挥它优势的地方都能够使用枚举;
- 它的优点有:
- 只会实例化一次;性能好;
- 见名知义,可读性好;
- 横向扩展能力好,可以通过一个元素关联其他元素;
- 减少传递参数错误,限定类型;
- 不可被更改;
结合它的优点去考虑使用场景;比如它的一个优点是减少传递参数错误,我们利用这个优点可以将一些状态值、类型值使用枚举来表示,以避免一些参数写错,且可读性更好;
3. Stream流的简化数据处理
- 相信同学们经常会使用集合,然后对其中的集合进行操作,比如进行过滤,进行排序,获取第一条数据,转换成另外一种集合,往往会写非常多的代码,可是当JDK 8 的Stream出世之后,你能想到的一些处理操作,往往大部分都能靠Stream流解决,你以前可能会写很多代码来进行处理,优雅的Stream只需要一行语句即可完成你想要达成的效果!
- 博主介绍文章,内有Stream流使用的详细说明:Java 1.8 函数式编程详解
- 简单示例:
public static void main(String[] args) { List<Integer> list = Lists.newArrayList(3, 2, 1, 5, 4, 6, 7); // 过滤出不等于2、不等于3 的且按照升序排序的结果 List<Integer> result = list.stream().filter(x -> x != 2 && x != 3).sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList()); System.err.println(result); }
- 打印结果如下:
[1, 4, 5, 6, 7]
4. 使用Optional类消灭空指针异常(NullPointerException)
- 从 Java 8 引入的一个很有趣的特性是 Optional 类。Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException)
- 使用Optional 前:
@Data @ToString static class Student { @Min(value = 1, message = "年龄参数不正确") private Integer age; @NotBlank(message = "年龄参数不正确") private String name; } public static void main(String[] args) { Student student = null; System.err.println(student.getName()); }
- 使用Optional 后:
public static void main(String[] args) { Student student = null; Optional.ofNullable(student).ifPresent(x -> System.err.println(x.getName())); }
使用Optional后,只有当student不为Null的时候,才会进行构造Optionl类,才会去执行
打印操作
,所以它不会抛出空指针异常;更多用法,请自行学习。
5. 使用递归简化代码
- 递归常用于一些有规律的或遍历的方法中;一些遍历操作每次的执行流程都是一样的,只是参数发生了变化,那么我们可以使用递归的方式来简化代码开发;比如文件遍历,每次执行的操作都是一致的,但是我们不知道文件的名称是什么,数量有多少,通过递归的方式就能很好的解决问题;
- 递归示例文件扫描:
import java.io.File; /** * csdn: anyu */ public class Demo1 { public static void main(String[] args) { File dir=new File("D:\\CCC");//浏览F盘a文件夹下的所有内容 listFile(dir,""); } public static void listFile(File dir,String spance) { File[] files=dir.listFiles(); //列出所有的子文件 for(File file :files) { if(file.isFile())//如果是文件,则输出文件名字 { System.out.println(spance+file.getName()); }else if(file.isDirectory())//如果是文件夹,则输出文件夹的名字,并递归遍历该文件夹 { System.out.println(spance+file.getName()); listFile(file,"|--"+spance);//递归遍历 } } } }
文件遍历我们预先不知道有哪些文件,不能够直接去查询。使用递归既可以依次扫描,又可以满足每次扫描要执行的过程,这种有规律的操作使用递归方式可以极大地简洁代码,秀出你的style!
- 编程没有银弹,只有精准下药,方可药到病除。在什么场景下使用递归比较合适呢?我们来看看它的优缺点:
- 优点:
- 代码简洁
- 易于理解
如在树的前/中/后序遍历中,递归的实现明显比循环简单。
- 缺点:
- 时间和空间的消耗比较大
递归由于是函数调用自身,而函数的调用时消耗时间和空间的,每一次函数调用,都需要在内存栈中分配空间以保存参数,返回值和临时变量,而往栈中压入和弹出数据也都需要时间,所以降低了效率。
- 重复计算
递归中又很多计算都是重复的,递归的本质时把一个问题分解成两个或多个小 问题,多个小问题存在重叠的部分,即存在重复计算,如斐波那契数列的递归实现。
- 调用栈溢出
递归可能时调用栈溢出,每次调用时都会在内存栈中分配空间,而栈空间的容量是有限的,当调用的次数太多,就可能会超出栈的容量,进而造成调用栈溢出。
理清递归的使用方式、优缺点、使用场景,以及跟循环的区别,是合理使用递归的要点;
6. 泛型
- 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
- 在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
- 最常用的泛型方式:
- 使用泛型类来自定义返回对象,参见我们
章节2
的创建RespEntity对象 - 创建一个xml解析工具类:
import java.io.IOException; import java.io.StringWriter; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; public class XmlUtils { private static XmlMapper xmlMapper = new XmlMapper(); private XmlUtils() { throw new IllegalStateException("Utility class"); } static { // 忽略空Bean转json的错误 xmlMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); // 忽略 在json字符串中存在,但是在java对象中不存在对应属性的情况。防止错误 xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 大小写脱敏 xmlMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); } public static <T> T string2Obj(String xmlSrc, Class<T> clazz) { if (null == xmlSrc || clazz == null) { return null; } try { return xmlMapper.readValue(xmlSrc, clazz); } catch (IOException e) { throw new RuntimeException(e); } } public static String obj2String(Object o) { try { JAXBContext context = JAXBContext.newInstance(o.getClass()); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); StringWriter writer = new StringWriter(); marshaller.marshal(o, writer); return writer.toString(); } catch (Exception e) { throw new RuntimeException(e); } } }
string2Obj()这个方法使用了泛型,调用此方法会返回指定类型的对象;
- 使用泛型类来自定义返回对象,参见我们
- 泛型的优缺点:
- 泛型简单易用
- 类型安全
泛型的主要目标是实现java的类型安全。 泛型可以使编译器知道一个对象的限定类型是什么,这样编译器就可以在一个高的程度上验证这个类型
- 消除了强制类型转换 使得代码可读性好,减少了很多出错的机会
- 简洁,提高重用率。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
我们可以根据泛型的特点去思考它的应用场景;
7. 使用ThreadLocal简洁代码
-
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
-
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示:
-
场景模拟:我们写了一套通用代码,对接了一家公司的CRM系统,但是临时要求再增加一个账号,不同数据流向同系统的两个账号中;(每个账号数据独立,对接代码一样,通过账号独立来将两种数据隔离)使用ThreadLocal的话,可以在不改变代码逻辑的情况下,增加一个判断:如果是A平台数据,则使用A账号;如果是B平台数据则使用B平台的AppKey、AppSecret;而后续的通用接口直接从ThreadLocal中读取账号信息,就可以将数据传送到对应的账号平台;
-
它就像一个中间者,平台不直接从左侧拿取账号,而直接从ThreadLocal中获取,这样能够易于扩展、解耦合、且极大的减少了重复代码,提高了编程效率
-
ThreadLocal 相关知识:
- 工具类:
import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** * @author 暗余 * @date 2021/7/17 15:44 */ @SuppressWarnings("unused") public final class ThreadLocalUtils { private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>(16)); /** * 获取到ThreadLocal中值 * * @return ThreadLocal存储的是Map */ public static Map<String, Object> getThreadLocal() { return THREAD_LOCAL.get(); } /** * 从ThreadLocal中的Map获取值 * * @param key Map中的key * @param <T> Map中的value的类型 * @return Map中的value值 可能为空 */ public static <T> T get(String key) { return get(key, null); } /** * 从ThreadLocal中的Map获取值 * * @param key Map中的key * @param defaultValue Map中的value的为null 是 的默认值 * @param <T> Map中的value的类型 * @return Map中的value值 可能为空 */ @SuppressWarnings("unchecked") public static <T> T get(String key, T defaultValue) { Map<String, Object> map = THREAD_LOCAL.get(); if (MapUtils.isEmpty(map)) { return null; } return (T) Optional.ofNullable(map.get(key)).orElse(defaultValue); } /** * ThreadLocal中的Map设置值 * * @param key Map中的key * @param value Map中的value */ public static void set(String key, Object value) { Map<String, Object> map = THREAD_LOCAL.get(); map.put(key, value); } /** * ThreadLocal中的Map 添加Map * * @param keyValueMap 参数map */ public static void set(Map<String, Object> keyValueMap) { Map<String, Object> map = THREAD_LOCAL.get(); map.putAll(keyValueMap); } /** * 删除ThreadLocal中的Map 中的value * * @param key Map中的key */ public static void delete(String key) { Map<String, Object> map = THREAD_LOCAL.get(); if (MapUtils.isEmpty(map)) { return; } map.remove(key); } /** * 删除ThreadLocal中的Map */ public static void remove() { THREAD_LOCAL.remove(); } /** * 从ThreadLocal中的Map获取值 根据可key的前缀 * * @param prefix key 的前缀 * @param <T> Map中的value的类型 * @return 符合条件的Map */ @SuppressWarnings("unchecked") public static <T> Map<String, T> fetchVarsByPrefix(String prefix) { Map<String, T> vars = new HashMap<>(16); if (StringUtils.isBlank(prefix)) { return vars; } Map<String, Object> map = THREAD_LOCAL.get(); if (MapUtils.isEmpty(map)) { return vars; } return map.entrySet().stream().filter(test -> test.getKey().startsWith(prefix)) .collect(Collectors.toMap(Map.Entry::getKey, time -> (T) time.getValue())); } /** * 删除ThreadLocal中的Map 中的Value 按 Map中的Key的前缀 * * @param prefix Map中的Key的前缀 */ public static void deleteVarsByPrefix(String prefix) { if (StringUtils.isBlank(prefix)) { return; } Map<String, Object> map = THREAD_LOCAL.get(); if (MapUtils.isEmpty(map)) { return; } map.keySet().stream().filter(o -> o.startsWith(prefix)).collect(Collectors.toSet()).forEach(map::remove); } }
- 存入值:
ThreadLocalUtils.set($key, $value);
- 取值:
ThreadLocalUtils.get($key)
- 删除值:
ThreadLocalUtils.remove();
一定要记得用完后要remove() 否则会发生内存泄漏;
- 工具类:
8. 结合工厂模式、Spring特性、枚举 优化代码
- 特性说明:
- 工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
- Spring : 在Spring中,bean可以被定义为两种模式:prototype(多例)和singleton(单例)
- singleton(单例):只有一个共享的实例存在,所有对这个bean的请求都会返回这个唯一的实例。
- prototype(多例):对这个bean的每次请求都会创建一个新的bean实例,类似于new。
Spring bean 默认是单例模式,我们在使用过程中也是用的它的单列模式
- 枚举,枚举的好处上面已有讲解,可以往上翻。
- 既然它们都有这么多好处,我们在写代码中能否结合使用呢?将他们的优点结合起来,作为一种更上一层的设计模板?当然有!请往下看
- 实战:
- 需求如下:我有一个流程需求,在第一步我要做xxx事情,第二步我要做xxx事情,第三步我要做xxx事情,如何设计来让我们既达到要求又能拥有很好的代码设计呢?
- 需求分析:
- 首先我们可以定义一个枚举类,这个枚举类中定义了这个流程的所有步骤;
- 其次我们可以定义一个工厂类,这个工厂类可以为我们实例化一个处理当前步骤的需求,通过传入枚举类,然后根据当前流程来对应创建相应的需求
- 最后我们可以为每一个流程步骤创建一个处理类,这个类能够执行当前流程步骤需要做的事情;
前两步我们利用了枚举和工厂能够创建对应的处理类。第三步我们做任务处理的时候,就可以结合Spring来进行,如果没有引进Spring 会导致类膨胀,每个线程处理一次请求就要实例化一个类,显然性能消耗太大。我们利用Spring的默认单例可以重复使用一个类,且与其他被Spring 管理的工具类如Mybatis、MQ 等无缝衔接,更加易于使用。
- 代码演示:
- 枚举:
/** * @author 暗余 * @date 2021/7/27 15:38 */ public enum ProcessStage { /** * 流程1 */ ProcessOne("serviceOne"), /** * 流程2 */ ProcessTwo("serviceTwo"), /** * 流程3 */ ProcessThree("serviceThree"), /** * 流程4 */ ProcessFour("serviceFour"), /** * 流程5 */ AccountCreate("serviceFive"); private String serviceName; ProcessStage(String serviceName) { this.serviceName = serviceName; } public String getServiceName() { return serviceName; } }
- 工厂类:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author 暗余 * @date 2021-08-10 */ @Component public class ServiceImplFactory { @Autowired private Map<String, IDemoService> serviceMap = new ConcurrentHashMap<>(ProcessStage.values().length); public IDemoService getService(ProcessStage stage) { return serviceMap.get(stage.getServiceName()); } /** * 在别处就可以类似这样直接先从工厂类中get出Spring的实例,然后调用它的process方法即可。 */ public void test(){ Object process = getService(ProcessStage.ProcessOne).process(); System.err.println(process.toString()); } }
- 处理类接口
public interface IDemoService { public String process(); }
- 处理实现类1
import org.springframework.stereotype.Service; /** * @author 暗余 * @date 2021/9/23 11:07 */ @Service("serviceOne") public class DemoServiceOne implements IDemoService{ @Override public String process() { return "serviceOne"; } }
- 处理实现类2
import org.springframework.stereotype.Service; /** * @author 暗余 * @date 2021/9/23 11:07 */ @Service("serviceTwo") public class DemoServiceTwo implements IDemoService { @Override public String process() { return "serviceTwo"; } }
- 枚举: