简介

在日常开发中,在两个bean之间进行数据传递是常见的操作,例如在日常开发中,将数据从VO类转移到DO类等。在两个bean之间进行数据传递,最常见的解决方案,就是手动复制,但是它比较繁琐,充斥着大量的样板代码,这种高度规则化的代码很容易想到使用工具来解决。java中确实有不少框架来解决这个问题,这里统一做个对比。

解决方案

需求:在两个bean之间进行数据传递

// 第一个类:vo类
public class PersonVO {private Integer id;private String username;private String birthday;// getter、setter
}// 第二个类:领域模型
public class Person {private Integer id;private String name;private Date birthday;// getter、setter
}

方案1:手动转换

案例:手动编写vo类和领域模型之间的转换逻辑

public static PersonVO convertPersonToVO(Person person) {PersonVO vo = new PersonVO();vo.setId(person.getId());vo.setUsername(person.getName());vo.setBirthday(DateTimeUtil.convertDateWithDefaultPattern(person.getBirthday()));return vo;
}

这是最原始的方式,它最简单、最稳定,接下来的几种方式都是对于这种方式的优化

方案2:spring提供的BeanUtils

案例:

第一步:添加依赖

<dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>5.2.20.RELEASE</version>
</dependency>

第二步:编写转换逻辑

public static PersonVO convertPersonToVO(Person person) {PersonVO vo = new PersonVO();// spring提供的工具类,它可以在名称相同、类型匹配的属性上进行值传递,如果找不到匹配的属性,也不会报错,// 随后需要用户手动处理无法匹配的属性。BeanUtils.copyProperties(person, vo);// 手动处理无法匹配的属性vo.setUsername(person.getName());vo.setBirthday(DateTimeUtil.convertDateWithDefaultPattern(person.getBirthday()));return vo;
}

spring提供的工具类,在实际开发中是用的比较多的

方案3:apache提供的BeanUtils

案例:

第一步:添加依赖

<dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.4</version>
</dependency>

第二步:编写转换逻辑

public static PersonVO convertPersonToVO(Person person) {PersonVO vo = new PersonVO();// apache提供的工具类,和spring提供的几乎一样try {BeanUtils.copyProperties(vo, person);} catch (Exception e) {throw new RuntimeException(e);}vo.setUsername(person.getName());vo.setBirthday(DateTimeUtil.convertDateWithDefaultPattern(person.getBirthday()));return vo;
}

apache提供的工具类,也是实际开发中用的比较多的

方案4:mapstruct

一个框架,用户可以通过注解来指定数据传递的逻辑。它基于java提供的注解处理器API,可以在编译时按照用户指定的规则,生成在两个bean之间进行数据传递的代码。因为是在编译时动态生成代码,避免了运行时的反射调用,所有它的执行效率和手动编写的转换逻辑几乎没有区别。

案例:

第一步:添加依赖

<!--面向用户的api,指定转换逻辑-->
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.4.1.Final</version>
</dependency>
<!--在编译时动态生成字节码的组件,只需要在编译时使用,所以它的依赖范围是provided-->
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.4.1.Final</version><scope>provided</scope>
</dependency>

第二步:编写转换逻辑

// 指定当前接口是一个用于在两个bean之间进行数据传递的接口,
// mapstruct在编译时会处理这个注解。不要和mybatis中的混淆。
@Mapper  
public interface PersonMapper {// mapstruct会在编译时动态生成当前接口的实现类,这里通过这种方式在运行时获取到实现类的实例,// 方便外部调用PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);// 这里指定领域模型到vo类的转换逻辑@Mappings({// 指定两个bean之间类型匹配但是名称不一致的属性@Mapping(source = "name", target = "username"),  // 指定不需要进行数据传递的属性@Mapping(target = "birthday", ignore = true)})PersonVO toPersonVO(Person person);// 用户自定义数据转换的逻辑,如果某些属性需要使用java代码来计算,可以通过这种方式来指定@AfterMappingdefault void toPersonVOAfterMapping(Person person, @MappingTarget PersonVO vo) {vo.setBirthday(formatDate(person.getBirthday()));}default String formatDate(Date date) {return DateTimeUtil.convertDateWithDefaultPattern(date);}
}// 在外部调用上面的mapper接口
public static PersonVO convertPersonToVO(Person person) {return PersonMapper.INSTANCE.toPersonVO(person);
}

第三步:查看编译时生成的代码

public class PersonMapperImpl implements PersonMapper {public PersonMapperImpl() {}// 根据之前的注解,生成转换逻辑public PersonVO toPersonVO(Person person) {if (person == null) {return null;} else {PersonVO personVO = new PersonVO();personVO.setUsername(person.getName());personVO.setId(person.getId());this.toPersonVOAfterMapping(person, personVO);return personVO;}}
}

总结:mapstruct的使用比较复杂,这里只做一个简单的展示,并且不深入讲解它的逻辑,总之,它类似于lombok,在编译时根据注解动态生成字节码,注解中指定了转换逻辑。因为是编译时生成字节码,避免了反射调用,所以它的执行效率和手动编写转换逻辑几乎一致。

其它:dozer、orika、modelMapper等框架

相较于手动编写转换逻辑的代码而言,这些框架的使用都过于复杂了,不推荐使用,这里就不展示它们的使用案例了。

解决方案对比

首先,排除掉dozer、orika等,因为相较于手动编写转换逻辑而言,它们的使用都比较复杂,相当于为了避免一件麻烦事,引入另一件更麻烦的事。

其次,基于反射的工具类也不推荐,因为如果bean中的某个属性转换失败,它们不会报错,而是忽略,如果在开发过程中修改了属性名称,造成的数据传递失败不会在编译时被发现,要到运行时才能看出来。

最后,最推荐的,是手动编写转换逻辑或mapstruct。手动编写转换逻辑是最简单、最安全的方式,推荐把转换逻辑放到一个工具类中,实现代码的复用。mapstruct有一定的复杂度,但是它的功能强大,完全可以满足需求,而且如果修改了属性的名称,在编译时就会报错,而且它是编译时生成代码,效率上也满足要求。

具体使用哪一种,取决于项目组的要求。如果追求稳定,推荐手动编写转换逻辑,如果追求开发速度,推荐引入mapstruct,不推荐使用基于反射的BeanUtils,因为如果在开发中修改了属性名,基于BeanUtils的转换逻辑不会感知到这种修改,可能会导致某些错误要到运行时才能被发现。

不同解决方案的性能对比:之前提到基于反射的转换逻辑,执行起来比较慢,但是性能上具体表现如何,这里对不同解决方案的性能做一个对比,具体方法是,上面案例中每种转换方案都执行几万次,观察执行时间,对比它们的效率。

执行一百万次耗费的时间(毫秒)执行一千万次耗费的时间(毫秒)
手动编写转换逻辑6344768
spring提供的BeanUtils7715674
apache提供的BeanUtils419840389
mapstruct6074807

可以看到,apache提供的工具类明显比spring提供的工具类要慢,手动编写转换逻辑和mapstruct在执行速度上差别不大。

源码分析

这里对上面提到的几种解决方案做一个深入介绍。

spring提供的BeanUtils

整体流程

源码:

// 参数1:源对象
// 参数2:目标对象
// 参数3:目标对象的类对象,可以不传
// 参数4:要被忽略的属性,可以不传
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,@Nullable String... ignoreProperties) throws BeansException {Assert.notNull(source, "Source must not be null");Assert.notNull arget, "Target must not be null");Class<?> actualEditable = target.getClass();if (editable != null) {if (!editable.isInstance(target)) {throw new IllegalArgumentException("Target class [" + target.getClass().getName() +"] not assignable to Editable class [" + editable.getName() + "]");}actualEditable = editable;}// 获取目标对象的中所有属性,这里的PropertyDescriptor,包含属性名、属性的getter方法/setter方法实例PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);for (PropertyDescriptor targetPd : targetPds) {// 获取目标属性的setter方法Method writeMethod = targetPd.getWriteMethod();// 如果目标属性有setter方法并且没有被指定为需要忽略if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {// 根据目标属性的属性名,获取源对象中指定属性的getter方法PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());if (sourcePd != null) {// 如果可以获取到目标属性的getter方法Method readMethod = sourcePd.getReadMethod();if (readMethod != null &&// 这里判断getter方法的返回值和setter方法的参数是否匹配ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {try {if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {readMethod.setAccessible(true);}// 如果匹配,把getter方法的返回值传递给setter方法Object value = readMethod.invoke(source);if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}writeMethod.invoke(target, value);}catch (Throwable ex) {throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", ex);}}}}}
}

总结:BeanUtils的基本逻辑,就是获取目标对象中的所有属性,然后根据属性名获取源对象中的getter方法,如果getter方法的返回值和setter方法的参数相匹配,就进行数据传递。这里也可以解释,为什么属性名不一致或数据类型不一致无法进行属性传递。

深入了解

上面的代码中提到了PropertyDescriptor,它存储了一个属性和它的getter、setter方法,这里深入了解这些信息是如何被解析出来的。

相关组件

PropertyDescriptor:存储了一个属性和它的getter、setter方法,它是java.beans包下的api

// PropertyDescriptor
public class PropertyDescriptor extends FeatureDescriptor {private Reference<? extends Class<?>> propertyTypeRef;// getter方法private final MethodRef readMethodRef = new MethodRef();// setter方法private final MethodRef writeMethodRef = new MethodRef();private Reference<? extends Class<?>> propertyEditorClassRef;private boolean bound;private boolean constrained;// 属性名首字母大写// The base name of the method name which will be prefixed with the// read and write method. If name == "foo" then the baseName is "Foo"private String baseName;private String writeMethodName;private String readMethodName;
}// PropertyDescriptor的父类
public class FeatureDescriptor {// 类对象的引用private Reference<? extends Class<?>> classRef;
}// 方法引用,可以看到,Method对象被放到了软引用之中。
final class MethodRef {private String signature;private SoftReference<Method> methodRef;private WeakReference<Class<?>> typeRef;
}

相关组件和整体流程

整体流程:spring调用java提供的内省器,解析类对象,内省器会缓存解析结果,spring本身也提供了缓存机制。

涉及到的组件:

  • java提供的内省器 Introspector,它负责通过反射解析类对象中的信息,把解析结果存储到BeanInfo、PropertyDescriptor中
  • CachedIntrospectionResults:缓存内省器的解析结果,这个组件是spring提供的。

这里以组件为核心来介绍整体流程。

java提供的内省器 Introspector

Introspector:java提供的内省器,通过反射来解析类对象中的数据。

基本结构:

public class Introspector {// 类对象,一个内省器实例负责解析一个类对象private Class<?> beanClass;// 父类的bean信息private BeanInfo superBeanInfo;// 方法信息,key是方法名,值是解析出的方法信息private Map<String, MethodDescriptor> methods;// 属性信息,key是属性名,值是属性信息,这里还包括属性的getter、setter方法private Map<String, PropertyDescriptor> properties;// 类中方法名的前缀static final String ADD_PREFIX = "add";static final String REMOVE_PREFIX = "remove";static final String GET_PREFIX = "get";static final String SET_PREFIX = "set";static final String IS_PREFIX = "is";
}

整体流程:

1、创建内省器的实例,解析bean信息:

public static BeanInfo getBeanInfo(Class<?> beanClass)throws IntrospectionException
{if (!ReflectUtil.isPackageAccessible(beanClass)) {return (new Introspector(beanClass, null, USE_ALL_BEANINFO)).getBeanInfo();}// 缓存解析结果ThreadGroupContext context = ThreadGroupContext.getContext();BeanInfo beanInfo;synchronized (declaredMethodCache) {beanInfo = context.getBeanInfo(beanClass);}if (beanInfo == null) {// 为指定的类对象创建一个内省器实例,然后解析类对象中的信息beanInfo = new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo();synchronized (declaredMethodCache) {context.putBeanInfo(beanClass, beanInfo);}}return beanInfo;
}

可以看到,内行器会缓存解析出的结果,把它缓存到ThreadGroupContext中。ThreadGroupContext有两层缓存,第一层缓存,线程组实例和ThreadGroupContext实例,第二层缓存,类对象和BeanInfo,第二层缓存使用WeakHashMap来实现,key是弱引用,垃圾回收运行时会回收弱引用,这里可以理解为缓存的失效机制,保证内存安全。

2、解析类对象中的bean信息:整体流程

private BeanInfo getBeanInfo() throws IntrospectionException {// 解析基本的bean信息BeanDescriptor bd = getTargetBeanDescriptor();// 解析方法信息MethodDescriptor mds[] = getTargetMethodInfo();// 解析事件相关的方法,这个很少用到EventSetDescriptor esds[] = getTargetEventInfo();// 解析属性信息,包括属性的getter、setter方法,这里重点关注PropertyDescriptor pds[] = getTargetPropertyInfo();int defaultEvent = getTargetDefaultEventIndex();int defaultProperty = getTargetDefaultPropertyIndex();return new GenericBeanInfo(bd, esds, defaultEvent, pds,defaultProperty, mds, explicitBeanInfo);}

3、解析属性信息和属性信息对应的getter、setter方法,这是需要重点关注的方法

private PropertyDescriptor[] getTargetPropertyInfo() {// 用户手动指定的bean信息,这个特性已经用的很少了// Check if the bean has its own BeanInfo that will provide// explicit information.PropertyDescriptor[] explicitProperties = null;if (explicitBeanInfo != null) {explicitProperties = getPropertyDescriptors(this.explicitBeanInfo);}if (explicitProperties == null && superBeanInfo != null) {// We have no explicit BeanInfo properties.  Check with our parent.addPropertyDescriptors(getPropertyDescriptors(this.superBeanInfo));}for (int i = 0; i < additionalBeanInfo.length; i++) {addPropertyDescriptors(additionalBeanInfo[i].getPropertyDescriptors());}if (explicitProperties != null) {// Add the explicit BeanInfo data to our results.addPropertyDescriptors(explicitProperties);} else {// Apply some reflection to the current class.// 重点看这里,获取当前类和父类中所有的公共方法// First get an array of all the public methods at this levelMethod methodList[] = getPublicDeclaredMethods(beanClass);// Now analyze each method.for (int i = 0; i < methodList.length; i++) {Method method = methodList[i];if (method == null) {continue;}// 跳过静态方法// skip static methods.int mods = method.getModifiers();if (Modifier.isStatic(mods)) {continue;}// 获取方法的基本信息String name = method.getName();Class<?>[] argTypes = method.getParameterTypes();Class<?> resultType = method.getReturnType();int argCount = argTypes.length;PropertyDescriptor pd = null;if (name.length() <= 3 && !name.startsWith(IS_PREFIX)) {// Optimization. Don't bother with invalid propertyNames.continue;}try {// 这里就是解析getter、setter方法if (argCount == 0) { // 参数个数为0,证明是getter方法if (name.startsWith(GET_PREFIX)) {// 如果方法是get开头pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);} else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {// 如果方法是is开头并且返回值类型是boolean,例如,属性类型是boolean时,getter方法是isXXX// Boolean getterpd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);}} else if (argCount == 1) {  // 参数个数为1if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null);} else if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) {// 返回值是void并且方法以set开头,证明是setter方法// Simple setterpd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method);if (throwsException(method, PropertyVetoException.class)) {pd.setConstrained(true);}}} else if (argCount == 2) {  // 参数个数为2,带下标的setter方法if (void.class.equals(resultType) && int.class.equals(argTypes[0]) && name.startsWith(SET_PREFIX)) {pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, null, method);if (throwsException(method, PropertyVetoException.class)) {pd.setConstrained(true);}}}} catch (IntrospectionException ex) {// This happens if a PropertyDescriptor or IndexedPropertyDescriptor// constructor fins that the method violates details of the deisgn// pattern, e.g. by having an empty name, or a getter returning// void , or whatever.pd = null;}if (pd != null) {// If this class or one of its base classes is a PropertyChange// source, then we assume that any properties we discover are "bound".if (propertyChangeSource) {pd.setBound(true);}// 保存解析结果addPropertyDescriptor(pd);}}}// 处理解析好的方法processPropertyDescriptors();// 返回解析结果PropertyDescriptor result[] =properties.values().toArray(new PropertyDescriptor[properties.size()]);// Set the default index.if (defaultPropertyName != null) {for (int i = 0; i < result.length; i++) {if (defaultPropertyName.equals(result[i].getName())) {defaultPropertyIndex = i;}}}return result;
}

这段代码看起来很多,但是有许多特性已经被淘汰了,几乎不使用,例如,方法开头的explicitBeanInfo,它是用户手动指定的bean信息,这个特性几乎不会被用到,以及解析方法的过程中遇到的带下标信息的getter、setter方法,这些也不会被用到。

上面的逻辑只展示了内省器是如何解析属性对应的方法,它会遍历所有的方法,根据方法的名称、参数,来决定方法是哪个属性的getter或setter方法,然后把属性名和方法放到一个map中,key是属性名,value是一个list,存储了一个属性的getter、setter方法。随后会处理属性的getter、setter方法,把它们放到一个PropertyDescriptor中。

要注意,内省器中处理方法的核心原则是遍历所有的方法,它不会根据属性名来拼接方法名,然后直接获取方法的实例,这可能是为了兼容一些以前的特性,例如带下标的getter、setter方法,这些特性现在已经不用了,但是内省器中还是会处理这些方法。

缓存内行器执行结果的组件 CachedIntrospectionResults

CachedIntrospectionResults:缓存内省器的解析结果

基本结构:

public final class CachedIntrospectionResults {// 基于强引用的缓存,注意,缓存中的value也是当前类的实例。static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache =new ConcurrentHashMap<>(64);// 基于弱引用的缓存static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache =new ConcurrentReferenceHashMap<>(64);// 类对象的解析结果private final BeanInfo beanInfo;private final Map<String, PropertyDescriptor> propertyDescriptors;}

整体流程:

1、调用内省器并缓存它的执行结果

static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {// 从缓存中获取数据CachedIntrospectionResults results = strongClassCache.get(beanClass);  // 从强缓存中获取if (results != null) {return results;}results = softClassCache.get(beanClass);  // 从弱缓存中获取if (results != null) {return results;}// 这里会调用内省器来解析类对象,解析结果存放到当前类的实例中,然后把当前类的实例放到缓存中results = new CachedIntrospectionResults(beanClass);ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;// 这里的判断,决定使用强缓存还是弱缓存。// 如果要被解析的类的类加载器,和CachedIntrospectionResults的累加器是同一个,使用强缓存if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||isClassLoaderAccepted(beanClass.getClassLoader())) {classCacheToUse = strongClassCache;}else {if (logger.isDebugEnabled()) {logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");}classCacheToUse = softClassCache;}// 把解析结果存入缓存CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);return (existing != null ? existing : results);
}

2、缓存的清除机制:清除指定类加载器下的所有解析结果

public static void clearClassLoader(@Nullable ClassLoader classLoader) {acceptedClassLoaders.removeIf(registeredLoader ->isUnderneathClassLoader(registeredLoader, classLoader));// 清除强引用缓存strongClassCache.keySet().removeIf(beanClass ->isUnderneathClassLoader(beanClass.getClassLoader(), classLoader));// 清除弱引用缓存softClassCache.keySet().removeIf(beanClass ->isUnderneathClassLoader(beanClass.getClassLoader(), classLoader));
}

总结:CachedIntrospectionResults是一个工厂类,它负责管理缓存,并且它管理的缓存就是它自己的实例,每一个实例对应一个类对象的解析结果。

问题:为什么Introspector中提供了缓存机制,但是spring还要额外提供一个CachedIntrospectionResults来缓存解析结果?spring提供的缓存性能更高,Introspector中使用的缓存通过synchronized关键字保证线程安全,spring提供的缓存通过分段锁和cas操作来保证线程安全,性能更高。

总结

上面就是spring的BeanUtils是如何获取PropertyDescriptor实例,它主要是通过java提供的内省器来完成的,同时spring提供了缓存机制。

apache提供的BeanUtils

相较于spring提供的BeanUtils,它的功能更强大,但是执行效率也更低,它的内部同样是基于java提供的内省器,同时,它还提供了多种转换器,用于对数据格式进行转换。

mapstruct

基本原理

mapstruct是为java语言开发的代码生成器,可以通过注解的方式自动生成java bean之间的转换代码

mapstruct类似于lombok,基于java提供的注解处理器技术,来扫描和处理指定注解,在编译时生成转换代码。由于它是在编译时生成代码,不需要在运行时特殊处理或者反射,所以它的执行效率和原生代码一样。

java代码的编译过程,大致可以分为三个阶段:

  • 将源代码转换为抽象语法树(Abstract Syntax Tree),抽象语法树用于描述源代码的语法结构,语法树中的每一个节点都代表着源代码中的一个元素,例如包、运算符、返回值、代码注释等。
  • 编译器调用注解处理器,注解处理器会找到自己要处理的注解,然后修改语法树,生成新的代码
  • 编译器使用修改后的语法树来生成字节码文件。

抽象语法树:编译器在编译过程中生成的,用于表示源码的语法结构

注解处理器:编译器提供的扩展,用户可以编写自己的注解处理器,在编译时调用,处理特定的注解。

JSR-269规范:定义了java编译时的注解处理器(Pluggable Annotation Processing 插件式注解处理器),提供了一种标准化的方式让开发者在编译时处理java代码中的注解。

注解处理器的使用步骤:

  • 自定义注解
  • 继承一个特定的抽象类 AbstractProcessor,实现自己处理注解的逻辑
  • 基于SPI机制,注册自己的注解处理器,在META-INF/services目录下创建一个特定的文件,文件中是自定义注解处理器的全限定名。
  • 完成,此时,在编译源代码时,就会调用注解处理器

在mapstruct的核心原理中,注解处理器的定义和代码的生成是最核心的两部分,mapstruct使用freemarker模板引擎来生成java代码,用户还可以尝试自定义注解处理器来了解注解处理器的工作机制。

优缺点

类似于lombok,这种基于注解处理器的框架可以生成一些样板代码,减少代码量,但是它会增加源码的复杂度,而且不方便调试。相较于手动编写转换逻辑,是不推荐使用的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/78473.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/78473.shtml
英文地址,请注明出处:http://en.pswp.cn/web/78473.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于开闭原则优化数据库查询语句拼接方法

背景 在开发实践中&#xff0c;曾有同事在实现新功能时&#xff0c;因直接修改一段数据库查询条件拼接方法的代码逻辑&#xff0c;导致生产环境出现故障。 具体来看&#xff0c;该方法通过在函数内部直接编写条件判断语句实现查询拼接&#xff0c;尽管从面向对象设计的开闭原…

QT开发工具对比:Qt Creator、Qt Designer、Qt Design Studio

前端开发工具—Qt Designer Qt Designer是Qt框架的一部分&#xff0c;是一个图形用户界面设计工具。它允许开发者通过可视化方式设计和布局GUI组件&#xff0c;而无需手动编写UI代码。设计完成后&#xff0c;Qt Designer生成UI文件&#xff08;通常以.ui为扩展名&#xff09;&…

0基础 | STM32 | TB6612电机驱动使用

TB6612介绍及使用 单片机通过驱动板连接至电机 原因&#xff1a;单品机I/O口输出电流I小 驱动板&#xff1a;从外部引入高电压&#xff0c;控制电机驱动 电源部分 VM&#xff1a;电机驱动电源输入&#xff0c;输入电压范围建议为3.7&#xff5e;12V GND&#xff1a;逻辑电…

【操作系统】死锁

1. 定义 死锁是指两个或多个进程&#xff08;或线程&#xff09;在执行过程中&#xff0c;因争夺资源而造成的一种僵局&#xff0c;每个进程都无限期地等待其他进程释放它们所持有的资源。在这种情况下&#xff0c;没有任何进程能够继续执行&#xff0c;除非有外部干预。 2. …

C++入门☞关于类的一些特殊知识点

涉及的关于类中的默认成员函数的知识点可以看我的这篇博客哦~ C入门必须知道的知识☞类的默认成员函数&#xff0c;一文讲透运用 目录 初始化列表 类型转换 static成员 友元 内部类 匿名对象 对象拷贝时的一些编译器的优化 初始化列表 我们知道类中的构造函数的任务是完…

只用Prettier进行格式化项目

1.下载Prettier插件&#xff0c;禁用ESlint 2.在项目根目录新建.prettierrc文件 {"singleQuote": true,"jsxSingleQuote": true,"printWidth": 100,"trailingComma": "none","tabWidth": 2,"semi": f…

XXL-TOOL v1.4.0 发布 | Java工具类库

Release Notes 1、【新增】JsonRpc模块&#xff1a;一个轻量级、跨语言远程过程调用实现&#xff0c;基于json、http实现&#xff08;从XXL-JOB底层通讯组件提炼抽象&#xff09;。2、【新增】Concurrent模块&#xff1a;一系列并发编程工具&#xff0c;具备良好的线程安全、高…

基于LVGL的登录界面设计

目录 一、演示 二、前言 三、部件知识 3.1 图片按钮部件 3.1.1 图片按钮部件的组成 3.1.2 图片的来源 3.1.3 添加/清除的状态 3.1.4 图片按钮部件 API 函数 3.2 键盘部件(lv_keyboard) 3.2.1 键盘部件的组成 3.2.2 键盘部件的相关知识 3.2.2.1 键盘部件模式 3.…

S3 跨账户复制:增强云中的灾难恢复计划

您准备好提升您的云和 DevOps 技能了吗&#xff1f; &#x1f425;《云原生devops》专门为您打造&#xff0c;我们精心打造的 30 篇文章库&#xff0c;这些文章涵盖了 Azure、AWS 和 DevOps 方法论的众多重要主题。无论您是希望精进专业知识的资深专业人士&#xff0c;还是渴望…

线程与进程深度解析:从fork行为到生产者-消费者模型

线程与进程深度解析&#xff1a;从fork行为到生产者-消费者模型 一、多线程环境下的fork行为与线程安全 1. 多线程程序中fork的特殊性 核心问题&#xff1a;fork后子进程的线程模型 当多线程程序中的某个线程调用fork时&#xff1a; 子进程仅包含调用fork的线程&#xff1…

Circular Plot系列(五): circle plot展示单细胞互作

这是我们circle系列的最后一节&#xff0c;我想常见的弦图是绕不开的&#xff0c;所以最后从前面介绍的circle plot思路&#xff0c;做一遍弦图。其实前面的内容如果消化了&#xff0c;plot互作弦图也就不成什么问题了。 效果如下&#xff1a; #cellchat提取互作结果&#xff…

(11)Vue-Router路由的详细使用

本系列教程目录&#xff1a;Vue3Element Plus全套学习笔记-目录大纲 文章目录 第2章 路由 Vue-Router2.1 Vue路由快速入门2.1.1 创建项目2.1.2 路由运行流程 2.2 传递参数-useRoute2.2.1 路径参数-params1&#xff09;普通传参2&#xff09;传递多个参数3&#xff09;对象方式传…

react + antd 实现后台管理系统

文章目录 完整路由搭建Layout 和 Aside组件引入 AntdAside组件实现 项目效果图 项目完整代码地址 https://gitee.com/lyh1999/react-back-management 项目完整代码地址 react依赖安装 最好采用yarn 安装 react-router 安装依赖 配置路由 history模式 / // src/router/…

基于AWS Marketplace的快速解决方案:从选型到部署实战

1. 引言&#xff1a;为什么选择AWS Marketplace&#xff1f; 在数字化转型的背景下&#xff0c;企业需要快速获取成熟的软件工具和服务以降低开发成本。AWS Marketplace 作为亚马逊云科技的官方应用商店&#xff0c;提供超过万款预配置的第三方和AWS原生解决方案&#xff0c;涵…

2021年第十二届蓝桥杯省赛B组C++题解

2021年第十二届蓝桥杯省赛B组C题解 关键词&#xff1a;蓝桥杯、省赛、题解、C、算法 一、个人见解 第十二届蓝桥杯省赛B组共有10道题目&#xff0c;包含5道填空题&#xff08;T1-T5&#xff09;和5道编程题&#xff08;T6-T10&#xff09;&#xff0c;总分150分。比赛时长4小…

日语学习-日语知识点小记-进阶-JLPT-N1阶段(1):语法单词

日语学习-日语知识点小记-进阶-JLPT-N1阶段&#xff08;1&#xff09;&#xff1a;语法单词 1、前言&#xff08;1&#xff09;情况说明&#xff08;2&#xff09;工程师的信仰&#xff08;3&#xff09;高级语法N1语法和难点一、N1语法学习内容&#xff08;高级语法&#xff…

Python|Pyppeteer实现自动登录小红书(32)

前言 本文是该专栏的第32篇,结合优质项目案例持续分享Pyppeteer的干货知识,记得关注。 本文中,笔者以小红书为例,基于Pyppeteer实现自动登录“小红书”。 需要注意的是,对Pyppeteer不太熟悉的同学,可往前翻阅本专栏前面介绍的Pyppeteer知识点,本专栏将带你了解并熟练使…

【翻译、转载】【转载】LLM 的函数调用与 MCP

来源&#xff1a; https://www.dailydoseofds.com/p/function-calling-mcp-for-llms/ 【代码以图像显示的是原文内容&#xff0c;以代码形式显示的是大模型给出的参考】 LLM 的函数调用与 MCP 在 MCP 变得像现在这样主流&#xff08;或流行&#xff09;之前&#xff0c;大多…

【QT】QT中http协议和json数据的解析-http获取天气预报

QT中http协议和json数据的解析 1.http协议的原理2.QT中http协议的通信流程2.1 方法步骤 3.使用http协议&#xff08;通过http下载图片和获取天气预报信息&#xff09;3.1 http下载网络上的图片(下载小文件)3.1.1 示例代码3.1.2 现象 3.2 获取网络上天气预报3.2.1 免费的天气预报…

hot100:链表倒数k个节点- 力扣(LeetCode)

题目&#xff1a; 实现一种算法&#xff0c;找出单向链表中倒数第 k 个节点。返回该链表中倒数第k个节点。 示例一&#xff1a; 输入&#xff1a;{1,2,3,4,5},2 返回值&#xff1a;{4,5} 说明&#xff1a;返回倒数第2个节点4&#xff0c;系统会打印后面所有的节点来比较。 …