背景
后台系统中产品模块,需要支持对已上架产品相关服务的快速修改,即:无需走审批流程。类似下图:
image.png
此次需要新增题库功能,查看代码。发现存在许多冗余逻辑及无用代码,遂,开始优化!
分析
首先明确本次优化的目标
- 使用设计模式,提取重复的类和方法,消除重复代码
- 清理不用的代码
- 针对后续可能存在的类似需求,提高扩展性,可读性
确立目标后,我们开始分析一波代码,经分析发现,这部分功能主要包含下列三步:
- 参数校验(包含通用、特殊参数校验两部分)
- 入库,即:插入服务信息
- 一致性处理(移除缓存和发送 MQ 消息)
感觉有点模板方法模式和工厂模式的味道,下面我们先了解下这两种设计模式
简单工厂模式
先来看下简单工厂模式的定义:
?
简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。
她对应的结构图
image.png
可以看到包含三种角色:
- Factory:核心工厂类,负责实现创建所有产品实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。在工厂类中提供了静态的工厂方法 factoryMethod ,返回类型为抽象产品类型 Product
- Product:抽象产品角色,工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法。能提高系统灵活性
- ConcreteProduct:具体产品角色,每个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。
模板方法模式
一如既往的先看看定义
?
模板方法模式(Template Method Pattern):定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法模式是一种类行为型模式。
她的结构图:
image.png
包含的角色
- AbstractClass:抽象类,定义了一系列基本操作(Primitive Operations)。它们可以是具体的,也可以是抽象的。每个基本操作对应算法的一个步骤,在子类中可以重定义或实现这些步骤。同时定义一个模板方法(Template Method),即:算法的框架
- ConcreteClass:抽象类的具体子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作
实现方案
我们先定义模板接口
public interface ProductServiceConfig {
/**
* 配置服务信息模板方法
*
* @param reqVO 请求vo。每个功能传参不一致,用泛型代替
* @param operator 操作人
* @param company 平台
*/
void configServiceInfo(T reqVO, String operator, Integer company);
}
我们想将通用的校验逻辑放到模板类中实现,而不同功能的请求参数又不同,该怎么办呢?
这里我们自定义了注解 CommonAttribute
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CommonAttribute {
}
然后在每个具体请求类上标记需要校验的字段,以产品协议请求对象举例
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value = "产品协议配置请求对象", description = "ProductAgreementReqVO")
public class ProductAgreementReqVO extends ArkSerializable {
@CommonAttribute
@NotNull(message = "产品id不能为空")
@ApiModelProperty(value = "产品id")
private Integer productId;
@CommonAttribute
@NotNull(message = "产品版本id不能为空")
@ApiModelProperty(value = "产品版本id")
private Integer productVersionId;
@NotNull(message = "产品sku id不能为空")
@ApiModelProperty(value = "产品sku id")
private Integer productSkuId;
@ApiModelProperty(value = "产品协议id")
@NotNull(message = "产品协议id不能为空")
private Integer containAgreementId;
@ApiModelProperty(value = "协议填写配置 1下单前 2 下单后")
@NotNull(message = "协议填写配置不能为空")
private Short agreementSelectPeriod;
}
最后在模板类中定义反射方法 getCommonAttributes 获取通用校验参数。模板类如下
@Slf4j
public abstract class AbstractProductServiceConfigTemplate implements ProductServiceConfig {
protected AbstractProductServiceConfigTemplate() {
}
@Override
public final void configServiceInfo(T reqVO, String operator, Integer company) {
// 参数校验
checkSpecificParam(reqVO);
checkCommonParam(reqVO);
// 插入服务信息
modifyDbInfo(reqVO, operator);
// 一致性处理(移除缓存发送消息)
consistencyProcessing(reqVO, company);
}
/**
* 默认的通用参数校验,可以在子类中选择性覆盖
*
* @param reqVO T
*/
@SuppressWarnings("DuplicatedCode")
protected void checkCommonParam(T reqVO) {
Map map = getCommonAttributes(reqVO);
if (MapUtil.isEmpty(map)) {
log.info("map is empty");
return;
}
Integer productId = MapUtil.getQuietly(map, "productId", Integer.class, null);
Integer productVersionId = MapUtil.getQuietly(map, "productVersionId", Integer.class, null);
if (Objects.isNull(productId) || Objects.isNull(productVersionId)) {
log.info("productId or productVersionId is empty, map: {}", map);
return;
}
}
protected void checkSpecificParam(T reqVO) {
}
protected abstract void modifyDbInfo(T reqVO, String operator);
protected void consistencyProcessing(T reqVO, Integer company) {
}
/**
* 反射获取通用校验参数
*
* @param reqVO 请求vo
* @return Map
*/
protected Map getCommonAttributes(T reqVO) {
Map commonAttributes = new HashMap<>();
Class> clazz = reqVO.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 过滤掉不需要的字段
if (field.isAnnotationPresent(CommonAttribute.class)) {
try {
field.setAccessible(true);
commonAttributes.put(field.getName(), field.get(reqVO));
} catch (IllegalAccessException e) {
log.error("反射获取通用参数错误", e);
throw new ProductException(ProductErrorCodeEnum.SYSTEM_ERROR.getErrorCode(), ProductErrorCodeEnum.SYSTEM_ERROR.getErrorMsg());
}
}
}
return commonAttributes;
}
/**
* 发送消息
*
* @param key 重试次数key
* @param timeout 重试次数超时时间
* @param productServiceConfigTypeEnum 消息tag
* @param message 消息
* @param productVersionId 版本id
*/
protected void send(String key, Integer timeout, ProductServiceConfigTypeEnum productServiceConfigTypeEnum,
ProductServiceConfigMessage message, Integer productVersionId) {
try {
// 具体发送逻辑,实现
} catch (Exception e) {
log.error("发送MQ消息异常 e:", e);
}
}
}
以协议功能为例,看下具体的模板实现类
@Slf4j
@Service("agreement-service-config")
public class ProductAgreementServiceImpl extends AbstractProductServiceConfigTemplate implements ProductAgreementService {
@Override
protected void checkSpecificParam(ProductAgreementReqVO reqVO) {
// 此功能的特殊插入校验
}
@Transactional(rollbackFor = Exception.class)
public void modifyDbInfo(ProductAgreementReqVO reqVO, String operator) {
// 具体入库逻辑
}
@Override
protected void consistencyProcessing(ProductAgreementReqVO reqVO, Integer company) {
// 删除缓存逻辑
// 发消息
super.send(RedisKeyEnum.PRODUCT_AGREEMENT_CONFIG_MQ_SEND.getKey() + ":" + company,
RedisKeyEnum.PRODUCT_AGREEMENT_CONFIG_MQ_SEND.getTimeout(),
ProductServiceConfigTypeEnum.AGREEMENT_SERVICE,
message, reqVO.getProductVersionId());
}
}
到这里,模板方法模式相关的代码已经实现。然后,定义工厂类
@Component
@SuppressWarnings({"rawtypes"})
public class ProductServiceConfigFactory {
@Resource
private Map productServiceConfigTemplateMap;
public AbstractProductServiceConfigTemplate getInstance(ServiceConfigTypeEnum serviceConfigTypeEnum) {
if (Objects.isNull(serviceConfigTypeEnum)) {
return null;
}
return productServiceConfigTemplateMap.get(serviceConfigTypeEnum.getServiceId());
}
}
这里利用 Spring 相关特性,将抽象类相关的实现类自动加载到 productServiceConfigTemplateMap 中,并且定义 ServiceConfigTypeEnum 替换了 if else
@AllArgsConstructor
@Getter
public enum ServiceConfigTypeEnum {
// ...
AGREEMENT(2, "agreement-service-config"),
;
private final Integer type;
private final String serviceId;
}
然后看下控制层代码
@Slf4j
@Api(value = "product-agreement-controller", tags = "产品协议配置相关接口")
@RestController
@RequestMapping(value = "/v1/product/agreement")
public class ProductAgreementController {
@Resource
private ProductServiceConfigFactory productServiceConfigFactory;
@SuppressWarnings({"unchecked", "rawtypes"})
@ApiOperation(value = "添加产品协议配置", httpMethod = "POST", notes = "添加产品协议配置")
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public Result insert(@ApiParam("添加产品协议配置请求对象") @Valid @RequestBody ProductAgreementReqVO reqVO,
@Ignore BindingResult result, @RequestParam String operator, @RequestParam Integer company) {
log.info("添加产品协议配置请求参数:{}", JSON.toJSONString(reqVO));
if (result.hasErrors()) {
return ValidateParam.errJSONResult(result);
}
AbstractProductServiceConfigTemplate instance = productServiceConfigFactory.getInstance(ServiceConfigTypeEnum.AGREEMENT);
instance.configServiceInfo(reqVO, operator, company);
return Result.success();
}
}
最后看下类图
AbstractProductServiceConfigTemplate.jpg
Spring 中的工厂模式与模板方法模式
工厂模式
BeanFactory
?
BeanFactory 是 Spring 的核心容器接口,是一个工厂,负责管理、创建 bean 对象。BeanFactory 根据传入的唯一标识(bean名称)获取 bean 对象,它在启动阶段会读取配置文件(xml 文件、Java配置类、注解),将配置信息转换成 BeanDefinition 对象,并注册到 BeanFactory 中。
根据上面这段文字,分析出它的工作原理主要包含三步:
- 读取配置文件
- 创建BeanDefinition
- 注册BeanDefinition
下面我们依次看下源码实现
读取配置文件并创建 BeanDefinition
spring 启动阶段会使用 ApplicationContext 或者具体实现类读取配置文件。根据配置文件的不同使用不同的类读取
- xml 配置:当配置文件在类路径下使用 ClassPathXmlApplicationContext,在文件系统下使用 FileSystemXmlApplicationContext
- Java 配置或者注解配置:AnnotationConfigApplicationContext
ps:我们以 AnnotationConfigApplicationContext 为例,看下源码。
测试类及输出如下
public class AnnotationConfigApplicationContextDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 注册Bean
context.register(MyBean.class);
// 扫描包
context.scan("com.xcs.spring");
// 打印Bean定义
for (String beanDefinitionName : context.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
// 输出:
// beanDefinitionName = org.springframework.context.annotation.internalConfigurationAnnotationProcessor
// beanDefinitionName = org.springframework.context.annotation.internalAutowiredAnnotationProcessor
// beanDefinitionName = org.springframework.context.annotation.internalCommonAnnotationProcessor
// beanDefinitionName = org.springframework.context.event.internalEventListenerProcessor
// beanDefinitionName = org.springframework.context.event.internalEventListenerFactory
// beanDefinitionName = myBean
// beanDefinitionName = myController
// beanDefinitionName = myRepository
// beanDefinitionName = myService
}
}
@Controller
public class MyController {
}
@Service
public class MyService {
}
@Repository
public class MyRepository {
}
public class MyBean {
}
debug截图如下
image.png
根据堆栈看出通过方法org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents 读取配置文件并创建 BeanDefinition。
其源码如下:
private Set scanCandidateComponents(String basePackage) {
// 创建list 存储扫描到的组件
Set candidates = new LinkedHashSet<>();
try {
// 构建搜索路径
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
// 获取资源
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
// 检查元数据是否表示一个候选组件。
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
// 如果是,则创建一个 ScannedGenericBeanDefinition 对象,并设置资源来源。
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回结果
return candidates;
}
注册 BeanDefinition
debug 发现注册方法最终走到 org.springframework.beans.factory.support.DefaultListableBeanFactory#registerBeanDefinition 类中,堆栈如下图所示:
image.png
部分源码
private final Map beanDefinitionMap = new ConcurrentHashMap<>(256);
// ...
// 同步地修改`beanDefinitionMap`,并将`beanName`添加到`beanDefinitionNames`列表中
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
removeManualSingletonName(beanName);
}
}
FactoryBean
image.png
说了 BeanFactory 就不得不提到和它特别相似的兄弟 FactoryBean,但其实他们一点关系没有。FactoryBean 是一种特殊的 bean,如果一个对象实现了 FactoryBean,注册到 IOC 容器后,如果调用 getBean 方法获取到的其实是 org.springframework.beans.factory.FactoryBean#getObject 方法返回的结果
看下案例
public class MyFactoryBean implements FactoryBean {
@Override
public MyBean getObject() throws Exception {
// 创建MyBean实例,并执行必要的初始化逻辑
MyBean myBean = new MyBean();
myBean.init();
return myBean;
}
@Override
public Class> getObjectType() {
return MyBean.class;
}
@Override
public boolean isSingleton() {
return true; // 或 false,根据需要
}
}
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean myBean = context.getBean("myBean", MyBean.class);
可以总结下它的用途:
?
创建代理对象,在代理对象中加入额外的初始化或业务逻辑,比如:缓存等。
写到这里,突然想到了前段时间刚刚看到的 mybatis 与 spring 整合相关的文章,其中
下面是原生 mybatis 的使用方法
String resource = "mybatis-config.xml";
// 读取 MyBatis 的配置文件。mybatis-config.xml 为 MyBatis 的全局配置文件,用于配置数据库连接信息。
InputStream resourceAsStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 通过Builder获取构建SqlSessionFactory(读取mybatis-config.xml文件配置)
// 构造会话工厂。通过 MyBatis 的环境配置信息构建会话工厂 SqlSessionFactory。
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
// 开启Session
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.findByUserId(1);
若我们想要使用 spring 对 mybatis 进行管理,达到通过@Autowired将Mapper注入进来便可以直接使用的效果,首先就要干掉SqlSessionFactory的维护。
mybatis 社区提供的 mybatis-spring-1.3.2.jar 是如何实现的呢?简单看下源码:
image.png
- 实现了FactoryBean,意味着它一定有一个getObject()方法,用于返回交给 spring 管理的实例
- 实现了 InitializingBean,这就意味着在这个 bean 的初始化时,spring 会回调它的afterPropertiesSet()方法。
嘿嘿,看下和我们这次聊的 FactoryBean 相关的内容吧!
// 构建 sqlSessionFactory,为 null 调用 afterPropertiesSet 方法创建
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
public void afterPropertiesSet() throws Exception {
// ...
this.sqlSessionFactory = buildSqlSessionFactory();
}
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
Configuration configuration;
// 省略代码为构建上面的 configuration 对象
// ...
// 核心:使用 SqlSessionFactoryBuilder 构建 SqlSessionFactory:
return this.sqlSessionFactoryBuilder.build(configuration);
}
然后,我们在 spring-boot 添加下面的配置类,自动配置 MyBatis 的 SqlSessionFactory 和事务管理器:
@Configuration
@AutoConfigureAfter(DruidAutoConfiguration.class) // 确保在 DruidAutoConfiguration 之后执行此配置
@ConditionalOnClass({MapperAutoConfiguration.class}) // 仅当类路径中存在 MapperAutoConfiguration 时才启用此配置
@AutoConfigureBefore(MapperAutoConfiguration.class) // 确保在此配置之前执行 MapperAutoConfiguration
public class TkMybatisAutoConfiguration {
@Resource // 通过名称注入 DataSource
private DataSource dataSource;
/**
* 创建并配置 SqlSessionFactory。
*
* @return 配置好的 SqlSessionFactory
* @throws Exception 如果配置过程中出现问题,则抛出异常
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); // 创建 SqlSessionFactoryBean 实例
sqlSessionFactoryBean.setDataSource(dataSource); // 设置数据源
// 创建路径匹配资源模式解析器
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternPatternResolver();
// 设置映射文件的位置,此处匹配所有 mapper 目录下的 *.Mapper.xml 文件
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/**/*Mapper.xml"));
// 设置拦截器,此处为自定义的拦截器
sqlSessionFactoryBean.setPlugins(new Interceptor[]{new CatMybatisPlugin()});
// 返回 SqlSessionFactory 实例
return sqlSessionFactoryBean.getObject();
}
/**
* 创建并配置事务管理器。
*
* @return 平台事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager() {
// 创建并返回 DataSourceTransactionManager 实例
return new DataSourceTransactionManager(dataSource);
}
}
模板方法模式
我们以 org.springframework.jdbc.core.JdbcTemplate 为例分析
image.png
execute 方法定义了数据库操作的基本流程,包括连接的获取、PreparedStatement 的创建、SQL 语句的执行以及资源的释放。
public T execute(ConnectionCallback action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(obtainDataSource());
try {
// Create close-suppressing Connection proxy, also preparing returned Statements.
Connection conToUse = createConnectionProxy(con);
return action.doInConnection(conToUse);
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
String sql = getSql(action);
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("ConnectionCallback", sql, ex);
}
finally {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
具体的 SQL 语句的设置和结果的处理则由传递给 execute 方法的 PreparedStatementCallback 实现类来完成。
在自己内部实现了 createConnectionProxy 方法
protected Connection createConnectionProxy(Connection con) {
return (Connection) Proxy.newProxyInstance(
ConnectionProxy.class.getClassLoader(),
new Class>[] {ConnectionProxy.class},
new CloseSuppressingInvocationHandler(con));
}
参考链接
- 巨佬超牛逼的 spring 源码分析仓库:spring-read[1]
- MyBatis 基本工作原理[2]
- 深入源码理解Spring整合MyBatis原理[3]
原文:https://juejin.cn/post/7410964371247104035
作者:doubleZ
Reference
[1]https://github.com/xuchengsheng/spring-reading: https://github.com/xuchengsheng/spring-reading
[2]https://www.cnblogs.com/steven-note/p/16952464.html: https://www.cnblogs.com/steven-note/p/16952464.html
[3]https://www.cnblogs.com/deepSleeping/p/15070404.html: https://www.cnblogs.com/deepSleeping/p/15070404.html