如何使用MyBatis的plugin插件实现多租户的数据过滤?

article/2025/10/31 10:09:16

如何实现多租户数据隔离

在中台服务或者saas服务中,当多租户入驻时,如何保证不同租户的数据隔离性呢?通常的解决方法有三种,分别如下:

  1. 一个租户一个独立数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本也高。
  2. 所有租户共享数据库,但一个租户一个数据库表。这种方案为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离,每个数据库可以支持更多的租户数量。
  3. 所有租户共享数据库,共享同一个数据库表,不同的租户数据通过租户的标识区分。这种方案共享程度最高、隔离级别最低。

通常为了降低成本,一般会选择第三种方案。这时,应该如何快速的实现多租户的数据隔离呢?在每一个查询语句中都添加上不同租户的标识语句么?基于mybatis提供的plugin插件,可以实现多租户过滤语句的横切逻辑,类似于AOP,让我们的业务代码从数据隔离的逻辑中抽离出来,专注于业务开发。

基于MyBatis插件plugin的实现

在MyBatis 中,通过其plugin插件机制,可以实现类似于AOP的横切逻辑编程,允许你在映射语句执行过程中的某一点进行拦截调用。定义了Interceptor 接口,实现指定方法的拦截,官网示例代码如下:

// ExamplePlugin.java
@Intercepts({@Signature(type= Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {private Properties properties = new Properties();public Object intercept(Invocation invocation) throws Throwable {// implement pre processing if need// 拦截执行方法之前的逻辑处理Object returnObject = invocation.proceed();// implement post processing if need// 拦截执行方法之后的逻辑处理return returnObject;}public void setProperties(Properties properties) {this.properties = properties;}
}

实现多租户的拦截过程中,通过对query操作进行拦截,实现了多租户过滤的如下功能:

  1. 可以动态设置多租户查询的开关,支持单个或者多个查询值的查询。
  2. 可以自定义多租户过滤的数据库字段,自定义查询数据库的别名设置,在多个JOIN关联查询中设置过滤的查询条件。
  3. 可以自定义多租户过滤查询的查询条件,例如,单个查询值的相等条件过滤,多个查询值的IN条件过滤

其大致的流程如下:

MultiTenancyQueryInterceptor多租户过滤器

在实现中,定义MultiTenancyQueryInterceptor实现Interceptor实现如上流程的逻辑。其源码如下:

@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MultiTenancyQueryInterceptor implements Interceptor {private static final String WHERE_CONDITION = " where ";private static final String AND_CONDITION = " and ";/*** 条件生成Factory*/private final ConditionFactory conditionFactory;/*** 查询条件生成工厂缓存*/private volatile Map<Class<? extends MultiTenancyQueryValueFactory>, MultiTenancyQueryValueFactory> multiTenancyQueryValueFactoryMap;/*** 多租户属性*/@Getter@Setterprivate MultiTenancyProperties multiTenancyProperties;public MultiTenancyQueryInterceptor() {this.conditionFactory = new DefaultConditionFactory();this.multiTenancyProperties = new MultiTenancyProperties();this.multiTenancyQueryValueFactoryMap = new HashMap<>();}@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object[] args = invocation.getArgs();MappedStatement mappedStatement = (MappedStatement) args[0];// 获取多租户查询注解MultiTenancy multiTenancy = this.getMultiTenancyFromMappedStatementId(mappedStatement.getId());if (!this.isMatchMultiTenancy(multiTenancy)) {log.info("{} is not match multi tenancy query!", mappedStatement.getId());return invocation.proceed();}// 验证多租户查询数据库字段if (StringUtils.isBlank(this.multiTenancyProperties.getMultiTenancyQueryColumn())) {log.error("property {} is invalid!", JSON.toJSONString(this.multiTenancyProperties));return invocation.proceed();}Object parameter = args[1];BoundSql boundSql = mappedStatement.getBoundSql(parameter);String originSql = boundSql.getSql();// 验证数据库表查询别名if (!this.matchPreTableName(originSql, multiTenancy.preTableName())) {log.info("pre table name {} is not matched sql {}!", multiTenancy.preTableName(), originSql);return invocation.proceed();}// 获取多租户查询条件Object queryObject;if (Objects.isNull(queryObject = this.getQueryObjectFromMultiTenancyQueryValueFactory(multiTenancy.multiTenancyQueryValueFactory()))) {log.error("parameter {} is invalid!", JSON.toJSONString(parameter));return invocation.proceed();}// 默认使用In 条件ConditionFactory.ConditionTypeEnum conditionTypeEnum = ConditionFactory.ConditionTypeEnum.IN;String conditionType;if (StringUtils.isNotBlank(conditionType = this.multiTenancyProperties.getConditionType())) {try {conditionTypeEnum = ConditionFactory.ConditionTypeEnum.valueOf(conditionType.toUpperCase());} catch (Exception e) {log.warn("invalid condition type {}!", conditionType);}}MultiTenancyQuery multiTenancyQuery = MultiTenancyQuery.builder().multiTenancyQueryColumn(this.multiTenancyProperties.getMultiTenancyQueryColumn()).multiTenancyQueryValue(queryObject).conditionType(conditionTypeEnum).preTableName(multiTenancy.preTableName()).build();String multiTenancyQueryCondition = this.conditionFactory.buildCondition(multiTenancyQuery);String newSql = this.appendWhereCondition(originSql, multiTenancyQueryCondition);// 使用反射替换BoundSql的sql语句Reflections.setFieldValue(boundSql, "sql", newSql);// 把新的查询放到statement里MappedStatement newMs = copyFromMappedStatement(mappedStatement, parameterObject -> boundSql);args[0] = newMs;return invocation.proceed();}private Object getQueryObjectFromMultiTenancyQueryValueFactory(Class<? extends MultiTenancyQueryValueFactory> multiTenancyQueryValueFactoryClass) {if (Objects.isNull(multiTenancyQueryValueFactoryClass)) {return null;}MultiTenancyQueryValueFactory multiTenancyQueryValueFactory = this.multiTenancyQueryValueFactoryMap.get(multiTenancyQueryValueFactoryClass);if (Objects.nonNull(multiTenancyQueryValueFactory)) {return multiTenancyQueryValueFactory.buildMultiTenancyQueryValue();}synchronized (this.multiTenancyQueryValueFactoryMap) {try {multiTenancyQueryValueFactory = multiTenancyQueryValueFactoryClass.newInstance();multiTenancyQueryValueFactoryMap.putIfAbsent(multiTenancyQueryValueFactoryClass, multiTenancyQueryValueFactory);return multiTenancyQueryValueFactory.buildMultiTenancyQueryValue();} catch (Exception e) {return null;}}}/*** 从MappedStatement 的id属性获取多租户查询注解 MultiTenancy** @param id MappedStatement 的id属性* @return MultiTenancy注解*/private MultiTenancy getMultiTenancyFromMappedStatementId(String id) {int lastSplitPointIndex = id.lastIndexOf(".");String mapperClassName = id.substring(0, lastSplitPointIndex);String methodName = id.substring(lastSplitPointIndex + 1, id.length());Class mapperClass;try {mapperClass = Class.forName(mapperClassName);Method[] methods = mapperClass.getMethods();for (Method method : methods) {if (method.getName().equals(methodName)) {return method.getAnnotation(MultiTenancy.class);}}} catch (ClassNotFoundException e) {e.printStackTrace();}return null;}private String appendWhereCondition(String originSql, String condition) {if (StringUtils.isBlank(originSql) || StringUtils.isBlank(condition)) {return originSql;}String[] sqlSplit = originSql.toLowerCase().split(WHERE_CONDITION.trim());// 没有查询条件if (this.noWhereCondition(sqlSplit)) {return originSql + WHERE_CONDITION + condition;}// 包含查询条件,添加到第一个查询条件的位置else {String sqlBeforeWhere = sqlSplit[0];String sqlAfterWhere = sqlSplit[1];return sqlBeforeWhere + WHERE_CONDITION + condition + AND_CONDITION + sqlAfterWhere;}}private boolean noWhereCondition(String[] sqlSplit) {return ArrayUtils.isNotEmpty(sqlSplit) && 1 == sqlSplit.length;}private boolean matchPreTableName(String sql, String preTableName) {if (StringUtils.isBlank(preTableName)) {return true;} else {return StringUtils.containsIgnoreCase(sql, preTableName);}}private boolean isMatchMultiTenancy(MultiTenancy multiTenancy) {return Objects.nonNull(multiTenancy)&& multiTenancy.isFiltered();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {Object multiTenancyQueryColumn;if (Objects.nonNull(multiTenancyQueryColumn = properties.get(MULTI_TENANCY_QUERY_COLUMN_PROPERTY))) {multiTenancyProperties.setMultiTenancyQueryColumn(multiTenancyQueryColumn.toString());}Object conditionType;if (Objects.nonNull(conditionType = properties.get(CONDITION_TYPE_PROPERTY))) {multiTenancyProperties.setConditionType(conditionType.toString());}}private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());builder.resource(ms.getResource());builder.fetchSize(ms.getFetchSize());builder.statementType(ms.getStatementType());builder.keyGenerator(ms.getKeyGenerator());if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {builder.keyProperty(ms.getKeyProperties()[0]);}builder.timeout(ms.getTimeout());builder.parameterMap(ms.getParameterMap());builder.resultMaps(ms.getResultMaps());builder.resultSetType(ms.getResultSetType());builder.cache(ms.getCache());builder.flushCacheRequired(ms.isFlushCacheRequired());builder.useCache(ms.isUseCache());return builder.build();}
}

在使用时,使用MultiTenancy注解标识需要进行多租户过滤,可以配置数据库查询的别名,查询条件的生成逻辑,以及多租户过滤开关。配置MultiTenancyProperties属性可以设置需要被拦截的数据库字段,以及拦截字段的查询条件。

以单个商品查询为例,根据商品id查询商品,多租户查询过滤的字段为商品code,定义数据库查询的别名为g,代码示例如下:

/*** mybatis插件配置**/    
//MybatisPlusConfig.class    
@Bean
public MultiTenancyQueryInterceptor multiTenancyQueryInterceptor() {MultiTenancyQueryInterceptor multiTenancyQueryInterceptor = new MultiTenancyQueryInterceptor();MultiTenancyProperties multiTenancyProperties = new MultiTenancyProperties();// 数据库查询字段multiTenancyProperties.setMultiTenancyQueryColumn("code");multiTenancyQueryInterceptor.setMultiTenancyProperties(multiTenancyProperties);return multiTenancyQueryInterceptor;}public interface GoodsMapper extends BaseMapper<Goods> {/*** 使用MultiTenancy注解,标识需要进行多租户查询过滤* preTableName 设置数据库查询别名,multiTenancyQueryValueFactory设置多租户查询条件*/@MultiTenancy(preTableName = "g", multiTenancyQueryValueFactory = UserMultiTenancyQueryValueFactory.class)GoodsDetailBo getGoodsById(Long id);
}/*** MultiTenancyQuery 查询参数设置**/  
@Test
public void testMultiTenancyQuery() {Long goodsId = 1253217755332722792l;// 只传入商品id查询条件GoodsDetailBo goodsDetailBo = goodsMapper.getGoodsById(goodsId);Assert.assertNotNull(goodsDetailBo);}// GoodsMapper.class映射的mybatis sql 语句
<select id="getGoodsById" resultMap="goodsDetailResultMap">SELECTg.id AS 'id',g.name AS 'name',g.code AS 'code',g.size AS 'size',g.weight AS 'weight'FROM boutique_goods g// 只有根据id查询条件WHERE g.id=#{id}</select>

 测试运行结果,能够查询到id为1253217755332722792l的商品,并且查询的sql为包含id和code的组合查询条件,结果如图:

解析多租户过滤注解MultiTenancy与配置参数MultiTenancyProperties

定义多租户过滤注解MultiTenancy,可以设置执行过滤操作的开关,数据库查询别名,查询条件生成规则,以及多租户查询开关。在mybatis自定义的plugin拦截器,拦截query方法,通过MappedStatement 的id获取到定义在mapper层的MultiTenancy注解。其源码如下:

/*** 多租户查询注解**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MultiTenancy {/*** 是否可以过滤*/boolean isFiltered() default true;/*** 数据库表前缀名*/String preTableName();/*** 过滤条件查询值Factory*/Class<? extends MultiTenancyQueryValueFactory> multiTenancyQueryValueFactory();
}/**
* 从MappedStatement 的id属性获取多租户查询注解 MultiTenancy
*
* @param id MappedStatement 的id属性
* @return MultiTenancy注解
*/
// MultiTenancyQueryInterceptor.class
private MultiTenancy getMultiTenancyFromMappedStatementId(String id) {int lastSplitPointIndex = id.lastIndexOf(".");String mapperClassName = id.substring(0, lastSplitPointIndex);String methodName = id.substring(lastSplitPointIndex + 1, id.length());Class mapperClass;try {mapperClass = Class.forName(mapperClassName);Method[] methods = mapperClass.getMethods();for (Method method : methods) {if (method.getName().equals(methodName)) {return method.getAnnotation(MultiTenancy.class);}}} catch (ClassNotFoundException e) {e.printStackTrace();}return null;}/*** 根据用户登录信息设置查询条件**/
public class UserMultiTenancyQueryValueFactory implements MultiTenancyQueryValueFactory {@Overridepublic Object buildMultiTenancyQueryValue() {// TODO 根据业务系统设置多租户统一查询条件,一般业务逻辑从用户上下文获取过滤条件return "test";}
}

定义MultiTenancyProperties,设置多租户中过滤的数据库字段,已经查询条件的设置,现在实现了相等条件和IN条件的查询条件实现方式,其源码如下:

public class MultiTenancyProperties implements Serializable {private static final long serialVersionUID = -1982635513027523884L;public static final String MULTI_TENANCY_QUERY_COLUMN_PROPERTY = "multiTenancyQueryColumn";public static final String CONDITION_TYPE_PROPERTY = "conditionType";/*** 租户的字段名称*/private String multiTenancyQueryColumn;/*** 租户字段查询条件* {@link ConditionFactory.ConditionTypeEnum}*/private String conditionType;public MultiTenancyProperties() {// 默认使用IN 条件,例如 id in(1,2,3)this.conditionType = ConditionFactory.ConditionTypeEnum.IN.name();}
}

 解析查询语句的生成规格ConditionFactory以及条件语句的追加逻辑

定义ConditionFactory接口实现查询sql查询语句的生成,其默认实现类DefaultConditionFactory实现了相等条件和IN条件的查询语句语法,其源码如下:

public class DefaultConditionFactory implements ConditionFactory {private static final String EQUAL_CONDITION = "=";private static final String IN_CONDITION = " in ";private final DBColumnValueFactory columnValueFactory;public DefaultConditionFactory() {this.columnValueFactory = new DefaultDBColumnValueFactory();}@Overridepublic String buildCondition(ConditionTypeEnum conditionType, String multiTenancyQueryColumn, MultiTenancyQuery multiTenancyQuery) {StringBuilder stringBuilder = new StringBuilder();String columnValue = this.columnValueFactory.buildColumnValue(multiTenancyQuery.getMultiTenancyQueryValue());// 根据条件类型设置查询条件switch (conditionType) {// IN条件case IN:stringBuilder.append(multiTenancyQueryColumn).append(IN_CONDITION).append("(").append(columnValue).append(")");break;// 相等条件case EQUAL:default:stringBuilder.append(multiTenancyQueryColumn).append(EQUAL_CONDITION).append(columnValue);break;}// 设置数据库表别名String preTableName;if (StringUtils.isNotBlank(preTableName = multiTenancyQuery.getPreTableName())) {stringBuilder.insert(0, ".").insert(0, preTableName);}return stringBuilder.toString();}
}

在原生的sql查询语句新增自定义的查询条件方法,是根据是否存在where查询条件字段进行动态的拼接。如果没有查询条件则直接添加,反之,则添加到第一个查询条件的位置。其源码如下:

// MultiTenancyQueryInterceptor.class 
private String appendWhereCondition(String originSql, String condition) {if (StringUtils.isBlank(originSql) || StringUtils.isBlank(condition)) {return originSql;}String[] sqlSplit = originSql.toLowerCase().split(WHERE_CONDITION.trim());// 没有查询条件if (this.noWhereCondition(sqlSplit)) {return originSql + WHERE_CONDITION + condition;}// 包含查询条件,添加到第一个查询条件的位置else {String sqlBeforeWhere = sqlSplit[0];String sqlAfterWhere = sqlSplit[1];return sqlBeforeWhere + WHERE_CONDITION + condition + AND_CONDITION + sqlAfterWhere;}}

多租户拦截器全部源码可以从多租户数据拦截器插件下载。


http://chatgpt.dhexx.cn/article/ELc4XU5j.shtml

相关文章

多种数据过滤与降维算法

数据过滤 1. 缺失值比率 (Missing Values Ratio) 方法的是基于包含太多缺失值的数据列包含有用信息的可能性较少。因此&#xff0c;可以将数据列缺失值大于某个阈值的列去掉。阈值越高&#xff0c;降维方法更为积极&#xff0c;即降维越少。 2. 低方差滤波 (Low Variance Filte…

QT重写QSortFilterProxyModel实现多列数据过滤

在QTableView中可以使用QSortFilterProxyModel来过滤model的数据&#xff0c;可以通过setFilterKeyColumn(int colnum)设置需要过滤的列&#xff0c;当设置为-1时则过滤对象为所有列&#xff0c;但是此种方式只要某一列中有符合条件的数据就会被显示出来 因此我们可以重写QSort…

Java流Stream-1:数据过滤filter

重要&#xff1a;默认过滤数据实体对象时为引用传递。 &#xff08;1&#xff09;若修改过滤后的值&#xff0c;原始值会改变。 &#xff08;2&#xff09;若过滤后的数据需要变更&#xff0c;且原始值不受影响&#xff0c;需要过滤时新建对象。 1 filter方法 按照指定条件过…

【Python数据分析与处理 实训02】 ---2012欧洲杯信息分析(数据过滤与排序)

【Python数据分析与处理 实训02】 —2012欧洲杯信息分析&#xff08;数据过滤与排序&#xff09; 探索2012欧洲杯信息 对于下面的数据集进行简单的一些数据的分析训练 若需要源数据请私信~ 读取数据 euro12 pd.read_csv("G:\Projects\pycharmeProject\大数据比赛\泰迪…

【SQL语法基础】数据过滤:SQL数据过滤都有哪些方法?

我在上篇文章中讲到过&#xff0c;提升查询效率的一个很重要的方式&#xff0c;就是约束返回结果的数量&#xff0c;还有一个很有效的方式&#xff0c;就是指定筛选条件&#xff0c;进行过滤。过滤可以筛选符合条件的结果&#xff0c;并进行返回&#xff0c;减少不必要的数据行…

若依ruoyiAOP切面用于数据过滤和权限处理实例

目录 1.什么是AOP 简介 2.若依的AOP实现 2.1若依数据过滤AOP 定义注解 实现切面类 2.2数据源AOP 1.什么是AOP 简介 AOP为Aspect Oriented Programming的缩写&#xff0c;意为&#xff1a;面向切面编程&#xff0c;通过预编译方式和运行期间动态代理实现程序功能的统一…

FineReport-数据过滤权限设置

目的本省份的人只能看到某表单属于该省份的数据 安徽只能看到安徽的数据 一、在数据库中建立一张权限对应表&#xff0c;该表字段包含帆软登录用户名、省份名称即可 二、在帆软报表模板数据集中设置过滤 select * from table1 t1 left join table2 t2 on t1.MZt2.USERNAME w…

布隆过滤器(亿级数据过滤算法)

介绍 我们以演进的方式来逐渐认识布隆过滤器。先抛出一个问题爬虫系统中URL是怎么判重的&#xff1f;你可能最先想到的是将URL放到一个set中&#xff0c;但是当数据很多的时候&#xff0c;放在set中是不现实的。 这时你就可能想到用数组hash函数来实现了。 index hash(URL)…

Pandas的数据过滤

作者|Amanda Iglesias Moreno 编译|VK 来源|Towards Datas Science 从数据帧中过滤数据是清理数据时最常见的操作之一。Pandas提供了一系列根据行和列的位置和标签选择数据的方法。此外,Pandas还允许你根据列类型获取数据子集,并使用布尔索引筛选行。 在本文中,我们将介绍…

数据过滤:SQL数据过滤都有哪些方法?

我在上篇文章中讲到过&#xff0c;提升查询效率的一个很重要的方式&#xff0c;就是约束返回结果的数量&#xff0c;还有一个很有效的方式&#xff0c;就是指定筛选条件&#xff0c;进行过滤。过滤可以筛选符合条件的结果&#xff0c;并进行返回&#xff0c;减少不必要的数据行…

数据过滤(MySQL)

数据过滤 数据过滤用在WHERE表达式里&#xff0c;常用的有基本查询过滤、条件查询过滤、模糊查询过滤、字段查询过滤以及正则表达式查询过滤。 一、基本查询过滤 基本查询过滤可以查询所有字段数据或指定一个字段或者多个字段的数据。 附带建表 mysql> create table use…

掌握这些数据过滤的技巧,再复杂的业务数据也能高效处理!

随着互联网的飞速发展&#xff0c;呈爆炸式增长的数据使用户逐渐迷失在了信息的海洋之中&#xff0c;在进行数据分析时&#xff0c;海量的业务数据往往会带来一些问题&#xff1a; 准确性差&#xff1a;无效数据以及无需进行分析的数据混杂在其中&#xff0c;导致分析结果与实际…

阿里云服务器初始化

初始化阿里云服务器 进入阿里云服务器&#xff0c;然后在 配置信息 点击 重新初始化磁盘 接着会出现一个提示框&#xff0c;点击 确认 即可 进入实例云盘中&#xff0c;点击 重新初始化磁盘 然后设置密码 完成这一步后&#xff0c;输入手机验证码。这时阿里云服务器就被初始…

腾讯云服务器如何开启虚拟化,腾讯云服务器虚拟化驱动是什么

腾讯云服务器虚拟化驱动是什么&#xff1f; 云服务器虚拟化驱动&#xff0c;为腾讯自研开发&#xff0c;专门用于虚拟化效率提升的驱动程序&#xff0c;云服务器虚拟化驱动在linux系统中驱动文件名是pvdriver&#xff0c;安装路径:/usr/local/qcloud/pvdriver/bin&#xff0c;在…

金山办公CEO章庆元:数字化、云化、订阅化趋势下,组织数字办公走向纵深

关注ITValue&#xff0c;看企业级最新鲜、最价值报道&#xff01; 企业办公行业今年有3个关键词——数字化、云化、订阅化。 从数字化来说&#xff0c;国家十四五规划明确提出了“加快建设数字经济、数字社会、数字政府&#xff0c;以数字化转型整体驱动生产方式、生活方式和治…

物联网端-云一体化应用管理解决方案

近年来&#xff0c;随着云计算的发展&#xff0c;“云边端一体化”、“云端协同”等词也频繁出现在大众眼。 什么是“端-云一体化”&#xff1f; 这里我们拆开来解释&#xff1a; 云&#xff1a;云计算、云数据中心&#xff1b; 端&#xff1a;指的是终端。 合起来的意思就是…

CloudCore引领核心网云化转型

文/刘皓 2015年7月&#xff0c;全球著名咨询公司IHS Infonetics发布最新NFV&#xff08;Network Functions Virtualization&#xff0c;网络功能虚拟化&#xff09;市场调研报告。报告显示&#xff0c;NFV市场空间将从2014年的9.5亿美元增长到2019年的116亿美元&#xff0c;年…

全面推进云化,使能数字化转型 ——徐直军在2016华为全球分析师大会上的发言

文/徐直军 女士们、先生们&#xff0c;各位老朋友、新朋友&#xff0c;大家上午好&#xff01;非常高兴在同样的地点跟各位老朋友再相会&#xff0c;也非常欢迎各位新朋友来参加华为2016年的分析师大会。 这次大会的组委会给我定的主题是《全面推进云化&#xff0c;使能数字化转…

阿里云人物动漫化

简介 使用阿里云人物动漫化功能制作一款属于自己的专属头像(该功能收费) 功能描述 人物动漫化能力可以将一张人物图像进行转换处理&#xff0c;生成二次元卡通形象&#xff0c;并返回动漫化后的结果图像。效果示例如下。 原图&#xff1a; 日漫风结果图&#xff1a; 3D特效结…

服务器虚拟化与云平台,虚拟服务器和云有哪些区别

原标题&#xff1a;虚拟服务器和云有哪些区别 虚拟服务器和云有哪些区别&#xff1f;如果不是专业的人员&#xff0c;其实对于服务器是搞不懂的&#xff0c;其实虚拟服务器和云都是对硬件的抽象&#xff0c;两者都有很多好处和使用的理由&#xff0c;那么服务器虚拟化和云的区别…