人的知识就好比一个圆圈,圆圈里面是已知的,圆圈外面是未知的。你知道得越多,圆圈也就越大,你不知道的也就越多。

0%

当存在实体引用时,我们会在前后端数据传递过程中做对引用字段做序列化与反序列化,也会在审批流弃审时做引用校验,本文主要是谈谈我对引用值序列化&反序列化的理解,弃审校验将在文章审批扩展中再分析。

反序列化

我认为反序列化是可以不要的,因为它就是把前端传递的值(字符串|json对象|json数组)解析成字符串,前端同样可以完成,无非是在表单保存的时候多一行赋值代码(封装到前端组件后,一样不必每处手动赋值)。
现在有点奇怪的是,假如我想保存冗余字段名称或编码,尽管前端已经传递了对象{"id": "xx", "code": "xx", "name": "xx"},但由于反序列化后后端只会接收到id,这样前端还得另外传递名称或编码。

序列化

同样,我认为序列化也可以不要,或者可以换一种实现方式。虽然它给开发者提供了便利,但如果服务调用者只是看我们API文档,他并不清楚某个外键字段到底是返回String、JSON Object还是JSON Array,JSON对象里面除了id、code、name外又包含哪些额外的字段。
我认为vo返回哪些字段应该明确在类里面定义,所以如果一定要做序列化,我会采用下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@JsonSerialize(using = ReferSerializer.class)
public static class ExampleVO {
@Refer(billTypes = "company", foreignKey = "id")
private String companyId;

@ByRefer(referField = "companyId", fieldName = "name")
private String companyName;
@ByRefer(referField = "companyId", fieldName = "code")
private String companyCode;
@ByRefer(referField = "companyId", fieldName = "type")
private String companyType;
...
}

@Refer注解指定哪个字段是外键,从哪些单据类型对应的实体中去查找,以及外键字段在原实体中的属性名。@ByRefer注解则指定要返回哪些字段,及两个实体间字段的对应关系。

缓存使用方式

由于序列化时首先会去缓存中查,缓存中没有再去查数据库,所以目前我们保存数据时会同步保存到redis中。
我认为缓存应该只缓存热点数据,而不是所有的数据都存里面,且我们应该考虑缓存过期策略。
有人会说只是同步缓存了id、code和name,影响不大,且不考虑是否真影响不大,其实我们并不只是同步缓存了id、code和name。比如在上面的ExampleVO类中,除了id、code和name外,我们还用到了companyType。按目前实现方式,要想序列化时得到companyType的值,我们会在注解@ReferSerialTransfer中设置extraFileds属性,这时有3种可能情况:

  • 缓存中没有数据,companyType有值
  • 缓存中有数据,但只有id、code和name,companyType没值
  • 缓存中有数据,但除id、code和name外,只有companyLevel(其他实体写入缓存的),companyType没值

由此可见,除非我们分析每个实体,把其可能被用的字段都写入缓存,否则很可能序列化中某个想要的字段查不到值,但是这样一是麻烦,二是会浪费大量内存。
我的实现里是在查数据之后再把数据写入缓存,并设置一个过期时限。

兼容性

在公有云合约系统中,像contractName、contractCode等字段都是在公共表里,但现在更新状态都是直接去更新合同表,所以我在各类合同表里也存了份字段。不过如果升级Hibernate到最新版本,又有新的问题:hibernate不会再自动同步更新公共表和实体表,
所以在改造后的实现中,我会依据属性ijz.bill.persistenceType的值(jpa|jdbc)来决定是使用EntityManager还是JdbcTemplate更新表字段。

dubbo官方已经提供了dubbo-spring-boot-starter,只需引入该依赖,我们便能快速地在项目中使用dubbo。但是我们系统现在用的是dubbox,这个没有提供starter,(论技术选型的重要性,为什么要选择已经停止维护了的第三方库?)那就只好自己实现个了>_<dubbox-spring-boot-starter。通过对dubbo-spring-boot-starter简单的封装,我们也能享受到starter带来的便利了!

没使用starter之前,我们通常要配置繁琐的dubbo.xml,以资金为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<description>Dubbo服务配置</description>

<dubbo:application name="ijz-finance" owner="yonyou"/>
<dubbo:protocol name="dubbo" port="${dubbo.protocol.port}" />
<dubbo:provider threads="${dubbo.provider.threads}" retries="0" timeout="${dubbo.provider.timeout}" filter="logcontext" />
<dubbo:consumer retries="0" timeout="${dubbo.provider.timeout}" filter="logcontext" check="false"/>
<dubbo:registry protocol="zookeeper" address="${zookeeper.addr}" />
<dubbo:monitor protocol="registry" />

<!-- ======== Dobbo Consumer Begin ======== -->
<!-- 附件上传 -->
<dubbo:reference id="fsAttachService"
interface="com.yyjz.icop.file.service.FsAttachService" url="${dubbo.url.file}" check="false"/>

<!-- 单据编码 -->
<dubbo:reference id="billCodeService"
interface="com.yyjz.icop.support.api.service.IBillCodeService" url="${dubbo.url.support}" check="false"/>

<!-- 获取参数dubbo接口 -->
<dubbo:reference id="regConfigService"
interface="com.yyjz.icop.support.api.service.IRegConfigAPIService"
url="${dubbo.url.support}" check="false" timeout="10000" />

<!--项目档案 -->
<dubbo:reference id="projectAPIService"
interface="com.yyjz.icop.share.api.service.ProjectAPIService" url="${dubbo.url.share}" check="false"/>

<!--设备档案 -->
<dubbo:reference id="deviceAPIService"
interface="com.yyjz.icop.share.api.service.DeviceAPIService" url="${dubbo.url.share}" check="false"/>

<!--物资档案 -->
<dubbo:reference id="materialAPIService"
interface="com.yyjz.icop.share.api.service.MaterialAPIService" url="${dubbo.url.share}" check="false"/>

<!--供应商档案 -->
<dubbo:reference id="supplierAPIService"
interface="com.yyjz.icop.share.api.service.SupplierAPIService" url="${dubbo.url.share}" check="false"/>

<!--客户档案 -->
<dubbo:reference id="customApiService"
interface="com.yyjz.icop.share.api.service.CustomApiService" url="${dubbo.url.share}" check="false"/>

<!--自定义档案-->
<dubbo:reference id="defdocAPIService"
interface="com.yyjz.icop.share.api.service.DefdocAPIService" url="${dubbo.url.share}" check="false"/>

<!--计量单位-->
<dubbo:reference id="UnitApiService"
interface="com.yyjz.icop.share.api.service.UnitApiService" url="${dubbo.url.share}" check="false"/>

<dubbo:reference id="accountPeriodService"
interface="com.yyjz.icop.share.api.service.AccountPeriodAPIService" url="${dubbo.url.share}" check="false"/>

<dubbo:reference id="userService"
interface="com.yyjz.icop.usercenter.service.IUserService" url="${dubbo.url.usercenter}" check="false" />

<!--公司信息-->
<dubbo:reference id="companyService"
interface="com.yyjz.icop.orgcenter.company.service.ICompanyService" url="${dubbo.url.orgcenter}" />

<!-- 组织中心 -->
<dubbo:reference id="orgCenterService"
interface="com.yyjz.icop.orgcenter.orgcenter.service.IOrgCenterService" url="${dubbo.url.orgcenter}" />

<!-- 消息 -->
<dubbo:reference id="pushMsgService"
interface="com.yonyou.message.center.service.PushMsgService" url="${dubbo.url.message}" check="false"/>
<dubbo:reference id="autoSendMsgService"
interface="com.yonyou.message.center.service.AutoSendMsgService" url="${dubbo.url.message}" check="false"/>

<!-- 预警service -->
<dubbo:reference id="ewTaskParamApiService"
interface="com.yyjz.icop.earlywarn.api.ewtask.service.IEwTaskParamApiService"
url="${dubbo.url.icop-earlywarn}" timeout="10000" check="false"/>

<!-- ijz-contract 公共合同 -->
<dubbo:reference id="commonContractApiService"
interface="com.yyjz.ijz.contract.api.cont.contractsimple.service.ICommonContractApiService"
url="${dubbo.url.ijz-contract}" timeout="10000" check="false"/>

<!-- ijz-contract 虚拟合同API服务 -->
<dubbo:reference id="virtualContractApiService"
interface="com.yyjz.ijz.contract.api.cont.contractsimple.service.IVirtualContractApiService"
url="${dubbo.url.ijz-contract}" timeout="10000" check="false"/>

<!-- ijz-contract 公共结算API服务 -->
<dubbo:reference id="commonSettleApiService"
interface="com.yyjz.ijz.contract.api.cont.contractsimple.service.ICommonSettleApiService"
url="${dubbo.url.ijz-contract}" timeout="10000" check="false"/>

<!-- ijz-tax [作废]发票开具 -->
<dubbo:reference id="invoiceIssueAPIService"
interface="com.yyjz.ijz.tax.api.taxsimple.saleinvoice.invoiceissue.service.IInvoiceIssueAPIService"
url="${dubbo.url.ijz-tax}" timeout="10000" check="false"/>

<!-- ijz-tax [作废]收票登记 -->
<dubbo:reference id="inInvoiceBusiAPIService"
interface="com.yyjz.ijz.tax.api.taxsimple.receiptinvoice.invoicecheckin.service.IInInvoiceBusiAPIService"
url="${dubbo.url.ijz-tax}" timeout="10000" check="false"/>

<!-- ijz-tax 发票开具/开票登记 -->
<dubbo:reference id="issueInvoiceAPIService"
interface="com.yyjz.ijz.tax.api.taxsimple.saleinvoice.issueinvoice.service.IIssueInvoiceAPIService"
url="${dubbo.url.ijz-tax}" timeout="10000" check="false"/>

<!-- ijz-tax 收票登记 -->
<dubbo:reference id="receiptInvoiceAPIService"
interface="com.yyjz.ijz.tax.api.taxsimple.receiptinvoice.receiptinvoice.service.IReceiptInvoiceAPIService"
url="${dubbo.url.ijz-tax}" timeout="10000" check="false"/>

<!-- icop-myproject 我的项目 -->
<dubbo:reference id="myProjectQueryService"
interface="com.yyjz.icop.myproject.api.service.IMyProjectQueryService"
url="${dubbo.url.icop-myproject}" timeout="10000" check="false"/>

<!-- ijz-budget 总预算 -->
<dubbo:reference id="totalBudgetAPIService"
interface="com.yyjz.ijz.budget.api.budgetsimple.totalbudget.service.ITotalBudgetAPIService"
url="${dubbo.url.ijz-budget}" timeout="10000" check="false"/>
<!-- ======== Dobbo Consumer _End_ ======== -->

<!-- ======== Dobbo Provider Begin ======== -->
<!-- 债权台账(ijz-contract 对外报量) -->
<dubbo:service ref="finAcctInService"
interface="com.yyjz.ijz.finance.api.finance.acct.service.IFinAcctInServiceAPI"/>

<!-- 债务台账(分包结算) -->
<dubbo:service ref="finAcctOutService"
interface="com.yyjz.ijz.finance.api.finance.acct.service.IFinAcctOutServiceAPI"/>

<!-- 资金公共API服务 -->
<dubbo:service ref="financeCommonAPIServiceImpl"
interface="com.yyjz.ijz.finance.api.common.service.IFinanceCommonAPIService"/>
<!-- ======== Dobbo Provider _End_ ======== -->
</beans>

使用starter之后,如需要对外暴露服务,只需在service上添加注解@Service,类似这样:

1
2
3
4
5
6
import com.alibaba.dubbo.config.annotation.Service;

@Service(interfaceClass = ActualCostApiService.class)
public class ActualCostServiceApiServiceImpl implements ActualCostApiService {
...
}

如需要引入外部服务,也只需在属性上添加注解@Reference,类似这样:

1
2
3
4
5
6
7
8
import com.alibaba.dubbo.config.annotation.Service;

@Service(interfaceClass = ActualCostApiService.class)
public class ActualCostServiceApiServiceImpl implements ActualCostApiService {
@Reference(url = "${dubbo.url.orgCenter}")
private ICompanyService companyService;
...
}

再也不用配置xml了,就是这么简单!

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。
利用SpringFox我们可以很快的将Swagger集成到Spring Boot项目中:

  1. 添加依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- api doc -->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    </dependency>
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    </dependency>
  2. 在Spring Boot应用启动类上添加注解@@EnableSwagger2
    1
    2
    3
    4
    5
    @EnableSwagger2
    @SpringBootApplication
    public class CostApplication extends SpringBootServletInitializer {
    ...
    }
  3. Controller类上添加相关文档注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    @Api(description = "其他费用")
    @UnifyReturn
    @RequestMapping("otherCost")
    @RestController
    public class OtherCostController {
    @Autowired
    private OtherCostService service;

    @ApiOperation(httpMethod = "POST", value = "保存其他费用")
    @PostMapping("save")
    public OtherCostVO save(@Valid @RequestBody OtherCostVO vo) {
    return StringUtils.isEmpty(vo.getId()) ? service.create(vo) : service.update(vo);
    }

    @ApiOperation(httpMethod = "POST", value = "删除其他费用")
    @PostMapping("delete")
    public void delete(@RequestBody List<OtherCostVO> voList) {
    service.delete(voList);
    }

    @ApiOperation(httpMethod = "GET", value = "查找其他费用")
    @GetMapping("queryDetail")
    public OtherCostVO findOne(String id) {
    return service.findById(id);
    }

    @ApiOperation(httpMethod = "POST", value = "分页查询其他费用")
    @PostMapping("queryList")
    public Page<OtherCostVO> queryList(@RequestBody QueryCondition condition) {
    return service.queryForPage(condition.getSearchText(), condition.getFilter(), condition.getPageable());
    }

    @ApiOperation(httpMethod = "GET", value = "生成单据编号")
    @GetMapping("generateCode")
    public String generateBillCode() {
    return service.generateBillCode();
    }

    @ApiOperation(httpMethod = "GET", value = "获取成本费用类别档案")
    @RequestMapping(value = "getCostTypes")
    public List<DefdocAPIBO> getCostTypes() {
    return service.getCostTypes();
    }

    @UnifyReturn(unify = false)
    @ApiOperation(httpMethod = "GET", value = "导出其他费用")
    @PostMapping("/export")
    public void exportExcel(HttpServletResponse response, @RequestBody ExportParams<OtherCostVO> params) {
    List<OtherCostVO> dataList;
    if (ExportDataScope.ALL.name().equalsIgnoreCase(params.getScope())) {
    QueryCondition condition = params.getCondition();
    Page<OtherCostVO> page = service.queryForPage(condition.getSearchText(), condition.getFilter(),
    PageRequest.of(0, 10000, condition.getSort()));
    dataList = page.getContent();
    } else {
    dataList = params.getData();
    }
    ExportUtils.exports(response, params.getFileName(), dataList, new ExportDefinitionBuilder<OtherCostVO>()
    .model(OtherCostVO.class)
    .simpleLabels(Arrays.asList("单据编号", "项目名称", "登记月份", "创建人", "合计金额"))
    .simpleProperties(Arrays.asList("billCode", "projectName", "period", "creator", "totalAmount"))
    .build());
    }

    @UnifyReturn(unify = false)
    @ApiOperation(httpMethod = "GET", value = "打印其他费用")
    @PostMapping("print")
    public JSONObject queryList(String id) {
    return PrintUtil.convertPrintData(service.findById(id), new PrintAttributeConvert() {
    @Override
    public void convert(JSONObject jsonObject) {
    }
    });
    }
    }
  4. 在vo类上添加相关注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @ApiModel(description = "其他费用")
    public class OtherCostVO extends BillVO {
    @ApiModelProperty("项目id")
    @NotNull(message = "项目不能为空")
    private String projectId; // 项目id
    @ApiModelProperty("项目id")
    private String projectName; // 项目名称
    @ApiModelProperty("登记月份id")
    @NotNull(message = "登记月份不能为空")
    private String periodId; // 登记月份
    @ApiModelProperty("登记月份")
    private String period; // 登记月份
    @NotNull(message = "合计金额不能为空")
    @ApiModelProperty("合计金额")
    private BigDecimal totalAmount; // 合计金额
    @ApiModelProperty("费用明细")
    @SublistNotEmpty(message = "费用明细不能为空")
    @Valid
    private List<OtherCostDetailVO> costDetails; // 费用明细
    ...
    }
    大功告成!启动项目后,访问地址http://{ip}:{port}/{projectname}/swagger-ui.html即可看到自动生成的API文档。
    下面以公有云成本)示例:
    API文档示例-公有云成本
    大家可以访问地址:https://cc.yonyouccs.com/ijz-cost-web/swagger-ui.html查看详细。

像必填、邮箱、最大值、不能重复、正则表达式等表单校验,我们前端都有做,其实后端也应该做的。利用@Valid等注解很容易实现,比如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@PostMapping("save")
public OtherCostVO save(@Valid @RequestBody OtherCostVO vo) {
return StringUtils.isEmpty(vo.getId()) ? service.create(vo) : service.update(vo);
}

public class OtherCostVO extends BillVO {
@NotNull(message = "项目不能为空")
private String projectId; // 项目id
private String projectName; // 项目名称
@NotNull(message = "登记月份不能为空")
private String periodId; // 登记月份
private String period; // 登记月份
@NotNull(message = "合计金额不能为空")
private BigDecimal totalAmount; // 合计金额
@SublistNotEmpty(message = "费用明细不能为空")
// @NotEmpty
@Valid
private List<OtherCostDetailVO> costDetails; // 费用明细

...
}

对于基本校验,spring都提供了相应注解,但有些特殊情况则需要我们自定义注解。比如我们系统中都是逻辑删除,这时我们如果要做子表非空校验,就不能再使用注解@NotEmpty了。那么如何创建自定义注解呢?
首先,创建注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 子表非空校验注解
*/
@Constraint(validatedBy = SublistNotEmptyValidator.class)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SublistNotEmpty {

String message() default "{org.hibernate.validator.constraints.NotEmpty.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

然后,创建注解解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 子表非空校验器
* 业务中采用逻辑删除,所以非空校验需要判断是否全部逻辑删除
*/
public class SublistNotEmptyValidator implements ConstraintValidator<SublistNotEmpty, List<? extends LogicDeleteAware>> {

@Override
public void initialize(SublistNotEmpty constraintAnnotation) {

}

@Override
public boolean isValid(List<? extends LogicDeleteAware> value, ConstraintValidatorContext context) {
return !CollectionUtils.isEmpty(value) && !value.stream().allMatch(LogicDeleteAware::isDeleted);
}
}

最后,只需要在要校验的字段上添加该注解就可以了,Spring会帮我们搞定的!(最上面的字段costDetails正是用到了该注解)
目前,除了子表非空@SublistNotEmpty注解,我还提供了子表不能重复注解@SublistNotRepeated。

由于后端要求接口返回值类型是JsonBackData,因此目前在每个controller中的方法里,我们都是按下面这种方式手动包装返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(value = "xx")
@ResponseBody
public JsonBackData xx(@RequestBody XX vo) {
JsonBackData back = new JsonBackData();
try {
back.setBackData(service.xx(vo));
back.setBackMsg("xx成功");
} catch (BusinessException e) {
back.setSuccess(false);
back.setBackMsg("xx失败:"+e.getMessage());
}
return back;
}

这样有两个弊端,一是重复编码,二是从自动生成的API文档中,我们不知道每个方法的返回值类型具体是什么。

方法执行分两种情况:正常返回和抛出异常。异常的统一处理我会在文章异常统一处理中再做介绍,下面只讲述如何利用aop来实现返回值类型统一。
先来看看添加aop之后,我们再怎么写controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@UnifyReturn
@RequestMapping("otherCost")
@RestController
public class OtherCostController {
@Autowired
private OtherCostService service;

@PostMapping("save")
public OtherCostVO save(@Valid @RequestBody OtherCostVO vo) {
return StringUtils.isEmpty(vo.getId()) ? service.create(vo) : service.update(vo);
}

@PostMapping("delete")
public void delete(@RequestBody List<OtherCostVO> voList) {
service.delete(voList);
}

@GetMapping("queryDetail")
public OtherCostVO findOne(String id) {
return service.findById(id);
}

@UnifyReturn(unify = false)
@PostMapping("print")
public JSONObject print(String id) {
return PrintUtil.convertPrintData(service.findById(id), new PrintAttributeConvert() {
@Override
public void convert(JSONObject jsonObject) {
}
});
}
}

如上,如果想要对某个controller中所有方法的返回值类型都做统一处理,只需在该controller类上添加注解@UnifyReturn;如果其中某个方法就要返回原始类型(比如上面的print方法),那么就在该方法上也添加注解@UnifyReturn,并覆盖注解的unify属性为false即可。

切面代码如下(UnifyResult就是JsonBackData,只是改了下类名):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 返回值类型统一切面
*/
@Component
@Aspect
public class UnifyResultAspect {
private MappingJackson2HttpMessageConverter converter;

public UnifyResultAspect(MappingJackson2HttpMessageConverter converter) {
this.converter = converter;
}

/**
* 切入点定义
*
* @param unifyReturn {@link UnifyReturn}
*/
@Pointcut(value = "@within(unifyReturn)", argNames = "unifyReturn")
public void pointcut(UnifyReturn unifyReturn) {
}

/**
* 后置通知
*
* @param joinPoint 链接点
* @param unifyReturn {@link UnifyReturn}
* @param result 返回值
* @throws IOException io异常
*/
@AfterReturning(pointcut = "pointcut(unifyReturn)", returning = "result", argNames = "joinPoint,unifyReturn,result")
public void unifyResult(JoinPoint joinPoint, UnifyReturn unifyReturn, Object result) throws IOException {
if (unifyReturn.unify()) {
// 如果方法上也添加了该注解,则覆盖类上的定义
Method method = ((MethodSignature) (joinPoint.getSignature())).getMethod();
UnifyReturn methodUnifyReturn = method.getAnnotation(UnifyReturn.class);
if (methodUnifyReturn == null || methodUnifyReturn.unify()) {
String message = this.getMessage(method);

UnifyResult backData = new UnifyResult.Builder()
.backData(result)
.backMsg(message)
.build();

// aop不能改变返回值类型
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
HttpOutputMessage outputMessage = new ServletServerHttpResponse(response);
converter.write(backData, MediaType.APPLICATION_JSON, outputMessage);
response.getOutputStream().close();
}
}
}

/**
* 返回提示消息
*
* @param method 执行的方法
* @return 提示消息
*/
private String getMessage(Method method) {
String message = null;
if (method.isAnnotationPresent(RequestMapping.class)) {
message = method.getAnnotation(RequestMapping.class).name();
} else if (method.isAnnotationPresent(GetMapping.class)) {
message = method.getAnnotation(GetMapping.class).name();
} else if (method.isAnnotationPresent(PostMapping.class)) {
message = method.getAnnotation(PostMapping.class).name();
} else if (method.isAnnotationPresent(PutMapping.class)) {
message = method.getAnnotation(PutMapping.class).name();
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
message = method.getAnnotation(DeleteMapping.class).name();
} else if (method.isAnnotationPresent(PatchMapping.class)) {
message = method.getAnnotation(PatchMapping.class).name();
}
return StringUtils.isEmpty(message) ? "操作成功" : message + "成功";
}
}

在文章返回值类型统一处理中,我介绍了怎样利用aop统一包装正常返回的值,其实异常情况也可以利用aop来实现。
不过Spring为异常统一处理提供了好几种其它的方式,我是使用注解@RestControllerAdvice和@ExceptionHandler来实现的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 异常统一处理类
* 异常也统一包装成UnifyResult对象
*/
@RestControllerAdvice
public class ExceptionAdvice {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionAdvice.class);

/**
* 处理spring校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<UnifyResult> handleException(MethodArgumentNotValidException e) {
return this.handle(new ValidateException(e));
}

/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<UnifyResult> handleException(Exception e) {
return this.handle(e);
}

private ResponseEntity<UnifyResult> handle(Exception e) {
String message = StringUtils.isEmpty(e.getMessage()) ? "系统异常,请联系管理员!" : e.getMessage();
LOGGER.error(message, e);

UnifyResult backData = new UnifyResult.Builder().success(false).backMsg(message).build();

HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json;charset=UTF-8");

return new ResponseEntity<>(backData, headers, HttpStatus.OK);
}
}

嗯,就这么简单,一个类就完成了异常统一处理。上面对Spring校验异常做了额外处理,是为了返回给前端更友好的提示。

另外关于异常处理,我认为项目中这地方可以调整下:

  • service层的好多接口都有显示抛出BusinessException(继承自RuntimeException),这个是没必要的。就像我们知道系统随处可能抛出RuntimeException一样,service层中也随处可能抛出BusinessException,但系统可没哪处有显示抛出RuntimeException,因为这是默认的。
  • 很多dubbo service接口也显示抛出了BusinessException(继承Exception),比如CompanyService,但这里的BusinessException是checked exception。我的建议是一般不要使用这类异常,如果一定要使用它,请明确告知开发者当捕获到该类异常时要如何处理(具体的处理方式,不是捕获完又抛出去)。另外要注意当遇到checked exception时,spring事务默认是不会回滚的。
  • 目前我们抛出异常,有些地方是直接抛出,有些地方是调用ExceptionUtils类,直接抛出即可。

增删改查是业务基本功能,目前大家都是按照一种固定的模式开发,但这种开发模式我认为有些地方不是很合理。

三层架构

三层架构,即表现层、业务逻辑层和数据访问层。假设有Example实体,现在我们要实现该实体的增删改查,按照目前的开发模式,UML类图是这样的:
三层架构UML类图(改造前)
看到类图,大家第一感觉是什么?类太多了!我们只是想要做个简单的增删改查,却要创建15个类!(其中表现层2个,业务逻辑层7个,数据访问层6个,黄色的属于公共类不计)。
15个类什么概念?公有云合约有8大类合同,那么就得创建120个类,再加上合同对应的8大类结算,就是240个类了,简直是类爆炸。那么要怎么精简呢?

  • 移除所有的扩展类(extend结尾)
    目前约定了所有相对于扩展类的非扩展类都是固定的且不应修改,有新功能就在添加到extend类上。但是首先非扩展类里的操作对于业务来说可能不是必需甚至是不应该提供的,其次非扩展类里的操作可能并不能满足实际业务需求(实际还是会修改),最后这种约定也是脆弱的,开发者想修改还是能轻易修改。
  • 2个service(接口加实现)
    目前service的写和查做了分离,另外还有专门的审批service,但是从业务对象上划分它们都是相同的,可以合并成1个。
  • 2个dao(接口加实现)
    无论是JdbcTemplate,还是EntityManger,又或者是ElasticSearchTemplate、RedisTemplate等等,它们都没有按操作类型(保存|修改|删除)的维度再去划分类,所有数据访问层操作放1个类里面就可以了。
    以下是我认为比较合理的三层架构:
    三层架构UML类图(改造后)

如上,忽略黄色公共类不计,现在再做增删改查只需创建6个类了(因为使用了Spring-Data-Jpa,所以有3个dao类)。

代码生成

上面提到,目前做一个简单的增删改查要创建15个类,但是却没有人反映过繁琐,原因就是使用了模板引擎来生成代码。代码生成的好处是不需要手写代码,那么它的缺点呢?
比如我要在所有activity里面添加新的公共规则,那么得去所有已生成的activity里面一个个添加;比如我要修改所有service中分页查询的内部实现,那么也得去所有已生成的service里面一个个修改。有人会说直接再重新生成不就可以了吗?可是万一我们已经对生成的文件做了部分修改呢?
可见,代码生成是一种伪封装,看似提供了便利,却不利于扩展和修改。在Spring Boot官网,明确指出Spring Boot的一项特性就是“Absolutely no code generation”,这是有原因的。
代码生成并非是完全不好,主要是看怎么使用,上面的场景我们就应该考虑封装而不是代码生成,Don’t Repeat Yourself !

持久层封装 VS (Spring-Data-Jpa & JdbcTemplate)

可能是出于性能原因考虑,目前持久层没用到Spring-Data-Jpa/Hibernate,而是对JdbcTemplate做了层封装,对此我有几点疑问:

  • 是否遇到过因为使用Jpa导致性能很差且不能优化的场景?
  • 是否有必要为了一点性能的提升(可能某些场景下Jpa性能更高)而放弃高效率开发?
  • 既然使用JdbcTemplate,为什么又按Hibernate操作实体的方式来对其做封装?
  • 既然未使用Jpa,为什么还要使用Jpa注解?为什么不自定义注解?
  • 何不考虑使用Mybatis?

在改造后的框架中,我并没有再去封装持久化操作,而是直接使用Spring-Data-Jpa & JdbcTemplate。使用Jpa的原因是现阶段主要是快速完成业务开发,而我们的业务对象之间的关系可能是复杂的,在复杂对象的级联操作上Jpa有优势。使用JdbcTemplate的原因则是有些关联查询确实用JdbcTemplate要灵活些。

IcopRule VS AOP

实体保存前后,可能需要做其它的操作,比如保存后同时保存附件,比如删除前校验是否能删除。目前系统中都是通过在dao(Activity)中添加IcopRule来实现的。
首先,像保存附件和校验等业务操作,我认为应该提到service层处理;其次,共性操作直接采用aop方式处理即可。

导入导出可说是表单必备功能,目前系统中已有好几个工具类,但这些工具类考虑的都不够全面(尤其是在导出方面),它们或者不能满足于某些特定场景,或者不具备可扩展性,又或者实现得不够优雅。
结合我在实际开发中的经验,我认为导入应该满足以下需求:

  • 值转换
  • 必填校验
  • 并行
  • 批量

导出相对复杂一些,需要满足以下更多需求:

  • 值转换
  • 特殊值标识
  • 批量
  • 主子表
  • 海量数据
  • 并行
  • 分页查询
  • 打包
  • 复杂表头
  • 自定义样式
  • 基于模板

为此我开发了一套类库:ijz-export,它基本实现了以上各类需求。下面将依次介绍如何利用该库来做导入/导出。

导入

简单的

1
2
3
4
5
6
7
List<People> dataList = ExcelImportUtils.imports(new FileInputStream("E:\\人员表.xlsx"),
new ImportDefinition.Builder<People>()
// .sheetName("人员表") // 未指定时默认读取第一个sheet
// .startIndex(3) // 未指定时默认从第一行开始读取
.modelClass(People.class) // 接收导入数据的类
.simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio")) // excel中每列依次对应的属性名
.build());

最简单的导入只需要设置modelClass和simpleColumns。如果需要读取指定sheet,则调用sheetName;如果需要从指定行开始读取数据,则调用startIndex。

值转换

场景:People类中有int类型字段sex(1表示男,2表示女),但在excel中的sex对应列的值就是字符串“男”、“女”,那么如何自动给sex字段赋相应的值?

1
2
3
4
5
6
7
8
9
10
11
12
List<People> dataList = ExcelImportUtils.imports(new FileInputStream("E:\\人员表.xlsx"),
new ImportDefinition.Builder<People>()
.modelClass(People.class)
.columns(Arrays.asList(
new ColumnDefinition.Builder().name("name").build(), // 不需要转换
new ColumnDefinition.Builder().name("birthday").valueFunction(new DateImportFunction()).build(), // 调用预置日期导入转换类,将字符串转换成日期(默认yyyy-mm-dd格式)
new ColumnDefinition.Builder().name("sex").valueFunction((value) -> value.equals("男") ? 1 : 2).build(), // 自定义转换
new ColumnDefinition.Builder().name("isSingle").valueFunction(new BooleanIntegerImportFunction()).build(), // 调用预置布尔导入转换类,将“是”、“否”分别转换成1、2
new ColumnDefinition.Builder().name("isMarried").valueFunction(new BooleanImportFunction()).build(), // 调用预置布尔导入转换类,将“是”、“否”分别转换成true、false
new ColumnDefinition.Builder().name("ratio").valueFunction(new PercentageImportFunction()).build() // 调用预置百分比导入转换类,将百分数转换成小数
))
.build());

目前预置了DateImportFunction、BooleanImportFunction、BooleanIntegerImportFunction和PercentageImportFunction四个值导入转换类,对应的也预置了DateExportFunction、BooleanExportFunction、BooleanIntegerExportFunction和PercentageExportFunction这四个值导出转换类。

必填校验

1
2
3
4
5
6
7
8
9
10
11
12
List<People> dataList = ExcelImportUtils.imports(new FileInputStream("E:\\人员表.xlsx"),
new ImportDefinition.Builder<People>()
.modelClass(People.class)
.columns(Arrays.asList(
new ColumnDefinition.Builder().name("name").required(true).build(),
new ColumnDefinition.Builder().name("birthday").required(true).build(),
new ColumnDefinition.Builder().name("sex").required(true).build(),
new ColumnDefinition.Builder().name("isSingle").build(),
new ColumnDefinition.Builder().name("isMarried").required(false).build(),
new ColumnDefinition.Builder().name("ratio").required(false).build()
))
.build());

默认是非必填,设置必填后如果excel中有值为空,就会抛出ImportException,并提示哪行哪列值设置了必填却为空。

并行

1
2
3
4
5
6
7
List<People> dataList = ExcelImportUtils.imports(new FileInputStream("E:\\人员表.xlsx"),
new ImportDefinition.Builder<People>()
.modelClass(People.class)
.simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried"))
.parallel(true)
// .threadNumber(50) // 未指定时线程数为可用处理器数量
.build());

默认是串行导入,如果设置为并行导入,建议线程数根据实际情况设置大些(导入是io密集型任务)。

批量

1
2
3
4
5
6
7
8
9
10
11
12
13
List<List<?>> dataList = ExcelImportUtils.batchImports(new FileInputStream("E:\\人员表.xlsx"),
Arrays.asList(
new ImportDefinition.Builder<People>()
// .sheetName("人员表1")
.modelClass(People.class)
.simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried"))
.build(),
new ImportDefinition.Builder<People>()
// .sheetName("人员表2")
.modelClass(People.class)
.simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried"))
.build()
));

未指定sheetName时,将按顺序读取sheet数据。

看到这里,相信大家也发现了,要定制各种导入,重点在于如何构建ImportDefinition,而通过调用ImportDefinition.Builder类,可以很方面的构建ImportDefinition。不止是ImportDefinition,类库中各类Definition都有对应的Builder。(对,正是运用了建造者设计模式。)

导出

简单的

1
2
3
4
5
6
7
8
9
ExcelExportUtils.export(new FileOutputStream("E:\\人员表.xlsx"),
buildData(100), // 构建要导出数据列表
new ExportDefinition.Builder()
// .sheetName("人员表") // 未指定时默认为sheet1
// .simpleTitle("人员表") // 未指定时没有标题
// .title(new TitleDefinition.Builder().name("人员表").height(2).build()) // 自定义标题高度
// .simpleLabels(Arrays.asList("姓名", "出生年月", "性别", "是否单身", "是否已婚", "百分比")) // 未指定时没有labels
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.build());

最简单的导出只需设置simpleProperties。标题默认高度为一行,如果要指定标题高度,就调用title方法。如果要导出labels,注意labels要与properties一致。

值转换

1
2
3
4
5
6
7
8
9
10
11
12
ExcelExportUtils.export(new FileOutputStream("E:\\人员表.xlsx"),
buildData(100),
new ExportDefinition.Builder()
.properties(Arrays.asList(
new PropertyDefinition.Builder().name("name").build(), // 不需要转换
new PropertyDefinition.Builder().name("birthday").valueFunction(new DateExportFunction()).build(), // 调用预置日期导出转换类,将日期转换成字符串(默认yyyy-mm-dd格式)
new PropertyDefinition.Builder().name("sex").valueFunction((value) -> (int) value == 1 ? "男" : "女").build(), // 自定义转换
new PropertyDefinition.Builder().name("isSingle").valueFunction(new BooleanIntegerExportFunction()).build(), // 调用预置布尔导出转换类,将1、2分别转换成“是”、“否”
new PropertyDefinition.Builder().name("isMarried").valueFunction(new BooleanExportFunction()).build(), // 调用预置布尔导入转换类,将true、false分别转换成“是”、“否”
new PropertyDefinition.Builder().name("ratio").valueFunction(new PercentageExportFunction()).build()// 调用预置百分比导入转换类,将小数转换成百分数
))
.build());

特殊值标识

场景:将人员数据中已经结婚的用红色字体突出显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ExcelExportUtils.export(new FileOutputStream("E:\\人员表.xlsx"),
buildData(100),
new ExportDefinition.Builder()
.properties(Arrays.asList(
new PropertyDefinition.Builder().name("name").build(),
new PropertyDefinition.Builder().name("birthday").build(),
new PropertyDefinition.Builder().name("sex").build(),
new PropertyDefinition.Builder().name("isSingle").build(),
new PropertyDefinition.Builder().name("isMarried").valueFunction(new BooleanExportFunction())
.styleFunction((wb, value) -> {
if ((boolean) value) {
CellStyle style = wb.createCellStyle();
Font dataFont = wb.createFont();
dataFont.setColor(HSSFColor.HSSFColorPredefined.RED.getIndex());
style.setFont(dataFont);
return style;
}
return new DefaultStyleFunctionFactory().getDataStyleFunction().apply(wb);
})
.build(),
new PropertyDefinition.Builder().name("ratio").valueFunction(new PercentageExportFunction()).build()
))
.build());

批量

1
2
3
4
5
6
7
8
9
10
11
12
13
ExcelExportUtils.batchExport(new FileOutputStream("E:\\人员表.xlsx"),
Arrays.asList(
buildData(100),
buildData(100)
),
Arrays.asList(
new ExportDefinition.Builder()
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.build(),
new ExportDefinition.Builder()
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.build()
));

主子表

首先要确定主子表导出到excel中的展现形式,我的想法是做成类似EasyUI中SubGrid这样的:
主子表导出示例
由于实现起来较为复杂,且目前未遇到实际需求,所以暂时没做。后面如需要导出主子表,可以转变为批量导出。

海量数据

1
2
3
4
5
6
7
ExcelExportUtils.export(new FileOutputStream("E:\\人员表.xlsx"),
buildData(2000000),
new ExportDefinition.Builder()
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.windowSize(100) // 内存中数据量达到100,就刷新到磁盘,默认100
.compress(false) // 临时文件非常大时,可以考虑压缩临时文件,但会引起性能损失,默认不压缩
.build());

使用poi导出海量数据,需要注意两点:

  • 使用SXSSFWorkbook
  • 每张sheet中最多只能导出1048576行数据,数据量超出该范围时,将数据导出到多个sheet里

以上两点在内部实现中已经考虑到了,另外根据实际情况,可以设置内存中数据量达到多少时就刷新到磁盘、以及是否需要压缩临时文件。

并行

上面说过,导出使用了SXSSFWorkbook,因此当内存中数据达到一定数量时,这些数据就会刷新到磁盘,当后面的数据刷新到磁盘,就不能回头去写前面的数据了,这样就不能使用多线程来同时往多个行写数据。当然可以设置不刷新数据到磁盘,但这样又会大大降低导出性能。因此目前未实现并行导出。

分页查询

场景:要导出1000w条数据,如果一次性都查出来放内存中,很可能会导出内存溢出,此时就需要考虑分页查询,数据一部分一部分的写到磁盘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ExcelExportUtils.export(new FileOutputStream("E:\\人员表.xlsx"),
new QueryModel(
new PageQuery(
0, // 起始id值
"id", // id字段名
1000, // 每次查询数量
null // 查询条件
), // 分页查询对象
pageQuery -> {
/*
方法体内代码是模拟数据库查询
*/
List<People> dataList = buildData(1000);
dataList.get(dataList.size() - 1).setId(pageQuery.getId() + pageQuery.getPageSize());

if (pageQuery.getId() == 2000000) {
dataList.remove(dataList.size() - 1);
dataList.get(dataList.size() - 1).setId(pageQuery.getId() + pageQuery.getPageSize() - 1);
}
return dataList;
} // 分页查询函数
), // 查询模型对象
new ExportDefinition.Builder()
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.build());

此时就不是传递dataList而是改为传递QueryModel了。PageQuery对象里之所以是id而不是pageIndex,是因为海量数据分页查询时,主键应设置为long且保持自增,不然查询会有性能问题,且可能会重复查询数据。

打包

场景:当sheet中数据量超过100w时,打开excel是很慢的,可以考虑把数据分批导出到多个excel,最后压缩成zip导出。

1
2
3
4
5
6
ExcelExportUtils.packageExport(new FileOutputStream("E:\\人员表.zip"),
buildData(1000000),
new ExportDefinition.Builder()
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.packageSize(200000) // 默认每个excel的最大存储数据量为200000
.build());

复杂表头

场景:很多时候我们导出的表头是这种简单的:
简单表头示例
但也会遇到需要合并单元格等复杂需求,比如这样:
复杂表头示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ExcelExportUtils.export(new FileOutputStream("E:\\人员表.xlsx"), 
buildData(100),
new ExportDefinition.Builder()
.labels(Arrays.asList(
new LabelDefinition.Builder().name("a").children(
Arrays.asList(
new LabelDefinition.Builder().name("b").children(
Arrays.asList(
new LabelDefinition.Builder().name("姓名").build(),
new LabelDefinition.Builder().name("出生年月").build()
)
).build(),
new LabelDefinition.Builder().name("性别").build()
)
).build(),
new LabelDefinition.Builder().name("c").children(
Arrays.asList(
new LabelDefinition.Builder().name("是否单身").build(),
new LabelDefinition.Builder().name("是否已婚").build(),
new LabelDefinition.Builder().name("百分比").build()
)
).build()
))
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.build());

唯一需要注意的是,最底层的labels应与properties一一对应。

自定义样式

1
2
3
4
5
6
ExcelExportUtils.export(new FileOutputStream("E:\\人员表.xlsx"),
buildData(100),
new ExportDefinition.Builder()
.simpleProperties(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio"))
.styleFunctionFactory(new DefaultStyleFunctionFactory())
.build());

DefaultStyleFunctionFactory实现了StyleFunctionFactory接口,类结构如下:
主题样式类结构
利用工厂模式,可以很方面扩展主题样式。

基于模板

场景:实际业务中,可能需要导出各种复杂的excel,当封装的库不能实现时,就可以考虑使用jxls基于模板来导出excel。

1
2
3
4
5
ExcelExportUtils.exportByTemplate(
new FileOutputStream("E:\\template.xlsx"),
"template.xlsx", // 模板地址
null // 数据
);

jxls的缺点在于,当数据量很大时性能会有问题,且每处导出都需要创建模板略显麻烦。

在库依赖&库设计方面,目前系统在四个方面存在不足:依赖库版本一致性、库与库松耦合、库内部高内聚、库依赖冗余。

依赖库版本一致性

意义

含义:多个库都依赖了某个库,被依赖的库在多个库中的版本号应该保持一致。

之所以强调一致性,是因为如果不一致,则在系统集成时很容易遇到版本冲突问题。
假设有以下场景:

  • 库C有版本1和版本2
  • 版本1中有类A,类A中方法X
  • 版本2中新增了类B,且在类A中新增了方法Y,移除了方法X
  • 库A依赖了库C的版本1
  • 库B依赖了库C的版本2
  • 项目M同时依赖了库A和库B

那么假如最终在项目M里:

  • 只有库C的版本2,但某处调用了库C的版本1中类A的方法X,就会报方法X找不到异常
  • 只有库C的版本1,但某处调用了库C的版本2中类A的方法Y,就会报方法Y找不到异常
  • 只有库C的版本1,但某处引用了库C的版本2中类B,就会报类B找不到异常
  • 同时存在库C的版本1和版本2,由于ClassLoader只会加载相同的class一次,因此也会出现以上问题

至于最终在项目M里采用的时哪个版本,则是根据以下规则:

  • 声明优先原则
  • 路径近者优先原则
  • 排除原则
  • 版本锁定原则

我们当然可以根据以上规则来限定最终依赖的版本,但是我们并不清楚会间接依赖到哪些冲突的库,即使知道了每个库都去显示声明也会很繁琐,而且当项目中同时调用了不同版本中互不兼容的方法时,还是会遇到问题。

现有问题

以资金项目为例,它依赖的很多库都存在多个版本,如下图所示:
maven依赖版本不一致示例(一)
maven依赖版本不一致示例(二)
maven依赖版本不一致示例(三)
maven依赖版本不一致示例(四)
maven依赖版本不一致示例(五)
maven依赖版本不一致示例(六)
maven依赖版本不一致示例(七)

看到这有人会问了,不是版本不一致就会有问题吗,怎么这么多jar版本不一致,但却没发现啥问题呢?版本冲突问题我遇到过好几次了,印象较深的一次就是由于spring-data-jpa版本不一致而导致系统启动异常。现在之所以没出现问题,我想有两点原因:一是我们系统里很多jar虽然存在多个版本,但这些版本之间差别不大(通过版本号看出来),二是因为各版本间有冲突的类或方法我们并没有调用。

解决方案

项目中各依赖库版本不一致,根本原因在于没有全局定义库版本号。我们可以利用maven依赖传递机制,单据创建maven项目:ijz-boot-dependencies,该项目中只有一个pom文件,所有可能被用到的库的版本号都预先定义在pom文件中,下面是pom文件具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
<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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.3.RELEASE</version>
</parent>

<groupId>com.ijz.boot</groupId>
<artifactId>ijz-boot-dependencies</artifactId>
<version>1.0.0-RELEASE</version>
<packaging>pom</packaging>

<properties>
<!-- ijz starters -->
<dubbox-spring-boot-starter.version>1.0.0-RELEASE</dubbox-spring-boot-starter.version>
<bill-spring-boot-starter.version>1.0.0-RELEASE</bill-spring-boot-starter.version>
<shiro-spring-boot-starter.version>1.0.0-RELEASE</shiro-spring-boot-starter.version>

<!-- ijz framework -->
<ijz-export.version>1.0.0-RELEASE</ijz-export.version>

<!-- icop -->
<icop-config.version>1.5.0-RELEASE</icop-config.version>
<icop-monitor-client.version>1.5.1-RELEASE</icop-monitor-client.version>
<logstash-logback-encoder.version>5.3</logstash-logback-encoder.version>

<!-- icop api -->
<icop-orgcenter-api.version>1.5.19</icop-orgcenter-api.version>
<icop-share-api.version>1.9.17.4-SNAPSHOT</icop-share-api.version>
<icop-support-api.version>1.5.1-RELEASE</icop-support-api.version>
<icop-file-api.version>1.5.2-RELEASE</icop-file-api.version>
<icop-myproject-api.version>0.1.0-SNAPSHOT</icop-myproject-api.version>

<!-- ijz api -->
<ijz-contract-api.version>0.0.1-SNAPSHOT</ijz-contract-api.version>
<ijz-cost-api.version>0.0.1-SNAPSHOT</ijz-cost-api.version>
<ijz-finance-api.version>1.0.23-SNAPSHOT</ijz-finance-api.version>
<ijz-tax-api.version>1.0.21-SNAPSHOT</ijz-tax-api.version>
<ijz-budget-api.version>1.0.7-SNAPSHOT</ijz-budget-api.version>
<ijz-material-api.version>0.0.1-SNAPSHOT</ijz-material-api.version>

<!-- third party dependencies -->
<!-- dubbo相关 -->
<dubbo.version>2.8.4</dubbo.version>
<io-netty.version>3.10.6.Final</io-netty.version>
<zkclient.version>0.1</zkclient.version>

<servlet-api.version>4.0.1</servlet-api.version>
<springfox.verdion>2.9.2</springfox.verdion>
<fastjson.version>1.2.56</fastjson.version>

<!-- poi相关 -->
<poi.version>4.0.1</poi.version>
<jxls.version>2.5.1</jxls.version>
<jxls-poi.version>1.1.0</jxls-poi.version>

<!-- shiro相关 -->
<shiro-spring.version>1.4.0</shiro-spring.version>
<iuap-auth.version>1.5.5</iuap-auth.version>
<iuap-utils.version>3.1.0-RELEASE</iuap-utils.version>
<icop-core.version>1.5.9</icop-core.version>
<icop-context.version>1.5.17</icop-context.version>
<esapi.version>2.1.0.1</esapi.version>

<!-- project -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>

<!-- plugins -->
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
<maven-war-plugin.version>3.2.2</maven-war-plugin.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.ijz.spring.boot</groupId>
<artifactId>dubbox-spring-boot-starter</artifactId>
<version>${dubbox-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.ijz.spring.boot</groupId>
<artifactId>bill-spring-boot-starter</artifactId>
<version>${bill-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.ijz.spring.boot</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>${shiro-spring-boot-starter.version}</version>
</dependency>

<dependency>
<groupId>com.ijz.framework</groupId>
<artifactId>ijz-export</artifactId>
<version>${ijz-export.version}</version>
</dependency>

<dependency>
<groupId>com.yonyou.construction.icop</groupId>
<artifactId>icop-config</artifactId>
<version>${icop-config.version}</version>
</dependency>
<dependency>
<groupId>com.yonyou.construction.icop</groupId>
<artifactId>icop-monitor-client</artifactId>
<version>${icop-monitor-client.version}</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>${logstash-logback-encoder.version}</version>
</dependency>

<dependency>
<groupId>com.yyjz</groupId>
<artifactId>icop-orgcenter-api</artifactId>
<version>${icop-orgcenter-api.version}</version>
<exclusions>
<exclusion>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>icop-share-api</artifactId>
<version>${icop-share-api.version}</version>
<exclusions>
<exclusion>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</exclusion>
<exclusion>
<groupId>com.yyjz</groupId>
<artifactId>icop-exception</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>icop-support-api</artifactId>
<version>${icop-support-api.version}</version>
<exclusions>
<exclusion>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</exclusion>
<exclusion>
<groupId>com.yyjz</groupId>
<artifactId>icop-exception</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>icop-file-api</artifactId>
<version>${icop-file-api.version}</version>
<exclusions>
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yyjz.icop</groupId>
<artifactId>icop-myproject-api</artifactId>
<version>${icop-myproject-api.version}</version>
</dependency>

<dependency>
<groupId>com.yyjz</groupId>
<artifactId>ijz-contract-api</artifactId>
<version>${ijz-contract-api.version}</version>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>ijz-cost-api</artifactId>
<version>${ijz-cost-api.version}</version>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>ijz-finance-api</artifactId>
<version>${ijz-finance-api.version}</version>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>ijz-tax-api</artifactId>
<version>${ijz-tax-api.version}</version>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>ijz-budget-api</artifactId>
<version>${ijz-budget-api.version}</version>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>ijz-material-api</artifactId>
<version>${ijz-material-api.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>${io-netty.version}</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>${zkclient.version}</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-api.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.verdion}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.verdion}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls</artifactId>
<version>${jxls.version}</version>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-poi</artifactId>
<version>${jxls-poi.version}</version>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<dependency>
<groupId>com.yonyou.iuap</groupId>
<artifactId>iuap-auth</artifactId>
<version>${iuap-auth.version}</version>
</dependency>
<dependency>
<groupId>com.yonyou.iuap</groupId>
<artifactId>iuap-utils</artifactId>
<version>${iuap-utils.version}</version>
<exclusions>
<exclusion>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>icop-core</artifactId>
<version>${icop-core.version}</version>
<!-- json-lib和spring-webmvc(这个也应该排除,但是没有spring-web)之外,其他都排除掉 -->
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</exclusion>
<exclusion>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</exclusion>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</exclusion>
<exclusion>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
</exclusion>
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
<exclusion>
<groupId>org.owasp.esapi</groupId>
<artifactId>esapi</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</exclusion>
<exclusion>
<groupId>org.logback-extensions</groupId>
<artifactId>logback-ext-spring</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>com.googlecode.log4jdbc</groupId>
<artifactId>log4jdbc</artifactId>
</exclusion>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<groupId>io.springside</groupId>
<artifactId>springside-redis</artifactId>
</exclusion>
<exclusion>
<groupId>com.yonyou.iuap</groupId>
<artifactId>iuap-securitylog-rest-sdk</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<exclusion>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.jetty.aggregate</groupId>
<artifactId>jetty-webapp</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jsp</artifactId>
</exclusion>
<exclusion>
<groupId>org.csource</groupId>
<artifactId>fastdfs_client</artifactId>
</exclusion>
<exclusion>
<groupId>io.springside</groupId>
<artifactId>springside-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
</exclusion>
<exclusion>
<groupId>com.yonyou.iuap</groupId>
<artifactId>iuap-auth</artifactId>
</exclusion>
<exclusion>
<groupId>com.yyjz</groupId>
<artifactId>icop-database</artifactId>
</exclusion>
<exclusion>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</exclusion>
<exclusion>
<groupId>com.yyjz</groupId>
<artifactId>icop-dubbo-filter</artifactId>
</exclusion>
<exclusion>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yyjz</groupId>
<artifactId>icop-context</artifactId>
<version>${icop-context.version}</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>com.yyjz</groupId>
<artifactId>icop-cache</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.owasp.esapi</groupId>
<artifactId>esapi</artifactId>
<version>${esapi.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>${maven-war-plugin.version}</version>
</plugin>
</plugins>
</build>

<!-- 从maven 私服拉取库 -->
<repositories>
<repository>
<id>icop-nexus</id>
<url>https://maven.yonyouccs.com/nexus/content/groups/public/</url>
<snapshots>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>

<!-- 发布到maven私服 -->
<distributionManagement>
<repository>
<id>icop-release</id>
<url>https://maven.yonyouccs.com/nexus/content/repositories/icop-release</url>
</repository>
<snapshotRepository>
<id>icop-snapshot</id>
<url>https://maven.yonyouccs.com/nexus/content/repositories/icop-snapshot</url>
</snapshotRepository>
</distributionManagement>
</project>

细心的朋友会发现,该项目继承自spring-boot-dependencies,一是为了避免重复定义大量第三方库的版本号,二来与spring boot保持一致,因为后端框架就是基于Spring Boot。

其他项目引入有以下两种方式引入ijz-boot-dependencies:

  • 继承
    1
    2
    3
    4
    5
    <parent>
    <groupId>com.ijz.boot</groupId>
    <artifactId>ijz-boot-dependencies</artifactId>
    <version>1.0.0-RELEASE</version>
    </parent>
  • import
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>com.ijz.boot</groupId>
    <artifactId>ijz-boot-dependencies</artifactId>
    <version>1.0.0-RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>
    引入之后,项目中就不需要再指定各个库的版本号了:
    1
    2
    3
    4
    5
    6
    7
    8
    <dependencies>
    <!-- 依赖的某个库 -->
    <dependency>
    <groupId>xx</groupId>
    <artifactId>xx</artifactId>
    <!-- <version>x.x</version> --> <!-- 不需要再指定版本号了 -->
    </dependency>
    </dependencies>

适配icop

由于目前所有web项目都需要继承icop-parent-pom,所以只能采用import方式引入,于是我们可以再创建maven项目:ijz-boot-starter-parent,该项目同样只有一个pom文件,这个pom既继承了icop-parent-pom,又依赖了ijz-boot-dependencies,下面是pom具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.yonyouccs</groupId>
<artifactId>icop-parent-pom</artifactId>
<version>1.6.2</version>
</parent>

<groupId>com.ijz.boot</groupId>
<artifactId>ijz-boot-starter-parent</artifactId>
<version>1.0.0-RELEASE</version>
<packaging>pom</packaging>

<properties>
<!-- 覆盖父类中默认版本号 -->
<icop-config.version>1.5.0-RELEASE</icop-config.version>
<logback.version>1.2.3</logback.version>
<jackson.version>2.9.8</jackson.version>
<javassist.version>3.24.1-GA</javassist.version>
</properties>

<dependencies>
<!-- 覆盖父类中定义的版本 -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.ijz.boot</groupId>
<artifactId>ijz-boot-dependencies</artifactId>
<version>1.0.0-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- 覆盖父类中默认版本号 -->
<dependency>
<groupId>com.yonyou.construction.icop</groupId>
<artifactId>icop-config</artifactId>
<version>${icop-config.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javassist.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<!-- 从maven 私服拉取库 -->
<repositories>
<repository>
<id>icop-nexus</id>
<url>https://maven.yonyouccs.com/nexus/content/groups/public/</url>
<snapshots>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>
</project>

如此一来,web项目只需继承ijz-boot-starter-parent,便既兼容icop,又引入了版本一致性。(库还是继承ijz-boot-dependencies

库与库松耦合

意义

含义:库与库之间,尽量使其独立存在。

模块与模块之间依赖过多,会带来维护和扩展上的不便。例如模块A和模块B都直接依赖模块C,这样当模块C发生改变时,模块A和模块B可能都需要同步修改。如果修改时没有理清耦合关系,那么带来的后果可能是灾难性的。
那要怎么减小模块间的耦合呢?面向接口编程!例如下图,电器与插座之间是低耦合的关系,就算替换了不同的插座,电器依然可以正常的工作,因为电器都实现了相同的接口。
低耦合示例

现有问题

目前好多类库耦合都过于紧密。例如bpm回调过程中需要回写审批人,审批人是直接调用icop-context库中的的UserContext类获取的,这样就直接依赖了icop-context,如果后面我们更换了上下文库,又或icop-context库自身做了修改,那么bpm回调代码也需要做相应的更改。

解决方案

耦合问题不像版本一致性,它不是改下pom文件就能解决的,而是需要我们在类库设计时具体分析。以获取上下文为例,我们可以参考spring-data-jpa的设计思路:
spring-data-jpa提供了审计功能,如果我们在实体字段上添加了@CreatedBy或@LastModifiedBy注解,那么当实体被创建或修改时,就会自动将相应字段赋值为当前用户。但spring-data-jpa并没有依赖spring-security库来获取当前用户信息,而是提供了AuditorAware接口,用户信息是由我们在该接口的getCurrentAuditor方法中返回的,这样就达到了和spring-security库解耦的目的。

库内部高内聚

意义

含义:封装良好的类库应该恰如其分的只提供一类功能。

在6大设计原则中有单一职责:一个类,应该只有一个引起它变化的原因。类如此,库也应如此,可以理解为类是最小的库,库是大的类群。遵循该原则可以使库的复杂度降低,可读性提高,变更引起的风险降低。

现有问题

库icop-pubapp-platform,它里面即包含持久化,又包含导入导出和打印,还做了es数据同步等。

解决方案

拆分,比如上面的icop-pubapp-platform,就可以拆分成4个:持久化、导入导出、打印、es数据同步。

库依赖冗余

意义

含义:一个库不应该依赖它不需要的库,库的依赖应尽量的少。

如果库过于臃肿,不仅会导致应用启动时间加长,也会带来潜在的风险。

现有问题

最典型的就是库icop-core,它的设计初衷是作为一个基础库供其它项目使用,基础库应该尽量简洁,只封装一些核心的功能,但是它却引入了大量没用到的库:
依赖库(一)
依赖库(二)
再就是一些api库,本应是很简单的只提供接口,却要引入一堆库:
icop-myproject-api依赖库

解决方案

对于没有用到的依赖,移除掉。