百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

巧用工厂与模板方法模式,优化业务代码

ztj100 2025-02-07 19:55 11 浏览 0 评论

背景

后台系统中产品模块,需要支持对已上架产品相关服务的快速修改,即:无需走审批流程。类似下图:

image.png

此次需要新增题库功能,查看代码。发现存在许多冗余逻辑及无用代码,遂,开始优化!

分析

首先明确本次优化的目标

  • 使用设计模式,提取重复的类和方法,消除重复代码
  • 清理不用的代码
  • 针对后续可能存在的类似需求,提高扩展性,可读性

确立目标后,我们开始分析一波代码,经分析发现,这部分功能主要包含下列三步:

  1. 参数校验(包含通用、特殊参数校验两部分)
  2. 入库,即:插入服务信息
  3. 一致性处理(移除缓存和发送 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 进行管理,达到通过@AutowiredMapper注入进来便可以直接使用的效果,首先就要干掉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

#记录我的9月生活#

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

相关推荐

利用navicat将postgresql转为mysql

导航"拿来主义"吃得亏自己动手,丰衣足食...

Navicat的详细教程「偷偷收藏」(navicatlite)

Navicat是一套快速、可靠并价格适宜的数据库管理工具,适用于三种平台:Windows、macOS及Linux。可以用来对本机或远程的MySQL、SQLServer、SQLite、...

Linux系统安装SQL Server数据库(linux安装数据库命令)

一、官方说明...

Navicat推出免费数据库管理软件Premium Lite

IT之家6月26日消息,Navicat推出一款免费的数据库管理开发工具——NavicatPremiumLite,针对入门级用户,支持基础的数据库管理和协同合作功能。▲Navicat...

Docker安装部署Oracle/Sql Server

一、Docker安装Oracle12cOracle简介...

Docker安装MS SQL Server并使用Navicat远程连接

...

Web性能的计算方式与优化方案(二)

通过前面《...

网络入侵检测系统之Suricata(十四)——匹配流程

其实规则的匹配流程和加载流程是强相关的,你如何组织规则那么就会采用该种数据结构去匹配,例如你用radixtree组织海量ip规则,那么匹配的时候也是采用bittest确定前缀节点,然后逐一左右子树...

使用deepseek写一个图片转换代码(deepnode处理图片)

写一个photoshop代码,要求:可以将文件夹里面的图片都处理成CMYK模式。软件版本:photoshop2022,然后生成的代码如下://Photoshop2022CMYK批量转换专业版脚...

AI助力AUTOCAD,生成LSP插件(ai里面cad插件怎么使用)

以下是用AI生成的,用AUTOLISP语言编写的cad插件,分享给大家:一、将单线偏移为双线;;;;;;;;;;;;;;;;;;;;;;单线变双线...

Core Audio音频基础概述(core 音乐)

1、CoreAudioCoreAudio提供了数字音频服务为iOS与OSX,它提供了一系列框架去处理音频....

BlazorUI 组件库——反馈与弹层 (1)

组件是前端的基础。组件库也是前端框架的核心中的重点。组件库中有一个重要的板块:反馈与弹层!反馈与弹层在组件形态上,与Button、Input类等嵌入界面的组件有所不同,通常以层的形式出现。本篇文章...

怎样创建一个Xcode插件(xcode如何新建一个main.c)

译者:@yohunl译者注:原文使用的是xcode6.3.2,我翻译的时候,使用的是xcode7.2.1,经过验证,本部分中说的依然是有效的.在文中你可以学习到一系列的技能,非常值得一看.这些技能不单...

让SSL/TLS协议流行起来:深度解读SSL/TLS实现1

一前言SSL/TLS协议是网络安全通信的重要基石,本系列将简单介绍SSL/TLS协议,主要关注SSL/TLS协议的安全性,特别是SSL规范的正确实现。本系列的文章大体分为3个部分:SSL/TLS协...

社交软件开发6-客户端开发-ios端开发验证登陆部分

欢迎订阅我的头条号:一点热上一节说到,Android客户端的开发,主要是编写了,如何使用Androidstudio如何创建一个Android项目,已经使用gradle来加载第三方库,并且使用了异步...

取消回复欢迎 发表评论: