[代码重构](master): Java文档更新

Java一般错误优化解决
master
土豆兄弟 2 years ago
parent 175b449b03
commit 24452c41d5

@ -5,12 +5,12 @@
## 0. 目录
Java并发编程
Spring 常见错误笔记
- Java并发编程
- Spring 常见错误笔记
- 业务编写之坑
- 代码编写常见问题
- 代码设计常见问题
- 安全常见问题
## 2. Spring 常见错误笔记
@ -253,7 +253,7 @@ public class HelloWorldController {
- 条件的成立在于解析 HelloWorldController 这个 Bean 时,我们会发现有方法标记了 Lookup此时就会添加相应方法到属性 methodOverrides 里面去
此过程由AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成)
### 2.1 Spring Core - Spring Bean 依赖注入常见错误
### 2.2 Spring Core - Spring Bean 依赖注入常见错误
- Spring @Autowired “控制反转、依赖注入”
#### A. 案例 1过多赠予无所适从
- @Autowired 时,不管你是菜鸟级还是专家级的 Spring 使用者,都应该制造或者遭遇过类似的错误:
@ -552,14 +552,881 @@ DataService innerClassDataService;
```
- 这个引用看起来有些许奇怪但实际上是可以工作的反而直接使用innerClassDataService 来引用倒是真的不可行。
- **对源码的学习是否全面决定了我们以后犯错的可能性大小**
#### D. @Value 没有注入预期的值
- @Value 必须指定一个字符串值,因为其定义做了要求,定义代码如下:
```java
public @interface Value {
/**
* The actual value expression &mdash; for example, <code>#{systemPropertie
*/
String value();
}
```
- 我们一般都会因为 @Value 常用于 String 类型的装配而误以为 @Value 不能用于非内置对象的装配,实际上这是一个常见的误区
- 例如,我们可以使用下面这种方式来 Autowired 一个属性成员:
```java
@Value("#{student}")
private Student student;
```
- 其中 student 这个 Bean 定义如下:
```java
@Bean
public Student student(){
Student student = createStudent(1, "xie");
return student;
}
```
- 使用 @Value 更多是用来装配 String而且它支持多种强大的装配方式典型的方式参考下面的示例
```java
//注册正常字符串
@Value("我是字符串")
private String text;
//注入系统参数、环境变量或者配置文件中的值
@Value("${ip}")
private String ip
//注入其他Bean属性其中student为bean的IDname为其属性
@Value("#{student.name}")
private String name;
```
- **异常案例**
- application.properties
```properties
username=admin
password=pass
```
```java
@RestController
@Slf4j
public class ValueTestController {
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@RequestMapping(path = "user", method = RequestMethod.GET)
public String getUser(){
return username + ":" + password;
}
}
```
- 我们去打印上述代码中的 username 和 password 时,我们会发现 password 正确返回了,但是 username 返回的并不是配置文件中指明的 admin
- **案例解析**
- Spring 是如何根据 @Value 来查询“值”的
- 通过方法 DefaultListableBeanFactory#doResolveDependency 来了解 @Value 的核心工作流程,代码如下:
```java
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
// ...
Class<?> type = descriptor.getDependencyType();
// 寻找 @Value
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
if (value instanceof String) {
// 解析 @Value
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null && containsBean(beanName) ?
getMergedBeanDefinition(beanName) : null);
value = evaluateBeanDefinitionString(strVal, bd);
}
// 转化 Value 解析的结果到装配的类型
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
try {
return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
}
catch (UnsupportedOperationException ex) {
// 异常处理
}
}
// ...
}
```
- @Value 的工作大体分为以下三个核心步骤。
- 寻找 @Value,在这步中,主要是判断这个属性字段是否标记为 @Value
- 依据的方法参考 QualifierAnnotationAutowireCandidateResolver#findValue
```java
@Nullable
protected Object findValue(Annotation[] annotationsToSearch) {
if (annotationsToSearch.length > 0) { // qualifier annotations have to be local
AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
//valueAnnotationType即为@Value
if (attr != null) {
return extractValue(attr);
}
}
return null;
}
```
- 解析 @Value 的字符串值
- 如果一个字段标记了 @Value,则可以拿到对应的字符串值,然后就可以根据字符串值去做解析,最终解析的结果可能是一个字符串,也可能是一个对象,这取决于字符串怎么写。
- 将解析结果转化为要装配的对象的类型
- 当拿到第二步生成的结果后,我们会发现可能和我们要装配的类型不匹配。假设我们定义的是 UUID而我们获取的结果是一个字符串那么这个时候就会根据目标类型来寻找转
化器执行转化,字符串到 UUID 的转化实际上发生在 UUIDEditor 中:
```java
public class UUIDEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException
if (StringUtils.hasText(text)) {
//转化操作
setValue(UUID.fromString(text.trim()));
}else {
setValue(null);
}
}
//...
}
```
- 解析 Value 指定字符串过程
```java
String strVal = resolveEmbeddedValue((String) value);
```
- 这里其实是在解析嵌入的值实际上就是“替换占位符”工作。具体而言它采用的是PropertySourcesPlaceholderConfigurer 根据 PropertySources 来替换。
- 不过当使用${username} 来获取替换值时,其最终执行的查找并不是局限在 application.property 文件中的。
- 而具体的查找执行,我们可以通过下面的代码 PropertySourcesPropertyResolver#getProperty来获取它的执行方式
```java
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
// 查到 Value 就退出
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Could not find key '" + key + "' in any property source");
}
return null;
}
```
- 在解析 Value 字符串时其实是有顺序的查找的源是存在CopyOnWriteArrayList 中,在启动时就被有序固定下来),一个一个“源”执行查找。在其中一个源找到后,就可以直接返回了。
- 查看 systemEnvironment 这个源,会发现刚好有一个 username 和我们是重合的,且值不是 pass。
- 刚好系统环境变量systemEnvironment中含有同名的配置。实际上对于系统参数systemProperties也是一样的这些参数或者变量都有很多如果我们没有意识到它
的存在,起了一个同名的字符串作为 @Value 的值,则很容易引发这类问题。
- **问题修正**
- 一定要注意**不仅要避免和环境变量冲突,也要注意避免和系统变量等其他变量冲突**
#### E. 错乱的注入集合
- 集合类型的自动注入是 Spring 提供的另外一个强大功能。
- 存在多个学生 Bean我们需要找出来并存储到一个 List里面去。多个学生 Bean 的定义如下:
```java
@Bean
public Student student1(){
return createStudent(1, "xie");
}
@Bean
public Student student2(){
return createStudent(2, "fang");
}
private Student createStudent(int id, String name) {
Student student = new Student();
student.setId(id);
student.setName(name);
return student;
}
```
- 有了集合类型的自动注入后,我们就可以把零散的学生 Bean 收集起来了,代码示例如
```java
@RestController
@Slf4j
public class StudentController {
private List<Student> students;
public StudentController(List<Student> students){
this.students = students;
}
@RequestMapping(path = "students", method = RequestMethod.GET)
public String listStudents(){
return students.toString();
}
}
```
- 通过上述代码,我们就可以完成集合类型的注入工作,输出结果如下:
> [Student(id=1, name=xie), Student(id=2, name=fang)]
- 当我们持续增加一些 student 时,可能就不喜欢用这种方式来注入集合类型了,而是倾向于用下面的方式去完成注入工作:
```java
@Bean
public List<Student> students(){
Student student3 = createStudent(3, "liu");
Student student4 = createStudent(4, "fu");
return Arrays.asList(student3, student4);
}
```
- 我们不妨将上面这种方式命名为“直接装配方式”,而将之前的那种命名为“收集方式”。
- 如果我们不小心让这 2 种方式同时存在了,结果会怎样?
- 然而,当我们运行起程序,就会发现后面的注入方式根本没有生效。即依然返回的是前面定义的 2 个学生。为什么会出现这样的错误呢?
- **案例解析**
- 收集装配风格Spring 使用的是 DefaultListableBeanFactory#resolveMultipleBeans
```java
@Nullable
private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
Class<?> type = descriptor.getDependencyType();
if (descriptor instanceof StreamDependencyDescriptor) {
// 装配 stream
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
Stream<Object> stream = matchingBeans.keySet().stream()
.map(name -> descriptor.resolveCandidate(name, type, this))
.filter(bean -> !(bean instanceof NullBean));
if (((StreamDependencyDescriptor) descriptor).isOrdered()) {
stream = stream.sorted(adaptOrderComparator(matchingBeans));
}
return stream;
}
else if (type.isArray()) {
// 装配 数组
Class<?> componentType = type.getComponentType();
ResolvableType resolvableType = descriptor.getResolvableType();
Class<?> resolvedArrayType = resolvableType.resolve(type);
if (resolvedArrayType != type) {
componentType = resolvableType.getComponentType().resolve();
}
if (componentType == null) {
return null;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, componentType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
Object result = converter.convertIfNecessary(matchingBeans.values(), resolvedArrayType);
if (result instanceof Object[]) {
Comparator<Object> comparator = adaptDependencyComparator(matchingBeans);
if (comparator != null) {
Arrays.sort((Object[]) result, comparator);
}
}
return result;
}
else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
//装配集合
//获取集合的元素类型
Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
if (elementType == null) {
return null;
}
// 根据元素类型查找所有的bean
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
// 转化查到的所有bean放置到集合并返回
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
Object result = converter.convertIfNecessary(matchingBeans.values(), type);
// ...
return result;
}
else if (Map.class == type) {
// 解析map
// ...
return matchingBeans;
}
else {
return null;
}
}
```
- 到这,我们就不难概括出这种收集式集合装配方式的大体过程了
- 1- 获取集合类型的元素类型
- 目标类型定义为 List<Student> students所以元素类型为 Student获取的具体方法参考代码行
- Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
- 2- 根据元素类型,找出所有的 Bean
- 有了上面的元素类型,即可根据元素类型来找出所有的 Bean关键代码行如下
- Map<String, Object> matchingBeans = findAutowireCandidates(beanName,elementType, new MultiElementDescriptor(descriptor));
- 3- 将匹配的所有的 Bean 按目标类型进行转化
- 经过步骤 2我们获取的所有的 Bean 都是以 java.util.LinkedHashMap.LinkedValues 形式存储的,和我们的目标类型大概率不同,所以最后一步需要做的是按需转化。
- 中,我们就需要把它转化为 List转化的关键代码如下:
- Object result = converter.convertIfNecessary(matchingBeans.values(), type);
- 两种装配集合的方式是不能同存的,结合本案例,当使用收集装配方式来装配时,能找到任何一个对应的 Bean则返回如果一个都没有找到才会采用直接装配的方式。
- **问题修复**
- **在对于同一个集合对象的注入上,混合多种注入方式是不可取的,这样除了错乱,别无所得。**
### 2.3 Spring Core - Spring Bean生命周期常见错误
#### A. 构造器内抛空指针异常
- 在构建宿舍管理系统时,有 LightMgrService 来管理 LightService从而控制宿舍灯的开启和关闭。我们希望在 LightMgrService 初始化时能够自动调用
LightService 的 check 方法来检查所有宿舍灯的电路是否正常,代码如下:
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
@Autowired
private LightService lightService;
public LightMgrService() {
lightService.check();
}
}
```
- 我们在 LightMgrService 的默认构造器中调用了通过 @Autoware 注入的成员变量
- LightService 的 check 方法:
```java
@Service
public class LightService {
public void start() {
System.out.println("turn on all lights");
}
public void shutdown() {
System.out.println("turn off all lights");
}
public void check() {
System.out.println("check all lights");
}
}
```
- 以上代码定义了 LightService 对象的原始类。
- 从整个案例代码实现来看,我们的期待是在 LightMgrService 初始化过程中LightService 因为标记为 @Autowired,所以能被自动装配好;然后在 LightMgrService
的构造器执行中LightService 的 shutdown() 方法能被自动调用;最终打印出 check all lights。
- 然而事与愿违,我们得到的只会是 NullPointerException
- **案例解析**
- Spring 类初始化过程没有足够的了解
- ![Spring类初始化](pic/Spring类初始化.png)
- 这个图初看起来复杂,我们不妨将其分为三部分:
- 第一部分,将一些必要的系统类,比如 Bean 的后置处理器类,注册到 Spring 容器,其中就包括我们这节课关注的 CommonAnnotationBeanPostProcessor 类;
- 第二部分,将这些后置处理器实例化,并注册到 Spring 的容器中;
- 第三部分,实例化所有用户定制类,调用后置处理器进行辅助装配、类初始化等等。
- 第一部分和第二部分并非是我们今天要讨论的重点这里仅仅是为了让你知道CommonAnnotationBeanPostProcessor 这个后置处理类是何时被 Spring 加载和实例化的。
- 拓展两个知识点:
- 很多必要的系统类,尤其是 Bean 后置处理器比如CommonAnnotationBeanPostProcessor、AutowiredAnnotationBeanPostProcessor 等),都是被 Spring 统一加载和管理的,
并在 Spring 中扮演了非常重要的角色;
- 通过 Bean 后置处理器Spring 能够非常灵活地在不同的场景调用不同的后置处理器,比如接下来我会讲到示例问题如何修正,修正方案中提到的 PostConstruct 注解,它的处理逻辑就需要用到
CommonAnnotationBeanPostProcessor继承自InitDestroyAnnotationBeanPostProcessor这个后置处理器。
- 我们重点看下第三部分,即 Spring 初始化单例类的一般过程,基本都是 getBean()->doGetBean()->getSingleton(),如果发现 Bean 不存在,则调用 createBean()-
doCreateBean() 进行实例化
- 查看 doCreateBean() 的源代码如下:
- Bean 初始化的三个关键步骤
- createBeanInstance 实例化 Bean
- populateBean 注入 Bean 依赖
- initializeBean 初始化 Bean
- 执行 @PostConstruct 标记的方法 这三个功能,这也和上述时序图的流程相符。
- 而用来实例化 Bean 的 createBeanInstance 方法通过依次调用 DefaultListableBeanFactory.instantiateBean() > SimpleInstantiationStrategy.instantiate(),最终执行到
BeanUtils.instantiateClass()
- 这里因为当前的语言并非 Kotlin所以最终将调用 ctor.newInstance() 方法实例化用户定制类 LightMgrService而默认构造器显然是在类实例化的时候被自动调用的Spring 也
无法控制。而此时负责自动装配的 populateBean 方法还没有被执行LightMgrService 的属性 LightService 还是 null因而得到空指针异常也在情理之中。
- **问题修正**
- 问题根源: **使用 @Autowired 直接标记在成员属性上而引发的装配行为是发生在构造器执行之后的**
- **修正代码1**
```java
@Component
public class LightMgrService {
private LightService lightService;
public LightMgrService(LightService lightService) {
this.lightService = lightService;
lightService.check();
}
}
```
- 当使用上面的代码时,构造器参数 LightService 会被自动注入 LightService 的 Bean从而在构造器执行时不会出现空指针。可以说**使用构造器参数来隐式注入**是一种 Spring 最佳实践
- **修正代码2**
- 添加 init 方法,并且使用 PostConstruct 注解进行修饰:
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
@Autowired
private LightService lightService;
@PostConstruct
public void init() {
lightService.check();
}
}
```
- - **修正代码3**
- 实现 InitializingBean 接口,在其 afterPropertiesSet() 方法中执行初始化代码:
```java
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService implements InitializingBean {
@Autowired
private LightService lightService;
@Override
public void afterPropertiesSet() throws Exception {
lightService.check();
}
}
```
- 后续的两种方案并不是最优的,但是在一些场景下,这两种方案各有所长
#### B. 意外触发 shutdown 方法
- 在类销毁时,也会有一些相对隐蔽的约定,导致一些难以察觉的错误
- 我们可能会去掉 @Service 注解,而是使用另外一种产生 Bean 的方式:创建一个配置类 BeanConfiguration标记 @Configuration)来创建一堆 Bean其中就
包含了创建 LightService 类型的 Bean并将其注册到 Spring 容器:
```java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
@Bean
public LightService getTransmission(){
return new LightService();
}
}
```
- 复用案例 1 的启动程序,稍作修改,让 Spring 启动完成后立马关闭当前 Spring 上下文这样等同于模拟宿舍管理系统的启停:
```java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Applica
context.close();
}
}
```
- 以上代码没有其他任何方法的调用,仅仅是将所有符合约定的类初始化并加载到 Spring 容器,完成后再关闭当前的 Spring 容器。
- 按照预期,这段代码运行后不会有任何的 log 输出,毕竟我们只是改变了 Bean 的产生方式。
- 但实际运行这段代码后,我们可以看到控制台上打印了 shutting down all lights。显然shutdown 方法未按照预期被执行了,这导致一个很有意思的 bug在使用新的 Bean 生
成方式之前,每一次宿舍管理服务被重启时,宿舍里所有的灯都不会被关闭。但是修改后,只有服务重启,灯都被意外关闭了。如何理解这个 bug?
- **问题解析**
- TODO 后续补充源码跟进
- **问题修正**
- 我们可以通过避免在 Java 类中定义一些带有特殊意义动词的方法来解决,当然如果一定要定义名为 close 或者 shutdown 方法,也可以通过将 Bean 注解内 destroyMethod 属性
设置为空的方式来解决这个问题。
- 第一种修改方式比较简单,所以这里只展示第二种修改方式,代码如下:
```java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
@Bean(destroyMethod="")
public LightService getTransmission(){
return new LightService();
}
}
```
## 3. 业务编写之坑
### 3.1
### 3.21 设计 - 代码重复:搞定代码重复的三个绝招
- 涉及知识: 设计模式、Java 高级特性、OOP
#### A. 利用工厂模式 + 模板方法模式,消除 if…else 和重复代码
- 业务模拟背景
- 假设要开发一个购物车下单的功能,针对不同用户进行不同处理:
- 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣;
- VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
- 内部用户可以免运费,无商品折扣
- 目标
- 实现三种类型的购物车业务逻辑,把入参 Map 对象Key 是商品 IDValue是商品数量转换为出参购物车类型 Cart。
```java
//购物车
@Data
public class Cart {
//商品清单
private List<Item> items = new ArrayList<>();
//总优惠
private BigDecimal totalDiscount;
//商品总价
private BigDecimal totalItemPrice;
//总运费
private BigDecimal totalDeliveryPrice;
//应付总价
private BigDecimal payPrice;
}
//购物车中的商品
@Data
public class Item {
//商品ID
private long id;
//商品数量
private int quantity;
//商品单价
private BigDecimal price;
//商品优惠
private BigDecimal couponPrice;
//商品运费
private BigDecimal deliveryPrice;
}
//普通用户购物车处理
public class NormalUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();
//把Map的购物车转换为Item列表
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);
//处理运费和商品优惠
itemList.stream().forEach(item -> {
//运费为商品总价的10%
item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(i
//无优惠
item.setCouponPrice(BigDecimal.ZERO);
});
//计算商品总价
cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算总运费
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算总折扣
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
// 计算应付价格
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
```
- 然后实现针对 VIP 用户的购物车逻辑。与普通用户购物车逻辑的不同在于VIP 用户能享受同类商品多买的折扣。所以,这部分代码只需要额外处理多买折扣部分
```java
public class VipUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
...
itemList.stream().forEach(item -> {
//运费为商品总价的10%
item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(i))
//购买两件以上相同商品,第三件开始享受一定折扣
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPerce))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
});
...
return cart;
}
}
```
- 最后是免运费、无折扣的内部用户,同样只是处理商品折扣和运费时的逻辑差异:
```java
public class InternalUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
...
itemList.stream().forEach(item -> {
//免运费
item.setDeliveryPrice(BigDecimal.ZERO);
//无优惠
item.setCouponPrice(BigDecimal.ZERO);
});
...
return cart;
}
}
```
- **分析上述坏代码**:
- 对比一下代码量可以发现,三种购物车 70% 的代码是重复的。原因很简单,虽然不同类型用户计算运费和优惠的方式不同,但整个购物车的初始化、统计总价、总运费、总优惠和支
付价格的逻辑都是一样的。
- 代码重复本身不可怕,可怕的是漏改或改错。比如,写 VIP 用户购物车的同学发现商品总价计算有 Bug不应该是把所有 Item 的 price 加在一起,而是应该把所有 Item
的 price*quantity 加在一起。这时,他可能会只修改 VIP 用户购物车的代码,而忽略了普通用户、内部用户的购物车中,重复的逻辑实现也有相同的 Bug
- 有了三个购物车后,我们就需要根据不同的用户类型使用不同的购物车了。如下代码所示,使用三个 if 实现不同类型用户调用不同购物车的 process 方法:
```java
@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {
//根据用户ID获得用户类型
String userCategory = Db.getUserCategory(userId);
//普通用户处理逻辑
if (userCategory.equals("Normal")) {
NormalUserCart normalUserCart = new NormalUserCart();
return normalUserCart.process(userId, items);
}
//VIP用户处理逻辑
if (userCategory.equals("Vip")) {
VipUserCart vipUserCart = new VipUserCart();
return vipUserCart.process(userId, items);
}
//内部用户处理逻辑
if (userCategory.equals("Internal")) {
InternalUserCart internalUserCart = new InternalUserCart();
return internalUserCart.process(userId, items);
}
return null;
}
```
- 电商的营销玩法是多样的,以后势必还会有更多用户类型,需要更多的购物车。我们就只能不断增加更多的购物车类,一遍一遍地写重复的购物车逻辑、写更多的 if 逻辑
- **改进措施**
- 抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑
- 这个模式就是**模板方法模式**
- 在父类中实现了购物车处理的流程模板,然后把需要特殊处理的地方留空白也就是留抽象方法定义,让子类去实现其中的逻辑。由于父类的辑不完整无法单独工作,因此需要定义为抽象类。
- 如下代码所示AbstractCart 抽象类实现了购物车通用的逻辑额外定义了两个抽象方法让子类去实现。其中processCouponPrice 方法用于计算商品折扣processDeliveryPrice 方法用于计算运费。
```java
public abstract class AbstractCart {
//处理购物车的大量重复逻辑在父类实现
public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);
//让子类处理每一个商品的优惠
itemList.stream().forEach(item -> {
processCouponPrice(userId, item);
processDeliveryPrice(userId, item);
});
//计算商品总价
cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算总运费
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算总折扣
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
// 计算应付价格
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}
//处理商品优惠的逻辑留给子类实现
protected abstract void processCouponPrice(long userId, Item item);
//处理配送费的逻辑留给子类实现
protected abstract void processDeliveryPrice(long userId, Item item);
}
```
- 有了这个抽象类,三个子类的实现就非常简单了。普通用户的购物车 NormalUserCart实现的是 0 优惠和 10% 运费的逻辑:
```java
@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {
@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}
@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(item.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity()))
.multiply(new BigDecimal("0.1")));
}
}
```
- VIP 用户的购物车 VipUserCart直接继承了 NormalUserCart只需要修改多买优惠策略
```java
@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {
@Override
protected void processCouponPrice(long userId, Item item) {
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
}else {
item.setCouponPrice(BigDecimal.ZERO);
}
}
}
```
- 内部用户购物车 InternalUserCart 是最简单的,直接设置 0 运费和 0 折扣即可:
```java
@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {
@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}
@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(BigDecimal.ZERO);
}
}
```
- 接下来,我们再看看如何能避免三个 if 逻辑
- 定义三个购物车子类时,我们在 @Service 注解中对 Bean 进行了命名。既然三个购物车都叫 XXXUserCart那我们就可以把用户类型字符串拼接 UserCart
构成购物车 Bean 的名称,然后利用 Spring 的 IoC 容器,通过 Bean 的名称直接获取到AbstractCart调用其 process 方法即可实现通用。
这就是**工厂模式**,只不过是借助 Spring 容器实现罢了:
```java
@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
String userCategory = Db.getUserCategory(userId);
AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
return cart.process(userId, items);
}
```
- 之后如果有了新的用户类型、新的用户逻辑,完全不用对代码做任何修改,只要新增一个 XXXUserCart 类继承 AbstractCart实现特殊的优惠和运费处理逻辑就可以了
- 我们就利用**工厂模式 + 模板方法模式**,不仅消除了重复代码,还避免了修改既有代码的风险
- 这就是设计模式中的开闭原则:对修改关闭,对扩展开放
#### B. 利用注解 + 反射消除重复代码
- 再看一个三方接口的调用案例,同样也是一个普通的业务逻辑
- 假设银行提供了一些 API 接口,对参数的序列化有点特殊,不使用 JSON而是需要我们把参数依次拼在一起构成一个大字符串
- 按照银行提供的 API 文档的顺序,把所有参数构成定长的数据,然后拼接在一起作为整个字符串。
- 因为每一种参数都有固定长度,未达到长度时需要做填充处理:
- 字符串类型的参数不满长度部分需要以下划线右填充,也就是字符串内容靠左;
- 数字类型的参数不满长度部分以 0 左填充,也就是实际数字靠右;
- 货币类型的表示需要把金额向下舍入 2 位到分,以分为单位,作为数字类型同样进行左填充
- 对所有参数做 MD5 操作作为签名为了方便理解Demo 中不涉及加盐处理)。
- 代码很容易实现,直接根据接口定义实现填充操作、加签名、请求调用操作即可:
```java
public class BankService {
//创建用户方法
public static String createUser(String name, String identity, String mobile, int age) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
//字符串靠左多余的地方填充_
stringBuilder.append(String.format("%-10s", name).replace(' ', '_'));
//字符串靠左多余的地方填充_
stringBuilder.append(String.format("%-18s", identity).replace(' ', '_'));
//数字靠右多余的地方用0填充
stringBuilder.append(String.format("%05d", age));
// 字符串靠左多余的地方用_填充
stringBuilder.append(String.format("%-11s", mobile).replace(' ', '_'));
// 最后加上MD5作为签名
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
return Request.Post("http://localhost:45678/reflection/bank/createUser")
.bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}
//支付方法
public static String pay(long userId, BigDecimal amount) throws IOException{
StringBuilder stringBuilder = new StringBuilder();
//数字靠右多余的地方用0填充
stringBuilder.append(String.format("%020d", userId));
//金额向下舍入2位到分以分为单位作为数字靠右多余的地方用0填充
stringBuilder.append(String.format("%010d", amount.setScale(2, , RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
//最后加上MD5作为签名
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
return Request.Post("http://localhost:45678/reflection/bank/pay")
.bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}
}
```
- 可以看到,这段代码的重复粒度更细:
- 三种标准数据类型的处理逻辑有重复,稍有不慎就会出现 Bug
- 处理流程中字符串拼接、加签和发请求的逻辑,在所有方法重复;
- 实际方法的入参的参数类型和顺序,不一定和接口要求一致,容易出错;
- 代码层面针对每一个参数硬编码,无法清晰地进行核对,如果参数达到几十个、上百个,出错的概率极大。
- **改造方法**
- 要用注解和反射
- 使用注解和反射这两个武器,就可以针对银行请求的所有逻辑均使用一套代码实现,不会出
现任何重复。
- 要实现接口逻辑和逻辑实现的剥离,首先需要以 POJO 类(只有属性没有任何业务逻辑的数据类)的方式定义所有的接口参数。比如,下面这个创建用户 API 的参数
```java
@Data
public class CreateUserAPI {
private String name;
private String identity;
private String mobile;
private int age;
}
```
- 有了接口参数定义,我们就能通过自定义注解为接口和所有参数增加一些元数据。如下所示,我们定义一个接口 API 的注解 BankAPI包含接口 URL 地址和接口说明:
```java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {
String desc() default "";
String url() default "";
}
```
- 然后,我们再定义一个自定义注解 @BankAPIField,用于描述接口的每一个字段规范,包含参数的次序、类型和长度三个属性:
```java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {
int order() default -1;
int length() default -1;
String type() default "";
}
```
-
### 3.22 设计 - 接口设计:系统间对话的语言,一定要统一
### 3.23 设计 - 缓存设计:缓存可以锦上添花也可以落井下石
### 3.24 设计 - 业务代码写完,就意味着生产就绪了?
### 3.25 设计 - 异步处理好用,但非常容易用错
### 3.26 设计 - 数据存储NoSQL与RDBMS如何取长补短、相辅相成

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>dev-protocol</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>java-demo</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>dev-protocol-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

@ -43,6 +43,7 @@
<module>longpolling/demo/demo3/dev-protocol-netty-client</module>
<module>longpolling/demo/demo3/dev-protocol-netty-server</module>
<module>longpolling/demo/demo3/dev-protocol-netty-common</module>
<module>code-language/java/java-demo</module>
</modules>
<properties>

Loading…
Cancel
Save