说说Spring中@Value注解
@Value注解是Spring3.0后提出来的一个注解。注解内容本身非常之简单,但是它提供的功能却非常之强大。
首先从它的注解本身定义知道,它能使用在:
- 字段上
- set方法上
- 方法入参上
- 当作元注解
它的功能大致可归类为:
- 注入普通字符串
- 书写SpEL表达式(功能强大包括:获取系统属性、调用静态方法、计算、注入bean、调用bean的方法等等~~~)
- 注入Resource。如:@Value("classpath:com/demo/config.txt") 使用Resource类型接收
- 注入URL资源。如:@Value("http://www.baidu.com") 使用Resource类型接收
@Value
首先看看@Value注解定义
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {
String value(); // 没有默认值 所以是必填的
}
接下来以一个具体实例,跟踪分析它的执行原理:
@Configuration
public class RootConfig {
@Value("#{person.name}")
private String personName;
@Bean
public Person person() {
return new Person("fsx", 18);
}
}
之前我们已经讲过,当Bean进行初始化完成之后会populateBean()对它的属性进行赋值,这个时候AutowiredAnnotationBeanPostProcessor这个后置处理器生效,从而对属性进行依赖注入赋值。
AutowiredAnnotationBeanPostProcessor它能够处理@Autowired和@Value注解
注意:因为@Value是BeanPostProcessor来解析的,所以具有容器隔离性(本容器内的Bean使用@Value只能引用到本容器内的值哦~,因为BeanPostProcessor是具有隔离性的)
推荐:所有的@Value都写在根容器(也就是我们常说的Service容器)内,请不要放在web容器里。也就是说,请尽量不要在controller里使用@Value注解,因为业务我们都要求放在service层
三层架构:Controller、Service、Repository务必做到职责分离和松耦合
我们直接从AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues()方法入手:(Spring5.1后为postProcessProperties方法,方法的语义更加清晰些了~)
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
// 这个方法是InstantiationAwareBeanPostProcessor的,它在给属性赋值的时候会被调用~~
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// InjectionMetadata 里包含private final Collection<InjectedElement> injectedElements;表示所有需要注入处理的属性们~~~
// 所以最终都是InjectionMetadata去处理~
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
} ...
return pvs;
}
}
InjectionMetadata
用于管理注入元数据的内部类。不建议直接在应用程序中使用。AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor以及org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor这几个最终的注入都是靠它~
本处直接看它的inject方法:
public class InjectionMetadata {
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Collection<InjectedElement> checkedElements = this.checkedElements;
Collection<InjectedElement> elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements);
// 遍历,一个一个处理
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
element.inject(target, beanName, pvs);
}
}
}
}
总而言之,最终是委托给了InjectedElement 。它有两个内部类的实现:AutowiredFieldElement和AutowiredMethodElement 此处我们只关注字段注入的。
InjectedElement是InjectionMetadata的一个public内部类,并且是抽象的。AutowiredFieldElement和AutowiredMethodElement都是AutowiredAnnotationBeanPostProcessor的private内部类。
另外:CommonAnnotationBeanPostProcessor中有InjectedElement的实现类:LookupElement、ResourceElement、EjbRefElement、WebServiceRefElement等来辅助完成注入~
AutowiredFieldElement
private class AutowiredFieldElement extends InjectionMetadata.InjectedElement {
private final boolean required;
private volatile boolean cached = false;
@Nullable
private volatile Object cachedFieldValue;
public AutowiredFieldElement(Field field, boolean required) {
super(field, null);
this.required = required;
}
@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
// member在抽象父类:InjectedElement中定义~~~
Field field = (Field) this.member;
Object value;
if (this.cached) {
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
}
else {
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
desc.setContainingClass(bean.getClass());
Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
Assert.state(beanFactory != null, "No BeanFactory available");
// 此处一般为SimpleTypeConverter,它registerDefaultEditors=true,所以普通类型大都能能通过属性编辑器实现转换的
TypeConverter typeConverter = beanFactory.getTypeConverter();
try {
// 最最最根本的原理,其实在resolveDependency这个方法里,它最终返回的就是一个具体的值,这个value是个Object~
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
} catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
}
synchronized (this) {
if (!this.cached) {
if (value != null || this.required) {
this.cachedFieldValue = desc;
registerDependentBeans(beanName, autowiredBeanNames);
if (autowiredBeanNames.size() == 1) {
String autowiredBeanName = autowiredBeanNames.iterator().next();
if (beanFactory.containsBean(autowiredBeanName) &&
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
this.cachedFieldValue = new ShortcutDependencyDescriptor(
desc, autowiredBeanName, field.getType());
}
}
} else {
this.cachedFieldValue = null;
}
this.cached = true;
}
}
}
if (value != null) {
ReflectionUtils.makeAccessible(field);
field.set(bean, value);
}
}
}
DefaultListableBeanFactory#resolveDependency
它是Spring容器整个体系里实现依赖查找的心脏~
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
...
@Override
@Nullable
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
// 最终它也会走到:doResolveDependency
return createOptionalDependency(descriptor, requestingBeanName);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
// 直接new一个Provider返回出去~
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
// 兼容jsr330的javax.inject.Provider
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
// ContextAnnotationAutowireCandidateResolver-> QualifierAnnotationAutowireCandidateResolver 他们能够解决 就直接返回 否则交给doResolveDependency
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}
}
...
// 咱们此处descriptor:field 'personName'
// beanName:rootConfig
// autowiredBeanNames:[]
// typeConverter:SimpleTypeConverter
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
try {
// 若你的despriptor是ShortcutDependencyDescriptor,这个就会直接去beanFactory.getBean(this.shortcut, this.requiredType) 提前返回 主要作用是缓存的效果~
Object shortcut = descriptor.resolveShortcut(this);
if (shortcut != null) {
return shortcut;
}
// public final class String
Class<?> type = descriptor.getDependencyType();
// 备注:QualifierAnnotationAutowireCandidateResolver会处理@Qualifier和@Value
//QualifierAnnotationAutowireCandidateResolver#getSuggestedValue()
//先拿出@Value注解的值 如果为null再去拿Method里这个注解的值~~~ 最终返回~ 所以@Value也是可以标注在方法上的
// 注意此处:若是@Value 这里返回值肯定是String 但是若是@Autowired此处返回值就可能是对象了~
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
// 说明这个注入是@Value 因为它是String
if (value instanceof String) {
// 使用StringValueResolver处理${}占位符~
// 所以我们常用的只使用@Value("${xxx}")这样来注入值或者你就是个字面量值,到这一步就已经完事了~解析完成
// 若你是个el表达式 或者文件资源Resource啥的,会继续交给下面的beanExpressionResolver处理,所以它是处理复杂类型的核心~
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null && containsBean(beanName) ?
getMergedBeanDefinition(beanName) : null);
// 此处注意:处理器是BeanExpressionResolver~~~~它是处理@Value表达式的核心方法
// 它的默认值是:StandardBeanExpressionResolver#evaluate
// 这里面就会解析
value = evaluateBeanDefinitionString(strVal, bd);
}
// 若我们没有定制,此处为SimpleTypeConverter... 值已经拿到手了,经由转换器以转换 就可以测地的返回喽~~~解析结束
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
try {
return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
}
catch (UnsupportedOperationException ex) {
// A custom TypeConverter which does not support TypeDescriptor resolution...
return (descriptor.getField() != null ?
converter.convertIfNecessary(value, type, descriptor.getField()) :
converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
}
}
// ====================下面就不解释了,多bean和required的解释 前面已经分析过了=======================
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
return null;
}
String autowiredBeanName;
Object instanceCandidate;
if (matchingBeans.size() > 1) {
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
}
else {
// In case of an optional Collection/Map, silently ignore a non-unique case:
// possibly it was meant to be an empty collection of multiple regular beans
// (before 4.3 in particular when we didn't even look for collection beans).
return null;
}
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}
else {
// We have exactly one match.
Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
autowiredBeanName = entry.getKey();
instanceCandidate = entry.getValue();
}
if (autowiredBeanNames != null) {
autowiredBeanNames.add(autowiredBeanName);
}
if (instanceCandidate instanceof Class) {
instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
}
Object result = instanceCandidate;
if (result instanceof NullBean) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
result = null;
}
if (!ClassUtils.isAssignableValue(type, result)) {
throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
}
return result;
}
finally {
ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
}
}
}
QualifierAnnotationAutowireCandidateResolver实现了AutowireCandidateResolver,对要自动绑定的field或者参数和bean definition根据@qualifier注解进行匹配,当然也支持javax.inject.Qualifier。同时也支持通过@value注解来绑定表达式的值。
从上面分析知道,当把@Value的占位符替换完成后,最终都会交给beanExpressionResolver由它来统一处理:包括根据beanName获取bean、SpEL计算等等~~~
BeanExpressionResolver
策略接口,用于通过将值作为表达式进行评估来解析值(如果适用)。它持有Bean工厂~
// @since 3.0
public interface BeanExpressionResolver {
// value此时还是复杂类型,比如本例的#{person.name}
// BeanExpressionContext:持有beanFactory和scope的引用而已~
@Nullable
Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException;
}
它的唯一实现类StandardBeanExpressionResolver(当然我们是可以自己实现的,比如后面我们自定义@Value功能,通过继承StandardBeanExpressionResolver来扩展实现。
StandardBeanExpressionResolver
语言解析器的标准实现,支持解析SpEL语言。
public class StandardBeanExpressionResolver implements BeanExpressionResolver {
// 因为SpEL是支持自定义前缀、后缀的 此处保持了和SpEL默认值的统一
// 它的属性值事public的 so你可以自定义~
/** Default expression prefix: "#{". */
public static final String DEFAULT_EXPRESSION_PREFIX = "#{";
/** Default expression suffix: "}". */
public static final String DEFAULT_EXPRESSION_SUFFIX = "}";
private String expressionPrefix = DEFAULT_EXPRESSION_PREFIX;
private String expressionSuffix = DEFAULT_EXPRESSION_SUFFIX;
private ExpressionParser expressionParser; // 它的最终值是SpelExpressionParser
// 每个表达式都对应一个Expression,这样可以不用重复解析了~~~
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>(256);
// 每个BeanExpressionContex都对应着一个取值上下文~~~
private final Map<BeanExpressionContext, StandardEvaluationContext> evaluationCache = new ConcurrentHashMap<>(8);
// 匿名内部类 解析上下文。 和TemplateParserContext的实现一样。个人觉得直接使用它更优雅
// 和ParserContext.TEMPLATE_EXPRESSION 这个常量也一毛一样
private final ParserContext beanExpressionParserContext = new ParserContext() {
@Override
public boolean isTemplate() {
return true;
}
@Override
public String getExpressionPrefix() {
return expressionPrefix;
}
@Override
public String getExpressionSuffix() {
return expressionSuffix;
}
};
// 空构造函数:默认就是使用的SpelExpressionParser 下面你也可以自己set你自己的实现~
public StandardBeanExpressionResolver() {
this.expressionParser = new SpelExpressionParser();
}
public void setExpressionParser(ExpressionParser expressionParser) {
Assert.notNull(expressionParser, "ExpressionParser must not be null");
this.expressionParser = expressionParser;
}
// 解析代码相对来说还是比较简单的,毕竟复杂的解析逻辑都是SpEL里边~ 这里只是使用一下而已~
@Override
@Nullable
public Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException {
if (!StringUtils.hasLength(value)) {
return value;
}
try {
Expression expr = this.expressionCache.get(value);
if (expr == null) {
// 注意:此处isTemplte=true
expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext);
this.expressionCache.put(value, expr);
}
// 构建getValue计算时的执行上下文~~~
// 做种解析BeanName的ast为;org.springframework.expression.spel.ast.PropertyOrFieldReference
StandardEvaluationContext sec = this.evaluationCache.get(evalContext);
if (sec == null) {
// 此处指定的rootObject为:evalContext --> BeanExpressionContext
sec = new StandardEvaluationContext(evalContext);
// 此处新增了4个,加上一个默认的 所以一共就有5个属性访问器了
// 这样我们的SpEL就能访问BeanFactory、Map、Enviroment等组件了~
// BeanExpressionContextAccessor表示调用bean的方法~~~~(比如我们此处就是使用的它) 最终执行者为;BeanExpressionContext 它持有BeanFactory的引用嘛~
// 如果是单村的Bean注入,最终使用的也是BeanExpressionContextAccessor 目前没有找到BeanFactoryAccessor的用于之地~~~
// addPropertyAccessor只是:addBeforeDefault 所以只是把default的放在了最后,我们手动add的还是保持着顺序的~
// 注意:这些属性访问器是有先后顺序的,具体看下面~~~
sec.addPropertyAccessor(new BeanExpressionContextAccessor());
sec.addPropertyAccessor(new BeanFactoryAccessor());
sec.addPropertyAccessor(new MapAccessor());
sec.addPropertyAccessor(new EnvironmentAccessor());
// setBeanResolver不是接口方法,仅仅辅助StandardEvaluationContext 去获取Bean
sec.setBeanResolver(new BeanFactoryResolver(evalContext.getBeanFactory()));
sec.setTypeLocator(new StandardTypeLocator(evalContext.getBeanFactory().getBeanClassLoader()));
// 若conversionService不为null,就使用工厂的。否则就使用SpEL里默认的DefaultConverterService那个
// 最后包装成TypeConverter给set进去~~~
ConversionService conversionService = evalContext.getBeanFactory().getConversionService();
if (conversionService != null) {
sec.setTypeConverter(new StandardTypeConverter(conversionService));
}
// 这个很有意思,是一个protected的空方法,因此我们发现若我们自己要自定义BeanExpressionResolver,完全可以继承自StandardBeanExpressionResolver
// 因为我们绝大多数情况下,只需要提供更多的计算环境即可~~~~~
customizeEvaluationContext(sec);
this.evaluationCache.put(evalContext, sec);
}
return expr.getValue(sec);
} catch (Throwable ex) {
throw new BeanExpressionException("Expression parsing failed", ex);
}
}
//Spring留给我们扩展的SPI
protected void customizeEvaluationContext(StandardEvaluationContext evalContext) {
}
}
如上,整个@Value的解析过程至此就全部完成了。可能有小伙伴会问:怎么不见Resource这种注入呢?其实,从上面不难看出,这个是ConversionService去做的事,它能够把一个字符串转换成Resource对象,仅此而已
总得来说@Value它自己做的事本身还是非常单一的:依赖注入,只是它把众多功能都很好的像插件一样插拔进来了,从而对用户很友好的显示了显它的神通广大~
需要注意的是,在整个依赖的解析过程中,有两个非常重要的接口:BeanExpressionResolver和AutowireCandidateResolver都扮演着重要角色。
自定义扩展@Value的功能
因为Spring上下文默认是这么注册的beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));,所以我们的思路应该是替换掉它。
@Value中使用${}读取不存在的key时,不报错而是原样输出的问题
我们通用的一个观念是这样的:若使用@Value("${app.full2}")给字段赋值,若key不存在启动应该报错,这样才符合我们预期。但是Spring默认是启动并没有报错,而且给我原样输出了。
解析占位符这款发生在:DefaultListableBeanFactory#resolveDependency中,里面有一句代码是:
@Override
public String resolveEmbeddedValue(String value) {
if (value == null) {
return null;
}
String result = value;
for (StringValueResolver resolver : this.embeddedValueResolvers) {
result = resolver.resolveStringValue(result);
if (result == null) {
return null;
}
}
return result;
}
同样的代码不同的现象,问题就出现在这里resolver.resolveStringValue(result)。这里总结一下,给AbstractBeanFactory设置处理器的地方有两个:
第一处:
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext, DisposableBean {
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
...
// 若没有指定EmbeddedValueResolver, 这里就用个匿名函数实现 做个保底嘛~~~
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(new StringValueResolver() {
@Override
public String resolveStringValue(String strVal) {
return getEnvironment().resolvePlaceholders(strVal);
}
});
}
//...
}
}
第二处:
//@since 3.1
public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfigurer implements BeanNameAware, BeanFactoryAware {
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) {
...
// Spring3.0后 加载完配置文件后,会把这个处理器放进Bean工厂里面去。
// New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}
}
它有两个子类:PropertyPlaceholderConfigurer,它的StringValueResolver实现为一个内部类实现的。另一个子类PropertySourcesPlaceholderConfigurer,它的实现StringValueResolver为一个lambda表达式。
它俩有个共同点:最终的解析都依赖于PropertyPlaceholderHelper并且,并且ignoreUnresolvablePlaceholders属性均为默认的fasle。
所以得出结论:若你手动配置过上面两个PlaceholderConfigurerSupport子类Bean,并且没有改变过ignoreUnresolvablePlaceholders这个值,那你最终会使用它们去解析${}占位符,从而如果找不到key就启动报错了。
但是若你没有手动配置过,那将最终交给AbstractBeanFactory的那个内部类处理,也就是这句话:return getEnvironment().resolvePlaceholders(strVal);而它最终解析如下:
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
// 两个接口方法。显然AbstractBeanFactory默认实现为这个方法,而并非Required的~~~
@Override
public String resolvePlaceholders(String text) {
return this.propertyResolver.resolvePlaceholders(text);
}
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
return this.propertyResolver.resolveRequiredPlaceholders(text);
}
}
public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {
// 调用的resolvePlaceholders这个方法,所以默认情况下 即使key不存在 也是没关系的
@Override
public String resolvePlaceholders(String text) {
if (this.nonStrictHelper == null) {
this.nonStrictHelper = createPlaceholderHelper(true);
}
return doResolvePlaceholders(text, this.nonStrictHelper);
}
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
if (this.strictHelper == null) {
this.strictHelper = createPlaceholderHelper(false);
}
return doResolvePlaceholders(text, this.strictHelper);
}
}
如上,就能解释了为何有时候你使用@Value找不到key就启动报错,有时候却原样输出呢? 这就是其根本原因。
关于SpringBoot环境下,默认情况下都是key必须存在的,否则启动报错。原因如下:
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class PropertyPlaceholderAutoConfiguration {
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
而在spring.factories文件里配置有它:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
总之:使用@Value注入时请保证key是存在的,否则建议请使用defaultValue语法处理
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2021-11-28 你知道为什么HashMap是线程不安全的吗?
2021-11-28 Java序列化与反序列化三连问:是什么?为什么要?如何做?
2021-11-28 什么情况用ArrayList or LinkedList呢?
2021-11-28 你能谈谈HashMap怎样解决hash冲突吗
2021-11-28 谈谈这几个常见的多线程面试题
2021-11-28 你能说说进程与线程的区别吗
2021-11-28 谈谈 Redis 的过期策略