上一章讲解了SpringBoot中的 AutoConfiguration自动装配,而这一章就来讲讲自动装配时会用到的Spring三大特性之一的IOC控制反转。
使用过Spring的人都熟知,SpringIOC容器可以在对象生成或初始化时就直接将数据注入到对象中,如果对象A的属性是另一个对象B,还可以将这个对象B的引用注入到注入到A的数据域中.
如果在初始化对象A的时候,对象B还没有进行初始化,而A又需要对象B作为自己的属性,那么就会用一种递归的方式进行注入,这样就可以把对象的依赖关系清晰有序的建立起来.
IOC容器解决问题的核心就是把创建和管理对象的控制权从具体的业务对象手中抢过来.由IOC容器来管理对象之间的依赖关系,并由IOC容器完成对象的注入.这样就把应用从复杂的对象依赖关系的管理中解放出来,简化了程序的开发过程.
springboot项目从一个main方法开始,main方法将会调用SpringApplication的run方法开始springboot的启动流程。所以,本文即从构造SpringApplication对象开始。
@SpringBootApplication
public class SpringBootLearnApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootLearnApplication.class, args);
}
}
我们跟进SpringApplication的run方法
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class[]{primarySource}, args);
}
这是一个静态方法,入参有两个:
1)main方法所在的类,该类后续将被作为主要的资源来使用,比如通过该类获取到basePackage;
2)main方法的命令行参数,命令行参数可以通过main传入,也就意味着可以在springboot启动的时候设置对应的参数,比如当前是dev环境、还是production环境等。
第2行代码,run方法将调用另外一个内部run方法,并返回一个ConfigurableApplicationContext,预示着spring容器将在后续过程中创建。
跟进另一个run方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return (new SpringApplication(primarySources)).run(args);
}
run方法中先是构造了一个SpringApplication实例对象,而后调用了SpringApplication的成员方法run,这个run方法将包含springboot启动流程的核心逻辑。
让我们继续跟进另一个run方法
public ConfigurableApplicationContext run(String... args) {
// 声明一个Context容器
ConfigurableApplicationContext context = null;
// 获取监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 调用监听器的启动
listeners.starting();
try {
// 创建并配置Environment(这个过程会加载application配置文件)
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 根据应用类型创建对应的Context容器
context = createApplicationContext();
// 刷新Context容器之前的准备
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新Context容器
refreshContext(context);
// 刷新Context容器之后处理
afterRefresh(context, applicationArguments);
// Context容器refresh完毕发布
listeners.started(context);
// 触发Context容器refresh完以后的执行
callRunners(context, applicationArguments);
} catch (Throwable ex) {}
try {
// Context启动完毕,Runner运行完毕发布
listeners.running(context);
} catch (Throwable ex) {}
return context;
}
run方法中,prepareContext和afterRefresh之间的refreshContext方法正是ioc容器刷新的入口方法。
打开refreshContext方法
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException var3) {
}
}
this.refresh((ApplicationContext)context);
}
跟进this.refresh((ApplicationContext)context);
@Deprecated
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);
this.refresh((ConfigurableApplicationContext)applicationContext);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}
我们可以看到最终调用的是参数未ConfigurableApplicationContext的refresh方法。而ConfigurableApplicationContext是一个接口
我们可以看看他的继承关系。
可以看到最终最终由AbstractApplicationContext进行实现refresh方法。
继续跟进AbstractApplicationContext的refresh方法。
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
// 刷新前准备,设置flag、时间,初始化properties等
this.prepareRefresh();
// 获取ApplicationContext中组合的BeanFactory
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
// 设置类加载器,添加后置处理器等准备
this.prepareBeanFactory(beanFactory);
try {
// 供子类实现的后置处理
this.postProcessBeanFactory(beanFactory);
// 调用Bean工厂的后置处理器
this.invokeBeanFactoryPostProcessors(beanFactory);
// 注册Bean的后置处理器
this.registerBeanPostProcessors(beanFactory);
// 初始化消息源
this.initMessageSource();
// 初始化事件广播
this.initApplicationEventMulticaster();
// 供之类实现的,初始化特殊的Bean
this.onRefresh();
// 注册监听器
this.registerListeners();
// 实例化所有的(non-lazy-init)单例Bean
this.finishBeanFactoryInitialization(beanFactory);
// 发布刷新完毕事件
this.finishRefresh();
} catch (BeansException var9) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
}
this.destroyBeans();
this.cancelRefresh(var9);
throw var9;
} finally {
this.resetCommonCaches();
}
}
}
首先我们得知道初始化过程,annotation或者xml中Bean的配置 --> 内存中的BeanDefinition --> Bean的过程。也就是实际的配置,转化成内存中的配置对象,再根据配置对象转化成具体的实例对象。
而从annotation或者xml的Bean配置 --> 内存中的BeanDefinition的过程。这个过程的实现在调用Bean工厂的后置处理器的时候完成,也就是invokeBeanFactoryPostProcessors方法。
让我们跟进invokeBeanFactoryPostProcessors方法
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory,this.getBeanFactoryPostProcessors();
//......
}
这里先获取了所有后置处理器,然后调用处理。再跟进PostProcessorRegistrationDelegate的invokeBeanFactoryFactoryPostProcessors方法
该方法很长,我们删减掉大部分内容
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory,
List<BeanFactoryPostProcessor> beanFactoryPostProcessors
) {
//
if (beanFactory instanceof BeanDefinitionRegistry) {
//
while (reiterate) {
// 调用BeanDefinition注册的后置处理器
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
//
}
//
} else {
//
}
//
}
可以看到,调用后置处理器的时候会调用到注册BeanDefinition的后置处理器。也就是从这里开始作为BeanDefinition的注册入口
跟进invokeBeanDefinitionRegistryPostProcessors
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors,
BeanDefinitionRegistry registry
) {
for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}
通过debug 我们看到了ConfigurationClassPostProcessor也就是它完成BeanDefinition注册这项工作的
我们跟进ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry方法
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
//
} else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
// 默认仅有主类被添加
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
//
// 解析被 @Configuration 注解的类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory,
this.problemReporter,
this.environment,
this.resourceLoader,
this.componentScanBeanNameGenerator,
registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
//
do {
// 解析的核心方法
parser.parse(candidates);
parser.validate();
//
candidates.clear();
//
} while (!candidates.isEmpty());
//
}
该主类将被作为一个配置类被解析,解析器即ConfigurationClassParser。
我们跟进parse方法看看
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 主类的解析将从这里进入
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
} else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
} else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
} catch (BeanDefinitionStoreException ex) {}
catch (Throwable ex) {}
}
this.deferredImportSelectorHandler.process();
}
继续跟进parse方法
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}
可以看到主类作为配置类的解析过程将从processConfigurationClass这里开始
跟进processConfigurationClass方法
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
//
// 由main方法所在的主类开始,向超类逐层向上递归解析
SourceClass sourceClass = asSourceClass(configClass);
do {
// 这里包含了解析单个配置类的核心逻辑
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
} while (sourceClass != null);
//
}
我们注意到,doProcessConfigurationClass方法将会完成解析的主要工作,但是又会返回一个新的sourceClass用于解析。而这个新的sourceClass会是当前上一个sourceClass的父类。所在解析过程是一个递归过程,由主类开始,向超类逐层向上递归解析处理。
继续跟进doProcessConfigurationClass方法,我们看看这个核心的解析逻辑。代码量对较多,我们只关注两个点
1)@ComponentScan注解解析,扫描并注册BeanDefinition
2)获取超类向上递归
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {
//
// 处理@ComponentScan注解
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
// 遍历@ComponentScan的属性值
for (AnnotationAttributes componentScan : componentScans) {
// 解析扫描
Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
//
}
}
//
// 判断是否有超类
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// 返回待解析的超类
return sourceClass.getSuperClass();
}
}
// 没有超类,则解析完毕
return null;
}
首先我们main方法所在的主类是被@SpringbootApplication注解所标注的,而@SpringbootApplication组合了@ComponentScan。所谓解析主类的时候将会处理@ComponentScan注解。解析@ComponentScan的主要工作的实现由ComponentScanAnnotationParser这个解析器来完成。通常这个解析器完成之后,被扫描到的BeanDefinition将会被注册到BeanFactory当中。
doProcessConfigurationClass方法的最后一部分是从当前被解析的类元数据中获取超类,如果超类存在且需要被解析那么就当做返回值返回回去,从而被外层的方法给递归处理。
到这里,ioc容器refresh过程部分就结束了。我们略过不少东西,将解析主类、解析@ComponentScan扫描Bean定义、注册到BeanFactory这个主要的流程过了一遍。当然,在这里可能还存在一个比较困惑的点。前面的文章中,我们提过几次:配置 -> BeanDefinition -> Bean这样一个过程。ioc的refresh过程却只有从配置 -> BeanDefinition这样一个过程,那么BeanDefinition -> Bean这个过程又在哪里呢?
上文讲到refresh只是把BeanDefinition注册到BeanFactory中,而不是把Bean注册到BeanFactory中。
而是在我们调用上下文的getBean的时候才会去根据BeanDefinition生成
@Override
public Object getBean(String name) throws BeansException {
//
return getBeanFactory().getBean(name);
}
上下文的getBean方法把功能实现委托给了BeanFactory,跟进AbstractBeanFactory的getBean方法
@Override
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
跟进doGetBean方法
protected <T> T doGetBean(
final String name,
@Nullable final Class<T> requiredType,
@Nullable final Object[] args,
boolean typeCheckOnly) throws BeansException {
final String beanName = transformedBeanName(name);
Object bean;
// 如果拿到已经注册的单例Bean,直接返回结果
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
//
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
//
try {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
//
// 创建单例
if (mbd.isSingleton()) {
// 回调创建
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
//
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
// 每次创建
prototypeInstance = createBean(beanName, mbd, args);
} finally {
//
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
//
}
} catch (BeansException ex) {
//
}
}
//
return (T) bean;
}
该方法的逻辑是先去单例的缓存中找,如果找得到直接返回。如果找不到,那么判断是单例还是原型,如果是单例创建并缓存,如果是原型那么每次都创建新的。
跟进创建单例的时候的getSingleton方法
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
// 内置锁控制
synchronized (this.singletonObjects) {
// 双重校验
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//
boolean newSingleton = false;
//
try {
// 回调创建Bean
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
//
}
catch (BeanCreationException ex) {
//
} finally {
//
}
if (newSingleton) {
// 添加单例到缓存中
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}
这里采用双重校验机制控制了单例,如果二次校验的时候发现缓存中没有Bean,那么就会回调创建的方法去创建一个Bean,然后再注册到本地堆缓存当中。
创建实现委托给了createBean方法,该方法的实现属于AbstractAutowireCapableBeanFactory,跟进该类的CreateBean方法
@Override
protected Object createBean(
String beanName,
RootBeanDefinition mbd,
@Nullable Object[] args) throws BeanCreationException {
//
RootBeanDefinition mbdToUse = mbd;
//
try {
// 创建Bean实例
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
//
return beanInstance;
} catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {
//
} catch (Throwable ex) {
//
}
}
继续跟进doCreateBean
protected Object doCreateBean(
final String beanName,
final RootBeanDefinition mbd,
final @Nullable Object[] args) throws BeanCreationException {
// 创建Bean实例对象
BeanWrapper instanceWrapper = null;
//
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//
try {
// 自动注入
populateBean(beanName, mbd, instanceWrapper);
//
} catch (Throwable ex) {
//
}
//
return exposedObject;
}
createBeanInstance创建实例,到这里,BeanDefinition就被初步创建成为了一个Bean实例对象。
前面我们说到,doCreateBean有两个步骤
1)创建Bean实例对象
2)自动注入
接下来,我们跟进populateBean方法看看当前这个创建好的Bean实例实怎么注入其它Bean的
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
//
// 获取待注入的property,配置文件中配置的<property>将在这里被处理
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
// 按照名字或者类型获取属性,这里会进行递归
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// 按照名字获取属性
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// 按照类型获取属性
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}
boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
PropertyDescriptor[] filteredPds = null;
if (hasInstAwareBpps) {
if (pvs == null) {
pvs = mbd.getPropertyValues();
}
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
// 后置处理器处理@Autowired @Resource等注解
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
}
// 注入<property>属性
if (pvs != null) {
applyPropertyValues(beanName, mbd, bw, pvs);
}
}
我们看到populateBean主要做两件事,获取属性值,然后把属性值给注入到Bean里面去。
我们重点关注后置处理器处理@Autowired @Resource注解的逻辑。
跟进AutowiredAnnotationBeanPostProcessor类的postProcessPropertyValues方法
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) {
return postProcessProperties(pvs, bean, beanName);
}
跟进postProcessProperties方法
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 获取当前Bean的元数据,将包含@Autowired等注解的标注的待注入元素
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
// 注入元素
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}
后置处理属性值包含两件事,找到当前Bean被@Autowired等注解标注的待注入的元素,然后注入相应的到元素。
跟进findAutowiringMetadata方法
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
.
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
if (metadata != null) {
metadata.clear(pvs);
}
// 构造元数据
metadata = buildAutowiringMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}
这里找到注解@Autowired的Field以后包装成Element,然后向父类递归,最后包装成元数据
我们回到postProcessProperties方法以后,再跟进inject注入方法看看
protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs)
throws Throwable {
if (this.isField) {
Field field = (Field) this.member;
ReflectionUtils.makeAccessible(field);
// 反射设置值,这里的取值会对依赖进行递归处理
field.set(target, getResourceToInject(target, requestingBeanName));
} else {
// 省略
}
}
这里主要是对Bean的Field的一个反射来设置值,值的获取将会进行递归处理
private InjectionMetadata buildResourceMetadata(final Class<?> clazz) {
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;
do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
ReflectionUtils.doWithLocalFields(targetClass, field -> {
if (webServiceRefClass != null && field.isAnnotationPresent(webServiceRefClass)) {
//
}
else if (ejbRefClass != null && field.isAnnotationPresent(ejbRefClass)) {
//
}
// 如果注解了@Resource
else if (field.isAnnotationPresent(Resource.class)) {
//
if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
// 添加element
currElements.add(new ResourceElement(field, field, null));
}
}
});
//
elements.addAll(0, currElements);
// 向父类递归
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
return new InjectionMetadata(clazz, elements);
}
元数据返回以后的流程和@Autowired也是一样的。
本文粗略地过了一下ioc依赖注入的过程,从BeanDefinition --> Bean的过程。我们一开始创建了Bean的实例,然后再通过递归解析依赖注入处理把Bean之间的关系结合处理。在最后还提了一下@Autowired和@Resource的后置处理器。
依赖注入的过程相对来说还是很复杂的,包含了非常多的细节处理。但是我们可以简单地去概括一下它,整个依赖注入的过程就是创建Bean,并建立Bean之间的关系。
以上内容均来自下面文章,本人只是自己做了个总结,方便以后学习,如有侵权,请告知我,马上删除!
springboot启动流程(目录) - __lay - 博客园 (cnblogs.com)
大家好,又见面了,我是你们的朋友全栈君。最近开发中用到了longtext这种字段。在mysql中该字段的最大长度为4G 如下图所示 开发中遇到的一个问题就是。例如有个article表,然后我们的页面要将数据以列表的形式展示到前端(只显示几个字段,如作者,标题等等,例如放到table中显示多条记录),但是是将该表中的所有信息都查出来,然后当用户点击某条记录的时候,会跳到详情页,在显示出详细的信息。这样当数据量比较多的时候,或者文本的内容比较大的时候,就出现问题了。打开页面,页面就会一直加载,数据量越大,加载时间就越长,然后才会显示数据列表。这会严重影响使用效果。 解决方法: 当然是sql语句的问题了,当像上面这样查询整个列表的时候,可以不查询longtext这个字段,将其他的字段查询出来。然后当用户点击某条数据时,再根据该条数据的id到数据库去单查这条数据,这时再将longtext给查出来即可。说到这里,还要说一种情况,就是有时候从数据库中查到的数据封装到实体类中,怎么也取不到某个字段的值,就是null。这个时候要看看sql语句,返回的结果集中是否将该字段封装并且映射到该类对应的字段上
一、前言目前最流行的两大前端框架,React和Vue,都不约而同的借助VirtualDOM技术提高页面的渲染效率。那么,什么是VirtualDOM?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解VirtualDOM的创建过程,并实现一个简单的Diff算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的VirtualDOM。敲单词太累了,下文VirtualDOM一律用VD表示。这是VD系列文章的第四篇,以下是本系列其它文章的传送门: 你不知道的VirtualDOM(一):VirtualDom介绍 你不知道的VirtualDOM(二):VirtualDom的更新 你不知道的VirtualDOM(三):VirtualDom更新优化 你不知道的VirtualDOM(四):key的作用 你不知道的VirtualDOM(五):自定义组件 你不知道的VirtualDOM(六):事件处理&异步更新今天,我们继续在之前项目的基础上进行优化。用过React或者Vue的朋友都知道在渲染数组元素的时候,编译器会提醒加上key这个属性,那么key是用来做什么的呢?二、key的作用在
参考链接:Python中的numpy.argminimportnumpyasnp np.random.seed(100) #多次运行得到相同的结果,设置随机数的种子x=np.random.random(50)xnp.min(x) #x的最小值np.argmin(x) #x的最小值的索引x[4] #x的第4位的索引值np.max(x) #x的最大值np.argmax(x) #x的最大值的索引x[36] #x的第36位的索引值ind=np.argwhere(x>0.5) #x>0.5的索引indx[ind] #x的索引对应的值 x=np.arange(10)xnp.random.shuffle(x) #乱序xnp.sort(x) #排序ind=np.argsort(x) #按索引排序indx[ind] #x的索引对应的值ind[:3] #索引的切片,第0到第3,不包括第3x[ind[:3]] #按索引的切片取值,第0到第3,不包括第3x[ind[3:]] #按索引的切片取值,第3到最后x[ind[-3:]] #按索引的切片取值,最后3个np.
C++赋值语句 在上一节说到,赋值语句是由赋值表达式和一个分号组成的,这一节来详细介绍下赋值语句,它是C++的重要组成部分。C++的赋值语句具有其他高级语言的赋值语句的功能,不同的是C++的赋值号=是一个运算符,比如在C++中可以这样写 x=y=z=m=n;复制在其他大多数语句中,这样写是不合法的。在C++中,赋值表达式可以包括在其他表达式中//如 if((x=y)>1) { cout<<"a>1"<<endl; }复制在上面的if语句中x=y不是赋值语句而是赋值表达式,是合法的。C++把赋值语句和赋值表达式做了区别,增加了表达式的种类。案例:当x小于0时,输出y=10;x大于0时,输出y=-1;x等于0时,输出y=0。#include<iostream> usingnamespacestd; intmain()//主函数 { intx,y;//定义变量 cin>>x;//键盘输入x if(x<0)//做判断x小于0 { y=-10; } elseif(x>0)//做判断x大于0 { y=-1;
写在前面本期大猫课堂将继续上期的RTricks系列。在这一期中,大猫将向大家介绍“Gaps&IslandsProblem”。这是在处理时间序列或者基因组数据中常见的一项任务。虽然常见,但要高效解决可不容易哦!PS:大猫发现好多人给大猫留了言,但是因为超过48小时以后就不能回复大家了。所以如果小伙伴们有问题,可以再试着给大猫留言哦,大猫看到一定第一时间回复哈。提出问题话说有个擅长使用SQL的小伙伴在StackOverflow上提出了这样一个问题,他说,Gaps&Islands问题在SQL中能很容易解决,那么在R中也能高效解决吗?慢着——什么是Gaps&Islands问题?这个小伙伴举了个栗子来说明,让我们看一下。假如我们有如下数据集:这是一个记录时间的数据集。每一行都有ID、起始时间(stime)、结束时间(etime)。我们可以发现,第1至4行的时间是有重叠的,其中最早的起始时间是(2014-01-1508:00:00),最晚的结束时间是(2014-01-1511:00:00)。而第5与第6行的时间也有重叠。Gaps&Islands问题就是说:能不能把时
近日Kylinv2.6.4版本发布,包含很多问题修复与各种改进。翻阅三年前写的Kylin测试文档,当时版本还是1.5.3。近两年Kylin版本迅速迭代,社区不断发展,已经成为Hadoop生态中不可或缺的OLAP引擎。01Kylin介绍ApacheKylin(麒麟)是由eBay开源的分布式分析引擎,提供Hadoop/Spark之上的SQL查询接口及多维分析(OLAP)能力以支持超大规模数据。Kylin能够实现海量数据的秒级甚至亚秒级查询,主要依赖其预计算与构建Cube的能力。Kylin底层数据存储在HBase中,数据输入与cubebuilding主要是Hive、Kafka,或者JDBC数据源(v2.3.0+版本),如下图所示:02Kylin功能和特性超快的大数据OLAP引擎,能够降低百亿数据规模下的查询延时。SQL查询能力。支持ANSISQL查询接口,提供了大部分SQL查询功能。交互式查询能力。查询延时控制在亚秒级,为Hadoop提供交互式查询能力。多维立方体。使用kylin为百亿以上数据集定义数据模型并构建立方体。实时OLAP能力,Kylin可以在数据产生时进行实时处理,用户可以在秒级
Linux系统的hosts文件存储在/etc/hosts下,它在IP地址、主机名、域名和机器别名之间创建静态关联。然后,您的Linode会为这些关联提供比必须由DNS解析的主机名或域名更高的优先级。host代码示例有多种方法使用hosts文件,您可以按照自己的想法来设置其关联。以下是一些例子。将别名mywebsite映射到给定的IP地址。这通常在域名开始使用之前,在开发期间预览站点时完成。203.0.113.10mywebsite将域名example.com映射到给定的IP地址。这在托管Web或邮件服务器时很有用。203.0.113.10example.com结合上面的两个选项,可以使用域名和别名映射到同一IP地址:203.0.113.10example.commywebsite将别名backupserver映射到给定的私有IPv6地址:fe80::f03c:91ff:fe24:3a2fbackupserver阻止进出域名example.com的所有流量。这经常用于通过hosts文件进行内容过滤或阻止广告。0.0.0.0example.com设置完全限定的域名(FQDN)。在下面的示例
AiTechYun编辑:chuxGlobalX推出了一款针对大数据和人工智能(AI)领域的ETF。在全球X未来分析技术ETF(AIQ)对参与这些技术的开发和使用的公司进行投资。AIQ在Nasdaq上市,费用率为0.68%。招股说明书将大数据和人工智能描述为互补的技术主题,主要是通过人工智能技术分析大量数据。该基金的指数从发达市场中挑选其组成部分,重点关注那些市值至少达到5亿美元,满足最低流动性要求的组合。公司可以分为两大类:人工智能开发人员或人工智能以及大数据分析硬件。开发人员包括提供实施人工智能技术,或允许客户使用人工智能来分析大数据的产品和服务的公司。招股说明书称,硬件类别包括生产半导体和存储器等人工智能组件的公司,以及涉及量子计算技术开发的公司。该指数中的公司对其在这些业务领域的风险敞口进行评级,并且必须在指数中包含的其中一个类别中达到最低分数。根据基金文件,选定的公司采用修正市场资本化方法进行加权,每年进行重组。这不是第一个针对大数据的ETF,但它是目前唯一一家支持交易的ETF。该PureFundsISE大数据ETF(BIGD)在2015年推出。然而,AIQ与GlobalX的全
当我们听到卷积神经网络(ConvolutionalNeuralNetwork,CNNs)时,往往会联想到计算机视觉。CNNs在图像分类领域做出了巨大贡献,也是当今绝大多数计算机视觉系统的核心技术,从Facebook的图像自动标签到自动驾驶汽车都在使用。最近我们开始在自然语言处理(NaturalLanguageProcessing)领域应用CNNs,并取得了一些引人注目的成果。我将在本文中归纳什么是CNNs,怎样将它们应用于NLP。CNNs背后的直觉知识在计算机视觉的用例里更容易被理解,因此我就先从那里开始,然后慢慢过渡到自然语言处理。什么是卷积运算?对我来说,最容易的理解方式就是把卷积想象成作用于矩阵的一个滑动窗口函数。这么说有些拗口,但是用动画显示就很直观了。3x3的滤波器做卷积运算。图片来源:http://deeplearning.stanford.edu/wiki/index.php/Feature_extraction_using_convolution把左侧的矩阵想象成一幅黑白图像。每一个元素对应一个像素点,0表示黑点,1表示白点(灰度图的像素值一般是0~255)。移动窗口又
转载请注明出处:http://www.cnblogs.com/fangkm/p/4374610.html 前面两篇文章介绍WebRTC的运行流程和使用框架接口,接下来就开始分析本地音视频的采集流程。由于篇幅较大,视频采集和音频采集分成两篇博文,这里先分析视频采集流程。分析的时候先分析WebRTC原生的视频采集流程,再捎带提一下Chromium对WebRTC视频采集的适配,这样能更好地理解WebRTC的接口设计。 1.WebRTC原生视频采集 在介绍视频设备的采集之前,首先要分析一下WebRTC的DeviceManager结构,因为视频采集的抽象接口VideoCapturer的WebRTC原生实现就是通过它来创建的。这个类的功能还包括枚举音视频设备的相关信息等。结构如下: 限于篇幅,该UML中没有标出DeviceManagerInterface接口的所有功能接口,具体包括:获取音频输入/输出设备列表、获取视频输入设备列表、根据设备信息创建VideoCapturer视频采集对象等。由于获取硬件设备列表,涉及到平台相关的调用,在Windows平台下的实现是Win32Device
聚合函数count,max,min,avg,sum...selectcount(*)fromT_EmployeeselectMax(FSalary)fromT_Employee 排序ASC升序DESC降序select*fromT_EmployeeorderbyFage 先按年龄降序排列。如果年龄相同,则按薪水升序排列select*fromT_EmployeeorderbyFAgeDESC,FSalaryASC orderby要放在where子句之后 通配符过滤通配符过滤用like单字符通配符‘_'多字符通配符‘%'select*fromT_EmployeewhereFNamelike'_erry' NULL是不知道的意思,而不是没有 SQLISNULL()、NVL()、IFNULL()和COALESCE()函数请看下面的"Products"表: P_IdProductNameUnitPriceUnitsInStockUnitsOnOrder1computer69925152printer365363telephone28015957 假如"UnitsOnOrder"是可选的,而且可以包
【题目链接】:http://acm.hdu.edu.cn/contests/contest_showproblem.php?pid=1008&cid=460 【解题思路】: 这题初看起来情况很复杂,动态性很高,但注意到一个很特殊的地方,就是他每次都是以MI这个为起点的,只要先从这个往下推,看得出的串有什么规律就行 了。这类题就是推导出数学式子,然后利用数学公式推导出条件。 1) 由MI往下推可得MII,MIIII,MIIIIIIII……等后跟着2^n个I,而任意位置3个I可组成一个U,如果不消去UU,则以后每次翻倍后,把U换成I,则必然有2^n个I; 2)而每次消去两个U,则以后再经翻倍,则少2^n-cur=6*k,所以判断只要将M后的UI序列中U换成3个I,统计下I的个数,若能满足存在某个n使2^n减去I的个数是6的倍数,
芯片为STM32F407,利用仿真完成设计,末尾为仿真图。 线程一通过DS18B20自动测量温度,当温度大于30℃或小于20℃时通过使能PC6串口使LED1灯发光进行报警; 线程二通过RTC时钟获取现实时间,每天的凌晨24点自动投食,通过使能PC7串口控制LED2灯发光0.5s; LCD实时显示当前温度与学生姓名及学号内容; 用EMWIN取模软件将汉字取模并通过LCD显示; 通过RT-THREAD多线程控制,线程一控制温度的相关内容,线程二控制RTC时钟及LCD显示屏内容。 移植时需要注意对于外设串口的配置 下述为thread线程的代码。#include"threads.h"#include"stm32f4xx_hal.h"#include"gpio.h" /*USERCODEBEGINIncludes*/#include"ILI9341B8.H"#include"DS18B20.H"RTC_DateTypeDefSDATE;RTC_TimeTypeDefSTIME;externuint8_tdata[5];externint16_tTEMP;uint8_tdate[2];//exte
小时候都知道每天写日记是个好习惯,慢慢发现自己忘记了这些习惯,好久没有给这干枯的博客添加一点新意了,这回也冒出个小芽来刷新一下博客。 今天写的一点也不复杂,就是回顾一些老的知识而已,也算是记一个笔记,好了闲话不多说了,开始今天的主题吧。 关于拍照,这里不是自己实现拍照,是调用系统拍照,很简单的,可是有些时候我也遇到一个问题,就是我没有主动压缩,系统却自动帮我压缩了,可是我需要这些高清的图片,解决方式网上也有说,但是我做的是自己的笔记,所以也不在乎赘余,最起码我是经过验证后,才写我笔记的。 下面是系统默认压缩图片的调用方式 1privatestaticfinalintTAKE_PICTURE=0x000001; 2publicvoidphoto(){ 3IntentopenCameraIntent=newIntent(MediaStore.ACTION_IMAGE_CAPTURE); 4startActivityForResult(openCameraIntent,TAKE_PICTURE); 5}复制 @Override protectedvoidonActivityResult
上一篇vuex其实超简单,只需3步简单介绍了vuex的3步入门,不过为了初学者容易消化,我削减了很多内容,这一节,就是把少掉的内容补上,如果你没看过上篇,请戳链接过去先看一下再回来,否则,你会觉得本文摸不着头脑. 纯属个人经验,难免有不正确的地方,如有发现,欢迎指正! 还是一样,本文针对初学者. 一、Getter 我们先回忆一下上一篇的代码 computed:{ getName(){ returnthis.$store.state.name } } 复制 这里假设现在逻辑有变,我们最终期望得到的数据(getName),是基于this.$store.state.name上经过复杂计算得来的,刚好这个getName要在好多个地方使用,那么我们就得复制好几份. vuex给我们提供了getter,请看代码(文件位置/src/store/index.js) importVuefrom'vue' importVuexfrom'vuex' Vue.use(Vuex) exportdefaultnewVuex.Store({ //类似vue的data state:{ name:'oldNa
给定起点和终点,直线的光栅化算法要找出哪些像素应该被着色。简单起见,这里假设。 一、直观的方法 当直线的斜率时,直线在向的变化速率小于在方向上的变化速率,因此可以遍历到间的每一个,计算对应的值并将其四舍五入画点。算法伪代码如下: k=(y2-y1)/(x2-x1) y=y1 forx=x1tox2 draw_point(x,round(y)) y+=k复制 而当时,必须交换和的地位——遍历的取值,每次迭代计算对应的。 二、Bresenham算法 假设,每当时,对应的要绘制点的坐标要么保持不变,要么增加1或减小1(简单起见,这里假设,即只可能不变或增加1)。我们需要给出一个判断条件,判断什么时候该增加1、什么时候该保持不变。 常见的方法是使用中点进行比较(有一种所谓“中点画线法”,其原理和Bresenham算法本质上是相同的)。假设保持不变时绘制的点坐标为,那么增加1
1、AOE-网介绍 我们在学习拓扑排序(如果没学,可以看看这篇博客:拓扑排序详解)的时候,已经接触了什么是AOV-网,AOV-网是优先考虑顶点的思路,而我们也同样可以优先考虑边,这个就是AOE-网的思路。 若在带权的有向无环图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续的时间),则此带权的有向无环图称为AOE网。记住AOE-网只是比AOV-网多了一个边的权重,而且AOV-网一般是设计一个庞大的工程各个子工程实施的先后顺序,而我们的AOE-网就是不仅仅关系整个工程中各个子工程的实施的先后顺序,同时也关系整个工程完成最短时间。 因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。 AOE-网还有一个特点就是:只有一个起点(入度为0的顶点)和一个终点(出度为0的顶点),并且AOE-网有两个待研究的问题: 1.完成整个工程需要的时间 2.哪些活动是影响工程进度的关键 2、名词解释 关键路径:AOE
1、Hadoop几种运行模式?Hadoop的运行模式包括:本地模式,伪分布模式,完全分布模式本地模式:不需要任何集群配置,是在单节点上部署,仅限于调试。伪分布模式:这种模式需要在单独的节点上进行相应的分布式设置,各个组件各自占用进程,模拟分布式各个节点。完全分布模式:需要在多台主机上进行分布式的设置,要求主机之间能互相通信,各个组件分别部署在独立的主机上,真正的实现多节点部署。 2、SCP命令使用scp-r路径user@hostname:路径或scp-ruser1@hostname1:路径user2@hostname2:路径 3、rsync命令使用rsycn[-av]路径user@hostname:路径 或rsycn[-av]user1@hostname1:路径user2@hostname2:路径 4、rsync\SCP不同(1)用rsync做文件的复制要比scp速度快(2)rsync会对两端的文件做对比,有差异的文件会被复制过去,完全相同的会被跳过(3)scp不做对比,直接复制,同名的文件会直接覆盖。 5、DataNode和NameNode进程同时只能工作一个,排查方案。(1)Nam
最近在学习DIBR并尝试实现。感觉网上相关资料比较少,大多还是爬虫,决定自己写一个。 DIBR就是depthimagebasedrendering问题。输入一个视角下的图像和深度图,要求你输出另外一个虚拟视角下的图像(当然两个视角的内外参矩阵都有办法通过已知信息求得)。 总共分三步:内参提取和外参提取,以及DIBR的主过程。这里按照网上其他博客的顺序,先介绍内参提取。看的过程中注意坐标系的定义。由于是第一次接触,这里我采用的坐标系可能和常规的坐标系不太一样。 开始之前先介绍一些定义(基于我自己实现算法的时候所使用的数据集): \((u,v,1)^T\)为一个点的像素坐标。 \((x,y,1)^T\)为一个点在图像平面上以物理距离衡量的坐标。 \((X,Y,Z)\)为一个点在相机坐标系下的坐标。 \((X_w,Y_w,Z_w)\)为一个点在世界坐标系下的坐标。 \(f_x、f_y\)分别表示X轴的焦距和Y轴的焦距(非理想情况下横向和纵向的焦距是不一样的)。 \(d_x、d_y\)分别表示X轴(对应于图像的U轴)和Y轴(对应于图像的V轴)每个像素的物理尺寸(一个像素实际有多长)。 \(p_