开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring源码分析之Bean的创建过程详解

发表于 2020-10-29

前文传送门:

  1. Spring源码分析之预启动流程
  2. Spring源码分析之BeanFactory体系结构
  3. Spring源码分析之BeanFactoryPostProcessor调用过程详解

本文内容:

  1. 在IOC中,是如何通过beanDefition创建出一个bean的?
  2. 各BeanPostProcessor在这过程中扮演的角色,调用时机?

话不多说,直接正题走起,上图!

下面是bean创建过程的大致流程图,本文将以图中顺序进行逐步源码分析,小伙伴亦可与图中流程边对照边品食

原矢量图地址:www.processon.com/view/link/5…

img

我们知道,在Spring IOC前段部分有注册了一系列的BeanPostProcessor,在Bean的创建过程中,就将要使用到他们了,下面我给大家一一列出

  • AutowiredAnnotationBeanPostProcessor:在new AnnotatedBeanDefinitionReader时注册
  • CommonAnnotationBeanPostProcessor: 在new AnnotatedBeanDefinitionReader时注册
  • ApplicationContextAwareProcessor: 在prepareBeanFactory时注册
  • ApplicationListenerDetector: 在prepareBeanFactory时注册
  • ImportAwareBeanPostProcessor: 在配置类后置处理器调用postProcessBeanFactory注册
  • BeanPostProcessorChecker:在registerBeanPostProcessors时注册

以上就是Spring中内置的所有BeanPostProcessor了

同样,我们先从最开始的入口refresh开始分析

1
2
3
4
5
java复制代码public void refresh(){
//....省略前面部分
// 实例化剩余的单例bean
finishBeanFactoryInitialization(beanFactory);
}

finishBeanFactoryInitialization

1
2
3
4
java复制代码protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory){
// 将所有非懒加载的bean加载到容器中
beanFactory.preInstantiateSingletons();
}

循环我们之前注册的所有beanDefinition,一个个的进行调用getBean注册到容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public void preInstantiateSingletons(){
// 循环所有beanDefinition
for (String beanName : beanNames) {
// 将beanDefinition转化为RootBeanDefinition
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 不是抽象类并且是单例并且非懒加载
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 是否为工厂bean
if (isFactoryBean(beanName)) {
// 由于是以&开头获取bean,这里返回的是一个工厂bean,并且不会调用getObject方法
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof FactoryBean) {
// 判断是否要立即初始化bean
FactoryBean<?> factory = (FactoryBean<?>) bean;
boolean isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
if (isEagerInit) {
// 以为&开头的方式再获取一次,此时会调用FactoryBean的getObject()方法
getBean(beanName);
}
}
}
else {
// 不是FactoryBean,直接使用getBean进行初始化
getBean(beanName);
}
}
}
}

接下来就是Spring的常规操作,调用do开头的doGetBean

1
2
3
java复制代码public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}

以下为doGetBean中获取单例bean的逻辑

1
2
3
4
5
6
7
8
9
10
java复制代码// 转化beanName 如果是以&开头则去除,如果有别名则获取别名
String beanName = transformedBeanName(name);
// 尝试从三级缓存中获取bean
Object sharedInstance = getSingleton(beanName);
// 是否从缓存中获取到了bean
if (sharedInstance != null && args == null) {
// 如果是工厂类且name不以&开头,则调用工厂类的getObject()
// 其他情况返回原对象
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}

getSingleton

1
2
3
java复制代码public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 从单例缓存池中获取
Object singletonObject = this.singletonObjects.get(beanName);
// 获取不到,判断bean是否正在创建
// 如果是正在创建,2种情况 1.多个线程在创建bean 2.发生循环依赖
// 如果是多个线程,则由于同步锁阻塞于此
// 循环依赖的问题较为复杂,将在下章详细分析
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 从早期对象缓存池中获取
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 从三级缓存中获取单例工厂
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 调用回调方法获取早期bean
singletonObject = singletonFactory.getObject();
// 将早期对象放到二级缓存,移除三级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}

getObjectForBeanInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码protected Object getObjectForBeanInstance(
Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {
// 判断name是否以&开头,是则直接返回该FactoryBean
/*public static boolean isFactoryDereference(@Nullable String name) {
return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
}*/
if (BeanFactoryUtils.isFactoryDereference(name)) {
return beanInstance;
}
// 不是工厂bean直接返回原对象
if (!(beanInstance instanceof FactoryBean)) {
return beanInstance;
}
// 尝试从缓存中获取,保证多次从工厂bean获取的bean是同一个bean
object = getCachedObjectForFactoryBean(beanName);
if (object == null) {
FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
boolean synthetic = (mbd != null && mbd.isSynthetic());
// 从FactoryBean获取对象
object = getObjectFromFactoryBean(factory, beanName, !synthetic);
}
return object;
}

getObjectFromFactoryBean的代码摘取片段

1
2
3
4
java复制代码protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess){
// 获取bean,调用factoryBean的getObject()
object = doGetObjectFromFactoryBean(factory, beanName);
}
1
2
3
java复制代码private Object doGetObjectFromFactoryBean(FactoryBean<?> factory, String beanName){
object = factory.getObject();
}

以上为从缓存中获取到bean,处理FactoryBean的逻辑,接下来我们看看实际创建bean的过程

以下为续接上面doGetBean中未从缓存中获取到bean的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// 如果有被@DependsOn标记,先创建DependsOn的bean
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
registerDependentBean(dep, beanName);
getBean(dep);
}
}
// 单例bean
if (mbd.isSingleton()) {
// 开始创建bean
sharedInstance = getSingleton(beanName, () -> {
// 真正创建bean
return createBean(beanName, mbd, args);
});
// 如果是工厂类且name不以&开头,则调用工厂类的getObject()
// 其他情况返回原对象
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

getSingleton,此方法为重载方法,与从缓存中获取bean并非同一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
// 开始创建bean时加锁,注意这个锁的同步对象与从缓存中获取时锁的同步对象相同
synchronized (this.singletonObjects) {
// 再次从缓存中获取,有直接返回,出现有的情况
// 1.线程一正在创建A实例,线程二尝试获取,被同步锁阻塞
// 2.线程一创建完毕,线程二进入同步代码块,从缓存中获取直接返回
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 标记正在创建中
beforeSingletonCreation(beanName);
boolean newSingleton = false;
try {
// 调用回调函数获取到bean
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
finally {
// 清理状态
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 将创建的bean添加到单例缓存池中,并移除二三级缓存
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

createBean,终于开始创建bean了~

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
// 第一次调用bean后置处理器,在bean实例化之前的进行处理
// Spring内置的后置处理器中,无相关实现
// 可使用自定义的后置处理器在这里进行中止bean的创建过程操作
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
// 如果自定义的后置处理器返回了bean,则直接return,bean的创建过程于此中断
return bean;
}
// 进行创建bean
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
// 实例化bean 第二次调用bean后置处理器,用于获取bean的有参构造器
instanceWrapper = createBeanInstance(beanName, mbd, args);
// 第三次 处理beanDefinition的元数据信息
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
// 是否允许暴露早期对象
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
// 第四次 用于获取早期对象时的处理
// 将获取早期对象的回调方法放到三级缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 第五、六次,填充属性 可使用的方式 byName byType @Resource @Value @Autowired @Inject
populateBean(beanName, mbd, instanceWrapper);
// 第七、八次,初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
// 第九次 判断bean是否有销毁方法,有则将bean注册到销毁集合中,用于容器关闭时使用
registerDisposableBeanIfNecessary(beanName, bean, mbd);
// 返回创建好的bean
return exposedObject;
}

你以为这就结束了?

接下来我们就来看看这里后置处理器到底做了什么吧

由于第一次调用并未有任何处理,我们从第二次调用开始分析

createBeanInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
// 获取beanClass
Class<?> beanClass = resolveBeanClass(mbd, beanName);
// 使用AutowiredAnnotationBeanPostProcessor进行构造器推断,找到所有的有参构造器
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
// 实例化bean,并根据参数自动装配
return autowireConstructor(beanName, mbd, ctors, args);
}
// 调用无参的构造方法实例化
return instantiateBean(beanName, mbd);
}

determineConstructorsFromBeanPostProcessors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码protected Constructor<?>[] determineConstructorsFromBeanPostProcessors(@Nullable Class<?> beanClass, String beanName)
throws BeansException {

if (beanClass != null && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
// 只有AutowiredAnnotationBeanPostProcessor进行了实现,其他的都返回null
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 确认候选的构造器
Constructor<?>[] ctors = ibp.determineCandidateConstructors(beanClass, beanName);
if (ctors != null) {
return ctors;
}
}
}
}
return null;
}

AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName){
// 获取到所有的构造方法
rawCandidates = beanClass.getDeclaredConstructors();
for (Constructor<?> candidate : rawCandidates) {
// 是否带有@Autowired注解
MergedAnnotation<?> ann = findAutowiredAnnotation(candidate);
if (ann != null) {
// 是否必须
boolean required = determineRequiredStatus(ann);
candidates.add(candidate);
}
else if (candidate.getParameterCount() == 0) {
// 无参构造器
defaultConstructor = candidate;
}
}
// 候选的构造器不为空
if (!candidates.isEmpty()) {
// 候选的构造器不为空而requiredConstructor为空表示有@Autowired标识的构造器
// 但是required=false
if (requiredConstructor == null) {
if (defaultConstructor != null) {
// 将无参构造器也加入到候选构造器集合中
candidates.add(defaultConstructor);
}
}
// 将集合中的构造器转化为数组
candidateConstructors = candidates.toArray(new Constructor<?>[0]);
}
// 候选的构造器为空,但有一个有参构造器,则使用有参构造器作为候选的构造器
else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
}
// 返回候选构造器数组
return (candidateConstructors.length > 0 ? candidateConstructors : null);
}

autowireConstructor 实例化并自动装配,摘取代码片段

1
2
3
4
5
java复制代码protected BeanWrapper autowireConstructor(
String beanName, RootBeanDefinition mbd, @Nullable Constructor<?>[] ctors, @Nullable Object[] explicitArgs) {

return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs);
}
1
2
3
4
5
6
7
8
9
10
11
java复制代码public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd,
@Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) {
for (Constructor<?> candidate : candidates) {
// 获取参数的类型
Class<?>[] paramTypes = candidate.getParameterTypes();
// 获取依赖的bean
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames..);
// 调用instantiate方法进行实例化bean
bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse));
}
}

以上便是bean的实例化过程

applyMergedBeanDefinitionPostProcessors

第三次主要是将标识了需要自动装配注解的属性或方法解析出来,包含的注解主要有 @Resource @Autowired @Value @Inject @PostConstruct @PreDestroy

1
2
3
4
5
6
7
8
9
10
java复制代码protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof MergedBeanDefinitionPostProcessor) {
// CommonAnnotationBeanPostProcessor解析@PostConstruct @PreDestroy @Resource
// AutowiredAnnotationBeanPostProcessor 解析@Autowired @Value @Inject
MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
}
}
}

CommonAnnotationBeanPostProcessor#postProcessMergedBeanDefinition

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
// 父类为InitDestroyAnnotationBeanPostProcessor
// 寻找@PostConstruct @PreDestroy注解的方法
// 用于bean的生命周期中初始化前的处理逻辑
super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName);
// 寻找@Resource注解标识的属性或方法元数据
// 将这些元数据保存到缓存中,用于在属性装配阶段使用
InjectionMetadata metadata = findResourceMetadata(beanName, beanType, null);
// 检查是否有重复的元数据,去重处理,如一个属性上既有@Autowired注解,又有@Resource注解
// 只使用一种方式进行注入,由于@Resource先进行解析,所以会选择@Resource的方式
metadata.checkConfigMembers(beanDefinition);
}

InitDestroyAnnotationBeanPostProcessor#postProcessMergedBeanDefinition

1
2
3
4
5
6
java复制代码public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
// 寻找PostConstruct @PreDestroy注解的方法
LifecycleMetadata metadata = findLifecycleMetadata(beanType);
// 去重处理
metadata.checkConfigMembers(beanDefinition);
}

所有的后置处理器的过程是相似的,这里取CommonAnnotationBeanPostProcessor进行分析

我们先来看看寻找元数据的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码private InjectionMetadata findResourceMetadata(String beanName, final Class<?> clazz, @Nullable PropertyValues pvs) {
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
// 从缓存中获取
// 调用postProcessMergedBeanDefinition方法时将元数据解析放入缓存
// 调用postProcessProperties方法时将元数据取出
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);
}
// 创建元数据,寻找@Resouce标识的属性或方法
metadata = buildResourceMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}

buildResourceMetadata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码private InjectionMetadata buildResourceMetadata(final Class<?> clazz){
// 判断是否为候选的class,不是则返回默认的空元数据
// resourceAnnotationTypes为Annotation集合,里面包含了@Resource @EJB @WebServiceRef
// 我们一般常用的只是@Resource
if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}
do {
// 循环所有的属性,判断属性是否存在WebServiceRef、EJB、Resource注解,有则构建元数据
// doWithLocalFields中就是将targetClass的所有field取出进行循环
ReflectionUtils.doWithLocalFields(targetClass, field -> {
if (webServiceRefClass != null && field.isAnnotationPresent(webServiceRefClass)) {
currElements.add(new WebServiceRefElement(field, field, null));
}
else if (ejbClass != null && field.isAnnotationPresent(ejbClass)) {
currElements.add(new EjbRefElement(field, field, null));
}
// 是否存在@Resource注解
else if (field.isAnnotationPresent(Resource.class)) {
if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
currElements.add(new ResourceElement(field, field, null));
}
}
});
// 与上一步相似,判断方法上是否存在这些注解
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
//......省略
});
// 获取父类
targetClass = targetClass.getSuperclass();
}
// 父类不是Object则继续循环父类中的属性和方法
while (targetClass != null && targetClass != Object.class);
// 将构建好的元数据封装到InjectionMetadata中返回
return InjectionMetadata.forElements(elements, clazz);
}

现在我们再来看看去重处理的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public void checkConfigMembers(RootBeanDefinition beanDefinition) {
Set<InjectedElement> checkedElements = new LinkedHashSet<>(this.injectedElements.size());
for (InjectedElement element : this.injectedElements) {
Member member = element.getMember();
// 检查该beanDefinition的externallyManagedConfigMembers集合中是否已经包含该成员(属性或者方法)
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
// 不包含则将该成员注册
beanDefinition.registerExternallyManagedConfigMember(member);
// 加入到已检查的集合
checkedElements.add(element);
}
}
this.checkedElements = checkedElements;
}

由于第四次,用于获取早期对象时的处理的调用,在Spring的内置处理器中也没有相应的实现,跳过

这一步和第一步一样,在AOP时将会用到,我们放到下章分析

紧接着就是填充属性的步骤了

populateBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
// 在这里可进行中止填充属性操作,实现InstantiationAwareBeanPostProcessor接口
// 并postProcessAfterInstantiation返回false,则直接返回,不会再往下执行
// Spring内中的后置处理器皆返回的true
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
return;
}
}
}
}
// 获得自动装配的类型,默认为0,
// 这里只有xml配置,ImportBeanDefinitionRegistrar,BeanFactoryPostProcessor可进行改变
// Spring整合Mybatis中,将Mapper的自动装配类型改成了BY_TYPE,
// 于是在Mapper得以在这里被填充SqlSessionTemplate,SqlSessionFactory属性
int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
// 获取到依赖的bean并放到newPvs中
autowireByType(beanName, mbd, bw, newPvs);
}
// 将新的属性列表赋给旧的引用
pvs = newPvs;
}
}

autowireByName 和 autowireByType差不多,autowireByType更为复杂一些,这里只分析autowireByType的处理过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码protected void autowireByType(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
// 查询非简单(Java内置 基本类型,String,Date等)的属性
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
// 循环所有属性名
for (String propertyName : propertyNames) {
// 获取方法参数
MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
// 构建一个依赖描述符
DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
// 获取依赖的bean
// resolveDependency方法中调用了doResolveDependency,该方法我们在下一步的后置处理器调用中分析
Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
// 将bean放置到属性集合中
if (autowiredArgument != null) {
pvs.add(propertyName, autowiredArgument);
}
}
}

现在,回到填充属性的过程

该第六次调用后置处理器了,这一次主要对属性和方法进行自动装配

1
2
3
4
5
6
7
8
9
10
11
java复制代码// CommonAnnotationBeanPostProcessor 处理@Resouce注解的装配
// AutowiredAnnotationBeanPostProcessor 处理@Autowired @Value @Inject注解的装配
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 处理自动装配,将依赖的属性装配到bean中
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
// ...省略已被废弃的代码...
pvs = pvsToUse;
}
}

这一步的逻辑也是差不多,由于AutowiredAnnotationBeanPostProcessor复杂一些,我们取AutowiredAnnotationBeanPostProcessor中的逻辑进行分析

1
2
3
4
5
6
7
8
9
java复制代码public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 取出之前postProcessMergedBeanDefinition时解析好的元数据
// @Autowired @Value @Inject 标识的属性或方法
// findAutowiringMetadata这里有没有和第四步中的很像呢~
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
// 进行自动装配
metadata.inject(bean, beanName, pvs);
return pvs;
}

findAutowiringMetadata,看看和第四步有多像吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码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);
}
// 构建元数据,找到@Autowird @Value @Inject 标识的属性或方法进行构建
metadata = buildAutowiringMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}

自动装配过程

1
2
3
4
5
6
7
8
9
10
java复制代码public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) {
// 取出之前去重过的元数据列表
Collection<InjectedElement> checkedElements = this.checkedElements;
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
// 进行属性或方法装配
element.inject(target, beanName, pvs);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs){
// 强转成Field
Field field = (Field) this.member;
// 创建一个依赖描述符
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
// 获取到依赖的bean
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
if (value != null) {
ReflectionUtils.makeAccessible(field);
// 将获取到的依赖bean利用反射装配到属性中
field.set(bean, value);
}
}
1
2
3
4
5
6
java复制代码public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
// 获取bean
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter){
// 解析@Value注解
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
}
// 根据类型寻找是否有匹配的beanDefinition
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
// 为空则判断是否必须
if (isRequired(descriptor)) {
// 必须则抛出NoSuchBeanDefinitionException异常
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
return null;
}
// 如果根据类型匹配出来的候选bean不止一个,则需要确认是哪一个
if (matchingBeans.size() > 1) {
// 确认出真正需要依赖的
// 先判断是否有@Primary注解的
// 没有再判断是否有实现了Priority注解的,取值最小的
// 没有最后使用属性名进行匹配
// 匹配不到则返回null
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
// 这里进行确认是否必须,必须则抛出异常
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
}
else {
return null;
}
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}
if (instanceCandidate instanceof Class) {
// 调用getBean方法
instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
}
Object result = instanceCandidate;
return result;
}

getBean方法

1
2
3
java复制代码public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory) {
return beanFactory.getBean(beanName);
}

以上就是自动装配的过程,再次回到填充属性的方法,进行小小的收尾

1
2
3
4
5
java复制代码// 如果不是xml byName byType 方式,其他方式pvs皆是空值
if (pvs != null) {
// 调用set方法赋值
applyPropertyValues(beanName, mbd, bw, pvs);
}
1
2
3
4
java复制代码protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
// 使用反射给属性赋值
bw.setPropertyValues(new MutablePropertyValues(deepCopy));
}

填充属性过程,over~

初始化过程

initializeBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd){
// 如果bean实现了BeanNameAware,BeanClassLoaderAware,BeanFactoryAware接口
// 则进行回调相应的方法
invokeAwareMethods(beanName, bean);
// 第七次 在bean的初始化前进行处理
// 调用@PostConstruct注解的方法,Aware接口的回调方法
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
// 调用初始化方法
// 如果bean实现了InitializingBean接口,则调用afterPropertiesSet方法
// 如果bean还实现了自定义的初始化方法,也进行调用
// 先afterPropertiesSet,再自定义
invokeInitMethods(beanName, wrappedBean, mbd);
// 第八次 处理初始化后的bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

以上为初始化中的大概流程,接下来我们一个个分析

首先是invokeAwareMethods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码private void invokeAwareMethods(String beanName, Object bean) {
// 以下过程一目了然,就不过多分析了
if (bean instanceof Aware) {
if (bean instanceof BeanNameAware) {
((BeanNameAware) bean).setBeanName(beanName);
}
if (bean instanceof BeanClassLoaderAware) {
ClassLoader bcl = getBeanClassLoader();
if (bcl != null) {
((BeanClassLoaderAware) bean).setBeanClassLoader(bcl);
}
}
if (bean instanceof BeanFactoryAware) {
((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
}
}
}

applyBeanPostProcessorsBeforeInitialization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName){
Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
// ImportAwareBeanPostProcessor处理ImportAware接口
// InitDestroyAnnotationBeanPostProcessor处理@PostContrust注解
// ApplicationContextAwareProcessor处理一系列Aware接口的回调方法
Object current = processor.postProcessBeforeInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

InitDestroyAnnotationBeanPostProcessor

1
2
3
4
5
6
7
java复制代码public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 取出在第四步解析@PostContrust @PreDestroy得到的元数据
LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());
// 调用init方法(@PostConstruct标识的)
metadata.invokeInitMethods(bean, beanName);
return bean;
}
1
2
3
4
5
6
7
8
9
java复制代码public void invokeInitMethods(Object target, String beanName) throws Throwable {
// 只取init的元数据(还有destroy的)
Collection<LifecycleElement> checkedInitMethods = this.checkedInitMethods;
if (!initMethodsToIterate.isEmpty()) {
for (LifecycleElement element : initMethodsToIterate) {
element.invoke(target);
}
}
}
1
2
3
4
5
java复制代码public void invoke(Object target) throws Throwable {
ReflectionUtils.makeAccessible(this.method);
// 直接反射调用
this.method.invoke(target, (Object[]) null);
}

ApplicationContextAwareProcessor的过程和invokeAwareMethods的过程类似,这里就不分析了

invokeInitMethods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd){
// 如果实现了InitializingBean接口,调用afterPropertiesSet方法
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
((InitializingBean) bean).afterPropertiesSet();
}
if (mbd != null && bean.getClass() != NullBean.class) {
// 调用自定义的初始化方法
String initMethodName = mbd.getInitMethodName();
if (StringUtils.hasLength(initMethodName) &&
!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
!mbd.isExternallyManagedInitMethod(initMethodName)) {
// 自定义init方法主要在@Bean注解进行声明,取出beanDefinition中的initMethod调用就好了
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}

applyBeanPostProcessorsAfterInitialization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
// Spring内置后置处理器中,只有ApplicationListenerDetector有处理逻辑
// ApplicationListenerDetector会将实现了ApplicationListener接口的bean添加到事件监听器列表中
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}
1
2
3
4
5
6
java复制代码public Object postProcessAfterInitialization(Object bean, String beanName){
if (bean instanceof ApplicationListener) {
// 将bean添加到事件监听器列表中
this.applicationContext.addApplicationListener((ApplicationListener<?>) bean);
}
}

以上,bean初始化完毕!

伴随着bean初始化完毕,bean就算创建完成了,本文也到此结束啦,有问题的小伙伴欢迎在下方留言哟~

下文预告:Spring源码分析之循环依赖

Spring 源码系列
  1. Spring源码分析之 IOC 容器预启动流程(已完结)
  2. Spring源码分析之BeanFactory体系结构(已完结)
  3. Spring源码分析之BeanFactoryPostProcessor调用过程(已完结)
  4. Spring源码分析之Bean的创建过程(已完结)
  5. Spring源码分析之什么是循环依赖及解决方案
  6. Spring源码分析之AOP从解析到调用
  7. Spring源码分析之事务管理(上),事物管理是spring作为容器的一个特点,总结一下他的基本实现与原理吧
  8. Spring源码分析之事务管理(下) ,关于他的底层事物隔离与事物传播原理,重点分析一下
Spring Mvc 源码系列
  1. SpringMvc体系结构
  2. SpringMvc源码分析之Handler解析过程
  3. SpringMvc源码分析之请求链过程
Mybatis 源码系列

暂定


追更,可关注我,近期有时间就文章全写完,分享纯粹为了乐趣,也有一种成就感吧,笔者这篇文章先就到这

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Google】再见SharedPreferences拥抱D

发表于 2020-10-29

前言

Google 增加了一个新 Jetpack 的成员 DataStore,主要用来替换 SharedPreferences, 而 Jetpack DataStore 有两种实现方式:

  • Proto DataStore:存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地
  • Preferences DataStore:以键值对的形式存储在本地和 SharedPreferences 类似

在上一篇文章 [Google] 再见 SharedPreferences 拥抱 Jetpack DataStore 中介绍了 SharedPreferences 都有那些坑,以及 Preferences DataStore 为我们解决了什么问题。

而今天这篇文章主要来介绍 Proto DataStore,Proto DataStore 通过 protocol buffers 将对象序列化存储在本地,所以首先需要安装 Protobuf 编译 proto 文件,Protobuf 编译大致分为 Gradle 插件编译和命令行编译,这两种方式已经发布到了博客上,欢迎点击下方链接前往查看。

  • Protobuf | 安装 Gradle 插件编译 proto 文件
  • Protobuf | 如何在 ubuntu 上安装 Protobuf 编译 proto 文件
  • Protobuf | 如何在 MAC 上安装 Protobuf 编译 proto 文件

由于目前主要在 MAC 和 ubuntu 上开发,所以只提供了这两种命令行编译方式,如果在 Win 上开发的同学,可以使用 Gradle 插件编译的方式。

这篇文章相关示例,已经上传到 GitHub 欢迎前去仓库 AndroidX-Jetpack-Practice/DataStoreSimple 切换到 datastore_proto 分支查看。

GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

通过这篇文章你将学习到以下内容:

  • 为何要有 Proto DataStore?
  • 什么序列化?什么是对象序列化?什么是数据的序列化?
  • 什么是 Protocol Buffer?为什么需要它?为我们解决了什么问题?
  • 如何在项目中使用 Proto DataStore?
  • 如何迁移 SharedPreferences 到 Proto DataStore?
  • proto2 和 proto3 语法如何选择?
  • 常用 proto3 语法解析?
  • MAD Skills 是什么?

为何要有 Proto DataStore

既生 Preference DataStore 何生 Proto DataStore,它们之间有什么区别?

  • Preference DataStore 主要是为了解决 SharedPreferences 所带来的性能问题
  • Proto DataStore 比 Preference DataStore 更加灵活,支持更多的类型
    • Preference DataStore 支持 Int 、 Long 、 Boolean 、 Float 、 String
    • protocol buffers 支持的类型,Proto DataStore 都支持
  • Preference DataStore 以 XML 的形式存储 key-value 数据,可读性很好
  • Proto DataStore 使用了二进制编码压缩,体积更小,速度比 XML 更快

从源码的角度

如果源码部分不是很了解,可以先忽略,继续往下看,之后回过头在来看就能理解了。

  • Preference DataStore 源码里定义了一个 proto 文件,通过 PreferencesSerializer 将每一对 key-value 数据映射到 proto 文件定义的 message 类型,proto 文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码syntax = "proto2";
......
message PreferenceMap {
map<string, Value> preferences = 1;
}

message Value {
oneof valueName {
bool boolean = 1;
float float = 2;
int32 integer = 3;
int64 long = 4;
string string = 5;
double double = 7;
}
}

在 DataStore 中使用的是 proto2 语法,将 XML 中 key-value 数据映射到 Map 中,并且在 proto 文件中只定义了 Int 、 Long 、 Boolean 、 Float 、 String 这几种类型。

  • Proto DataStore 我们可以自定义 proto 文件,并实现了 Serializer<T> 接口,所以更加灵活,支持更多的类型

刚才说到 Proto DataStore 通过 protocol buffers 使用了二进制编码压缩,将对象序列化存储在本地,那么序列化到底是什么?我们先来了解一些基本概念,方便我们对后续的内容有更好的理解。

序列化

序列化:将一个对象转换成可存储或可传输的状态,数据可能存储在本地或者在蓝牙、网络间进行传输。序列化大概分为对象序列化、数据序列化。

对象的序列化

Java 对象序列化 将一个存储在内存中的对象转化为可传输的字节序列,便于在蓝牙、网络间进行传输或者存储在本地。把字节序列还原为存储在内存中的 Java 对象的过程称为反序列化。

在 Android 中可以通过 Serializable 和 Parcelable 两种方式实现对象序列化。

Serializable

Serializable 是 Java 原生序列化的方式,主要通过 ObjectInputStream 和 ObjectOutputStream 来实现对象序列化和反序列化,但是在整个过程中用到了大量的反射和临时变量,会频繁的触发 GC,序列化的性能会非常差,但是实现方式非常简单,来看一下 ObjectInputStream 和 ObjectOutputStream 源码里有很多反射的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码ObjectOutputStream.java
private void writeObject0(Object obj, boolean unshared)
throws IOException{
......
Class<?> cl = obj.getClass();
......
}

ObjectInputStream.java
void readFields() throws IOException {
......
ObjectStreamField[] fields = desc.getFields(false);
for (int i = 0; i < objVals.length; i++) {
objVals[i] =
readObject0(fields[numPrimFields + i].isUnshared());
objHandles[i] = passHandle;
}
......
}

在 Android 中存在大量跨进程通信,由于 Serializable 性能差的原因,所以 Android 需要更加轻量且高效的对象序列化和反序列化机制,因此 Parcelable 出现了。

Parcelable

Parcelable 的出现解决了 Android 中跨进程通信性能差的问题,而且 Parcelable 比 Serializable 要快很多,因为写入和读取的时候都是采用自定义序列化存储的方式,通过 writeToParcel() 方法和 describeContents() 方法来实现,不需要使用反射来推断它,因此性能得到提升,但是使用起来比 Serializable 要复杂很多。

为了解决复杂性问题, AndroidStudio 也有对应插件简化使用过程,如果是 Java 语言可以使用 android parcelable code generator 插件, 如果 Kotlin 语言的话可以使用 @Parcelize 注解,快速的实现 Parcelable 序列化。

用一张表格汇总一下 Serializable 和 Parcelable 的区别

数据序列化

对象序列化记录了很多信息,包括 Class 信息、继承关系信息、变量信息等等,但是数据序列化相比于对象序列化就没有这么多沉余信息,数据序列化常用的方式有 JSON、Protocol Buffers、FlatBuffers。

  • JSON :是一种轻量级的数据交互格式,支持跨平台、跨语言,被广泛用在网络间传输,JSON 的可读性很强,但是序列化和反序列化性能却是最差的,解析过程中,要产生大量的临时变量,会频繁的触发 GC,为了保证可读性,并没有进行二进制压缩,当数据量很大的时候,性能上会差一点。
  • Protocol Buffers :它是 Google 开源的跨语言编码协议,可以应用到 C++ 、C# 、Dart 、Go 、Java 、Python 等等语言,Google 内部几乎所有 RPC 都在使用这个协议,使用了二进制编码压缩,体积更小,速度比 JSON 更快,但是缺点是牺牲了可读性

RPC 指的是跨进程远程调用,即一个进程调用另外一个进程的方法。

  • FlatBuffers :同 Protocol Buffers 一样是 Google 开源的跨平台数据序列化库,可以应用到 C++ 、 C# , Go 、 Java 、 JavaScript 、 PHP 、 Python 等等语言,空间和时间复杂度上比其他的方式都要好,在使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小,但是缺点是牺牲了可读性

最后我们用一张图来分析一下 JSON、Protocol Buffers、FlatBuffers 它们序列化和反序列的性能,数据来源于 JSON vs Protocol Buffers vs FlatBuffers

FlatBuffers 和 Protocol Buffers 无论是序列化还是反序列都完胜 JSON,FlatBuffers 最初是 Google 为游戏或者其他对性能要求很高的应用开发的,接下来我们来看一下今天主角 Protocol Buffer。

Protocol Buffer

Protocol Buffer ( 简称 Protobuf ) 它是 Google 开源的跨语言编码协议,可以应用到 C++ 、C# 、Dart 、Go 、Java 、Python 等等语言,Google 内部几乎所有 RPC 都在使用这个协议,使用了二进制编码压缩,体积更小,速度比 JSON 更快。

从 Proto3.0.0 Release Note 得知: protocol buffers 最初开源时,它实现了 Protocol Buffers 语言版本 2(称为 proto2), 这也是为什么版本数从 v2.0.0 开始,从 v3.0.0 开始, 引入新的语言版本(proto3),而旧的版本(proto2)继续被支持。所以到目前为止 Protobuf 共两个版本 proto2 和 proto3。

proto2 和 proto3 应该学习那个版本?

proto3 简化了 proto2 的语法,提高了开发的效率,因此也带来了版本不兼容的问题,因为 2019 年的时候才发布 proto3 稳定版本,所以在这之前使用 Protocol Buffer 的公司,大部分项目都是使用 proto2 的版本,从上文的源码分析部分可知,在 DataStore 中使用了 proto2 语法,所以 proto2 和 proto3 这两种语法都同时在使用。

对于初学者而言直接学习 proto3 语法就可以了,为了适应技术迭代的变化,当掌握 proto3 语法之后,可以顺带了解一下 proto2 语法以及 proto3 和 proto2 语法的区别,这样可以更好的理解其他的开源项目。

为了避免混淆 proto3 和 proto2 语法,在本文仅仅分析 proto3 语法,当我们了解完这些基本概念之后,我们开始分析 如何在项目中使用 Proto DataStore。

如何在项目中使用 Proto DataStore

Proto DataStore 同 Preferences DataStore 一样主要应用在 MVVM 当中的 Repository 层,在项目中使用 Proto DataStore 非常简单。

1. 添加 Proto DataStore 依赖

在 app 模块 build.gradle 文件内,添加以下依赖

1
2
3
4
arduino复制代码// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
// protobuf
implementation "com.google.protobuf:protobuf-javalite:3.10.0"

Google 推荐 Android 开发使用 protobuf-javalite 因为它的代码更小,做了大量的优化。

当添加完依赖之后需要新建 proto 文件,在本文示例项目中新建了一个 common-protobuf 模块,将新建的 person.proto 文件,放到了 common-protobuf 模块 src/main/proto 目录下。

proto 文件默认存放路径 src/main/proto,也可以通过修改 gradle 的配置,来修改默认存放路径

在 common-protobuf 模块,build.gradle 文件内,添加以下依赖

1
arduino复制代码implementation "com.google.protobuf:protobuf-javalite:3.10.0"

2. 新建 Person.proto 文件,添加以下内容

1
2
3
4
5
6
7
8
9
ini复制代码syntax = "proto3";

option java_package = "com.hi.dhl.datastore.protobuf";
option java_outer_classname = "PersonProtos";

message Person {
// 格式:字段类型 + 字段名称 + 字段编号
string name = 1;
}
  • syntax :指定 protobuf 的版本,如果没有指定默认使用 proto2,必须是.proto文件的除空行和注释内容之外的第一行
  • option :表示一个可选字段
    • java_package : 指定生成 java 类所在的包名
    • java_outer_classname : 指定生成 java 类的名字
  • message 中包含了一个 string 类型的字段(name)。注意 := 号后面都跟着一个字段编号
  • 每个字段由三部分组成:字段类型 + 字段名称 + 字段编号,在 Java 中每个字段会被编译成 Java 对象

在这里只需要了解这些 proto 语法即可,在文章后面会更详细的介绍这些语法。

3. 执行 protoc ,编译 proto 文件

以输出 Java 文件为例,执行以下命令即可输出对应的 Java 文件,如果配置了 Gradle 插件,可以忽略这一步,直接点击 Build -> Rebuild Project 即可生成对应的 Java 文件。

1
css复制代码protoc --java_out=./src/main/java -I=./src/main/proto  ./src/main/proto/*.proto
  • --java_out : 指定输出 Java 文件所在的目录
  • -I :指定 proto 文件所在的目录
  • *.proto : 表示在 -I 指定的目录下查找以 .proto 结尾的文件

4. 构建 DataStore

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码object PersonSerializer : Serializer<PersonProtos.Person> {
override fun readFrom(input: InputStream): PersonProtos.Person {
try {
return PersonProtos.Person.parseFrom(input) // 是编译器自动生成的,用于读取并解析 input 的消息
} catch (exception: Exception) {
throw CorruptionException("Cannot read proto.", exception)
}
}

override fun writeTo(t: PersonProtos.Person, output: OutputStream) = t.writeTo(output) // t.writeTo(output) 是编译器自动生成的,用于写入序列化消息
}
  • 实现了 Serializer<T> 接口,这是为了告诉 DataStore 如何从 proto 文件中读写数据
  • PersonProtos.Person 是通过编译 proto 文件生成的 Java 类
  • Person.parseFrom(input) 是编译器自动生成的,用于读取并解析 input 的消息
  • t.writeTo(output) 是编译器自动生成的,用于写入序列化消息

5. 从 Proto DataStore 中读取数据

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码fun readData(): Flow<PersonProtos.Person> {
return protoDataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(PersonProtos.Person.getDefaultInstance())
} else {
throw it
}
}
}
  • DataStore 是基于 Flow 实现的,所以通过 dataStore.data 会返回一个 Flow<T>,每当数据变化的时候都会重新发出
  • catch 用来捕获异常,当读取数据出现异常时会抛出一个异常,如果是 IOException 异常,会发送一个 PersonProtos.Person.getDefaultInstance() 来重新使用,如果是其他异常,最好将它抛出去

4. 向 Proto DataStore 中写入数据

在 Proto DataStore 中是通过 DataStore.updateData() 方法写入数据的,DataStore.updateData() 是一个 suspend 函数,所以只能在协程体内使用,每当遇到 suspend 函数以挂起的方式运行,并不会阻塞主线程。

以挂起的方式运行,不会阻塞主线程 :也就是协程作用域被挂起, 当前线程中协程作用域之外的代码不会阻塞。

首先我们需要创建一个 suspend 函数,然后调用 DataStore.updateData() 方法写入数据即可。

1
2
3
4
5
kotlin复制代码suspend fun saveData(personModel: PersonModel) {
protoDataStore.updateData { person ->
person.toBuilder().setAge(personModel.age).setName(personModel.name).build()
}
}

person.toBuilder() 是编译器为每个类生成 Builder 类,用于创建消息实例

到这里关于 Proto DataStore 读取数据和写入数据已经全部分析完了,接下来分析一下如何迁移 SharedPreferences 到 Proto DataStore。

迁移 SharedPreferences 到 Proto DataStore

迁移 SharedPreferences 到 Proto DataStore 只需要 3 步

1. 创建映射关系

将 SharedPreferences 数据迁移到 Proto DataStore 中,需要实现一个映射关系,将 SharedPreferences 中每一对 key-value 数据映射到 proto 文件定义的 message 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码private val shardPrefsMigration =
SharedPreferencesMigration<PersonProtos.Person>(
context,
SharedPreferencesRepository.PREFERENCE_NAME
) { sharedPreferencesView, person ->

// 获取 SharedPreferences 的数据
val follow = sharedPreferencesView.getBoolean(
PreferencesKeys.KEY_ACCOUNT,
false
)

// 将 SharedPreferences 每一对 key-value 的数据映射到 Proto DataStore 中
// 将 SP 文件中 ByteCode : true 数据映射到 Person 的成员变量 followAccount 中
person.toBuilder()
.setFollowAccount(follow)
.build()
}
  • 获取 SharedPreferences 存储的 key = ByteCode 的值
  • 将 key = ByteCode 数据映射到 Person 的成员变量 followAccount 中

2. 构建 DataStore 并传入 shardPrefsMigration

1
2
3
4
5
ini复制代码protoDataStore = context.createDataStore(
fileName = FILE_NAME,
serializer = PersonSerializer,
migrations = listOf(shardPrefsMigration)
)
  • 当 DataStore 对象构建完了之后,需要执行一次读取或者写入操作,即可完成 SharedPreferences 迁移到 DataStore,当迁移成功之后,会自动删除 SharedPreferences 使用的文件,Proto DataStore 和 Preferences DataStore 文件存储路径都是一样的,如下图所示

到这里关于 Jetpack DataStore 实现方式之一 Proto DataStore 全部都分析完了,我们一起来看一下 proto 语法。

常用的 proto3 语法

我梳理了常用的 proto3 语法,应该能满足大部分情况,更多语法可以参考 Google 官方教程 ,当掌握 proto3 语法之后,可以顺带了解一下 Proto2 语法,Proto3 虽然简化了 Proto2 的使用,提高了开发的效率,但是因为版本兼容问题,对于早期使用 Protocol Buffer 的团队,大部分都是使用 Proto2 语法。

一个基本的消息类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码syntax = "proto3";

option java_package = "com.hi.dhl.datastore.protobuf";
option java_outer_classname = "PersonProtos";

message Person {
// 格式:字段类型 + 字段名称 + 字段编号
string name = 1;
int32 age = 2;
bool followAccount = 3;
repeated string phone = 4;
Address address = 5;
}

message Address{
......
}
  • syntax :指定 protobuf 的版本,如果没有指定默认使用 proto2,必须是.proto文件的除空行和注释内容之外的第一行
  • option :表示一个可选字段
    • java_package : 指定生成 java 类所在的包名
    • java_outer_classname : 指定生成 java 类的名字
  • 在一个 proto 文件中,可以定义多个 message
  • message 中包含了 3 个字段:一个 string 类型(name)、一个整型类型(age)、一个 bool 类型(followAccount)。注意 := 号后面都跟着一个字段编号
  • 每个字段由三部分组成:字段类型 + 字段名称 + 字段编号,在 Java 中每个字段会被编译成 Java 对象,其他语言会被编译其他语言类型

字段类型

每一个消息类型中包含了很多个消息字段,每个消息字段都有一个类型,接下里用一个表格展示 proto 文件中的类型,以及对应的 Java 类型,如果其他语言可以查看官方文档。

.proto Type Notes Java Type
double double
float float
int32 使用变长编码,如果字段是负值,效率很低,使用 sint32 代替 int
int64 使用变长编码。如果字段是负值,效率很低,使用 sint64 代替 long
sint32 使用变长编码,如果是负值比普通的 int32 更高效 int
sint64 使用变长编码,如果是负值比普通的 int64 更高效 long
bool boolean
string 字符串必须始终包含 UTF-8 编码或 7-bit ASCII 文本,长度不能超过23 String

以上类型是经常会用到的,当然还有其他类型:uint32 、 uint64 、 fixed32 、 fixed64 、 sfixed32 、 sfixed64 、 bytes 等等,更多编码类型可以点击这里查看 Encoding

字段默认值

在 Proto3 中使用以下规则,编译成 Java 语言的默认值:

  • 对于 string 类型,默认值为空字符串("")
  • 对于 byte 类型,默认值是一个大小为 0 空 byte 数组
  • 对于 bool 类型,默认为 false
  • 对于数值类型,默认值为 0
  • 对于枚举类型,默认值是第一个定义的枚举值, 且这个值必须是 0 (这是为了兼容 proto2 语法)
  • 使用其他消息类型用作字段类型,默认值是 null (下文会详细分析)
  • 被 repeated 修饰字段,默认值是一个大小为 0 的空 List

字段编号

在每一个消息字段 = 号后面都跟着一个字段编号,如下所示:

1
ini复制代码string name = 1;

字段编号用于在消息的二进制格式中识别各个字段,字段编号非常重要,一旦开始使用就不能够再改变,字段编号的范围在 [1, 2^29 - 1] 之间,其中 [19000-19999] 作为 Protobuf 预留字段,不能使用。

注意 :在范围 [1, 15] 之间的字段编号在编码的时候会占用一个字节,包括字段编号和字段类型,在范围 [16, 2047] 之间的字段编号占用两个字节,因此,应该为频繁出现的消息字段保留 [1, 15] 之间的字段编号,一定要为将来频繁出现的元素留出一些空间。

repeated

在刚才的示例中,我给一个字段添加了 repeated 修饰符,如下所示:

1
ini复制代码repeated string phone = 4;

被 repeated 修饰的字段,对应 Java 类型中的 List,来看一下编译后的代码。

1
arduino复制代码private com.google.protobuf.Internal.ProtobufList<java.lang.String> phone_;

ProtobufList 其实是 List 子类,如下所示:

1
csharp复制代码public static interface ProtobufList<E> extends List<E>

包含其他消息类型

消息字段除了可以使用 int32 、 bool 、string 等等作为字段类型,还可以使用其他消息类型作为字段类型,如下所示:

1
2
3
4
5
6
7
8
less复制代码message Person {
// 格式:字段类型 + 字段名称 + 字段编号
Address address = 5;
}

message Address{
......
}

消息嵌套

在一个 proto 文件中,可以定义多个 message 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码message Person {
// 格式:字段类型 + 字段名称 + 字段编号
string name = 1;
int32 age = 2;
bool followAccount = 3;
repeated string phone = 4;
Address address = 5;
}

message Address{
string city = 1
}

当然 message 也是可以层级嵌套的,来看个示例:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码message Person {
// 格式:字段类型 + 字段名称 + 字段编号
string name = 1;
int32 age = 2;
bool followAccount = 3;
repeated string phone = 4;

message Address{
string city = 1;
}
Address address = 5;
}

这些 message 会被编译成静态内部类,如下所示:

1
2
3
4
5
scala复制代码public  static final class Address extends
com.google.protobuf.GeneratedMessageLite<
Address, Address.Builder> implements AddressOrBuilder {
......
}

枚举类型

同样我们可以给 message 添加枚举类型,也可以使用枚举类型作为字段类型,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码message Person {    
string name = 1;
message Address{
string city = 1;
}
Address address = 5;

enum Weekday{
SUN = 0;
MON = 1;
TUE = 2;
WED = 3;
THU = 4;
FRI = 5;
SAT = 6;
}
Weekday weekday = 6;
}

正如你所看到的,消息字段除了可以使用 int32 、 bool 、string 、其他消息类型作为字段类型之外,还可以使用枚举类型作为字段类型。

注意 :每一个枚举类型第一个枚举值必须为 0,因为:

  • 必须有一个 0 值,因为需要将 0 作为默认值
  • 值为 0 的元素必须是第一个枚举值,这是为了兼容 proto2 语法,在 proto2 中默认值总是第一个枚举值

oneof

根据 Google 文档分析 oneof 有两层意思:

  • 在 oneof 中声明多个字段,同时只有一个字段会被赋值,共享一块内存,主要用来节省内存
  • 如果 oneof 当中一个字段被赋值,然后在给其他字段赋值,会清除其他已赋值字段的值,最终 oneof 所有字段中只会有一个字段有值

我们来看一下简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码message PreferenceMap {
map<string, Value> preferences = 1;
}

message Value {
oneof valueName {
bool boolean = 1;
float float = 2;
int32 integer = 3;
int64 long = 4;
string string = 5;
double double = 7;
}
}
  • 在一个名为 valueName 的 oneof 中声明了很多个字段,这些字段会共享一块内存空间,同时只有一个字段会被赋值
  • 在名为 PreferenceMap 的 message 中声明了一个 map,Key 是字符串类型,Value 其实是 oneof 中声明的字段,同一时间,一个 Key 只会对应一个 Value

其实在编译的时候,会为每个 oneof 生成一个 Java 枚举类型,代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码public enum ValueNameCase {
BOOLEAN(1),
FLOAT(2),
INTEGER(3),
LONG(4),
STRING(5),
DOUBLE(7),
VALUENAME_NOT_SET(0); // 如果都没有赋值,会返回 `VALUENAME_NOT_SET`
private final int value;
private ValueNameCase(int value) {
this.value = value;
}

编译器会自动生成 getValueNameCase() 方法,用来检查哪个字段被赋值了,如果都没有赋值,会返回 VALUENAME_NOT_SET

常用的 proto3 语法到这里就介绍完了,文章只列举了常用的语法,如果需要把 proto3 语法都分析完,至少需要 2 篇文章才有可能介绍完,因为篇幅原因,源码分析部分会在后续的文章中分析。

MAD Skills 是什么

Google 近期发布了 MAD Skills(Modern Android Development)新系列教程,旨在帮助开发者使用最新的技术,开发更好的应用程序,以视频和文章形式介绍 MAD 各个部分,包括 Kotlin、Android Studio、Jetpack、App Bundles 等等, Google 仅仅提供了视频和文章,我在这基础上,我做了一些扩展:

  • 视频添加上了中英文字幕,帮助更好的学习
  • 视频的实战部分,将会提供对应的实战案例
  • 除了实战案例,还会提供对应的源码分析

每隔几个星期 Google 会发布一系列教程,目前已经开始了一系列关于导航组件 (Navigation component) 的视频教程。双语视频已经同步到 GitHub 仓库 MAD-Skills 可以先看视频部分,文章以及案例正在火速赶来。

参考文章

  • Google-DataStore – Jetpack Alternative For SharedPreferences
  • Google-Language Guide (proto3)
  • GitHub-protobuf
  • FlatBuffers 体验
  • Java 对象序列化
  • JSON vs Protocol Buffers vs FlatBuffers

总结

全文到这里就结束了,文章中相关的示例,已经上传到 GitHub 欢迎前去仓库 AndroidX-Jetpack-Practice/DataStoreSimple 切换到 datastore_proto 分支查看。

GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

当这篇文章写完时,已经写了 4 篇文章了,在准备写这篇文章之前,写了三篇文章介绍了 MAC 和 ubuntu 两种命令行编译方式以及 Gradle 插件的方式编译 proto 文件,因为看了下网上的方式都太老了,而且也不是很清楚,Gradle 插件的方式网上大部分都是 3.0.x ~ 3.7.x 的配置方式,当 protoc >= 3.8 之后有一些不同之处,所以重新写了这三种编译方式,以及记录了在这个过程中遇到的问题。

  • Protobuf | 安装 Gradle 插件编译 proto 文件
  • Protobuf | 如何在 ubuntu 上安装 Protobuf 编译 proto 文件
  • Protobuf | 如何在 MAC 上安装 Protobuf 编译 proto 文件

由于目前主要在 MAC 和 ubuntu 上开发,所以只提供了这两种命令行编译方式,如果在 Win 上开发的同学,可以使用 Gradle 插件编译的方式,如果有帮助 点个赞 就是对我最大的鼓励!


最后推荐我一直在更新维护的项目和网站:

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice
  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

+ 剑指 offer 及国内外大厂面试题解:[在线阅读](https://offer.hi-dhl.com)
+ LeetCode 系列题解:[在线阅读](https://leetcode.hi-dhl.com)
  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation
  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站

历史文章

  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(一)
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(二)
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 成员 Paging3 网络实践及原理分析(二)
  • Jetpack成员Paging3获取网络分页数据并更新到数据库中(三)
  • Jetpack 成员 Hilt 实践(一)启程过坑记
  • Jetpack 成员 Hilt 结合 App Startup(二)进阶篇)进阶篇
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
  • 全方面分析 Hilt 和 Koin 性能
  • 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战
  • Kotlin Sealed 是什么?为什么 Google 都用
  • Kotlin StateFlow 搜索功能的实践 DB + NetWork
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • [Google] 再见 SharedPreferences 拥抱 Jetpack DataStore

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot 第十五弹,如何开启远程调试?

发表于 2020-10-29

前言

上周末一个朋友庆生,无意间听他说起了近况,说公司项目太多了,每天一堆BUG需要修复,项目来回切换启动,真是挺烦的。

随着项目越来越多,特别是身处外包公司的朋友,每天可能需要切换两三个项目,难道一有问题就本地启动项目调试?

今天这篇文章就来介绍一下什么是远程调试,Spring Boot如何开启远程调试?

什么是远程调试?

所谓的远程调试就是服务端程序运行在一台远程服务器上,我们可以在本地服务端的代码(前提是本地的代码必须和远程服务器运行的代码一致)中设置断点,每当有请求到远程服务器时时能够在本地知道远程服务端的此时的内部状态。

简单的意思:本地无需启动项目的状态下能够实时调试服务端的代码。

为什么要远程调试?

随着项目的体量越来越大,启动的时间的也是随之增长,何必为了调试一个BUG花费十分钟的时间去启动项目呢?你不怕老大骂你啊?

什么是JPDA?

JPDA(Java Platform Debugger Architecture),即 Java 平台调试体系,具体结构图如下图所示:


其中实现调试功能的主要协议是JDWP协议,在 Java SE 5 以前版本,JVM 端的实现接口是 JVMPI(Java Virtual Machine Profiler Interface),而在Java SE 5及以后版本,使用 JVMTI(Java Virtual Machine Tool Interface) 来替代 JVMPI。

因此,如果你使用的是Java SE 5之前的版本,则使用的调试命令格式如下:

1
复制代码java -Xdebug -Xrunjdwp:...

如果你使用的是Java SE 5之后的版本,则使用的命令格式如下:

1
复制代码java -agentlib:jdwp=...

如何开启远程调试?

由于现在使用的大多数都是Java SE 5之后的版本,则之前的就忽略了。

日常开发中最常见的开启远程调试的命令如下:

1
复制代码java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9093 -jar xxx.jar

前面的java -agentlib:jdwp=是基础命令,后面的跟着的一串命令则是可选的参数,具体什么意思呢?下面详细介绍。

transport

指定运行的被调试应用和调试者之间的通信协议,有如下可选值:

  1. dt_socket: 采用socket方式连接(常用)
  2. dt_shmem:采用共享内存的方式连接,支持有限,仅仅支持windows平台

server

指定当前应用作为调试服务端还是客户端,默认的值为n(客户端)。

如果你想将当前应用作为被调试应用,设置该值为y;如果你想将当前应用作为客户端,作为调试的发起者,设置该值为n。

suspend

当前应用启动后,是否阻塞应用直到被连接,默认值为y(阻塞)。

大部分情况下这个值应该为n,即不需要阻塞等待连接。一个可能为y的应用场景是,你的程序在启动时出现了一个故障,为了调试,必须等到调试方连接上来后程序再启动。

address

对外暴露的端口,默认值是8000

注意:此端口不能和项目同一个端口,且未被占用以及对外开放。

onthrow

这个参数的意思是当程序抛出指定异常时,则中断调试。

onuncaught

当程序抛出未捕获异常时,是否中断调试,默认值为n。

launch

当调试中断时,执行的程序。

timeout

超时时间,单位ms(毫秒)

当 suspend = y 时,该值表示等待连接的超时;当 suspend = n 时,该值表示连接后的使用超时。

常用的命令

下面列举几个常用的参考命令,这样更加方便理解。

  1. 以Socket 方式监听 8000 端口,程序启动阻塞(suspend 的默认值为 y)直到被连接,命令如下:
1
复制代码-agentlib:jdwp=transport=dt_socket,server=y,address=8000
  1. 以 Socket 方式监听 8000 端口,当程序启动后 5 秒无调试者连接的话终止,程序启动阻塞(suspend 的默认值为 y)直到被连接。
1
复制代码-agentlib:jdwp=transport=dt_socket,server=y,address=localhost:8000,timeout=5000
  1. 选择可用的共享内存连接地址并使用 stdout 打印,程序启动不阻塞。
1
复制代码-agentlib:jdwp=transport=dt_shmem,server=y,suspend=n
  1. 以 socket 方式连接到 myhost:8000上的调试程序,在连接成功前启动阻塞。
1
复制代码-agentlib:jdwp=transport=dt_socket,address=myhost:8000
  1. 以 Socket 方式监听 8000 端口,程序启动阻塞(suspend 的默认值为 y)直到被连接。当抛出 IOException 时中断调试,转而执行 usr/local/bin/debugstub程序。
1
复制代码-agentlib:jdwp=transport=dt_socket,server=y,address=8000,onthrow=java.io.IOException,launch=/usr/local/bin/debugstub

IDEA如何开启远程调试?

首先的将打包后的Spring Boot项目在服务器上运行,执行如下命令(各种参数根据实际情况自己配置):

1
复制代码java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9193 -jar debug-demo.jar

项目启动成功后,点击 Edit Configurations,在弹框中点击 + 号,然后选择Remote。


然后填写服务器的地址及端口,点击 OK 即可。


以上步骤配置完成后,点击DEBUG调试运行即可。


配置完毕后点击保存即可,因为我配置的 suspend=n,因此服务端程序无需阻塞等待我们的连接。我们点击 IDEA 调试按钮,当我访问某一接口时,能够正常调试。


总结
–

每天一个小知识,今天你学到了吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Stream流的这些操作,你得知道,对你工作有很大帮助

发表于 2020-10-28

Stream流

Stream(流)是一个来自数据源的元素队列并支持聚合操作:

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而 是按需计算。
  • 数据源 流的来源。 可以是集合,数组等。
  • 聚合操作类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted 等。

Stream流操作的三个步骤:

  1. 创建Stream

一个数据源(如:集合、数组),获取一个流
2. 中间操作

一个中间操作链,对数据源的数据进行处理
3. 终止操作

一个终止操作,执行中间操作链,并产生结果

创建Stream步骤:

  • 通过Collection系列集合提供的顺序流stream()或并行流parallelStream()
  • 通过Arrays中的静态方法stream()获取数据流
  • 通过Stream类中的静态方法of()

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码package com.ysh.review01_Stream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamTest01 {
public static void main(String[] args) {
//第一种方式:通过集合中的stream()方法创建Stream
List<String> list= Arrays.asList("红太狼","灰太狼","喜羊羊");
Stream<String> stream=list.stream();
//通过集合中的parallelStream方法创建
Stream<String> stream2 = list.parallelStream();
//第二种方式:通过java.util.Arrays下的静态方法stream创建Stream
Integer[] integer=new Integer[]{1,2,4};
//这里需要注意的是Arrays中的stream方法里面的参数需要一个数组,且数组的类型是一个引用类型或者是一个包装类
Stream<Integer> stream3 = Arrays.stream(integer);
//第三种方式:通过Stream中的of方法,实际上这种方式创建Stream实际上间接的通过调用Arrays中的stream()静态方法
Stream<String> stream4=Stream.of("a","b","c");
}
}

Stream的中间操作

筛选和切片

filter:过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
typescript复制代码package com.ysh.review01_Stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StramTest02 {
public static void main(String[] args) {
Employee employee01=new Employee("yang","hui",29);
Employee employee02=new Employee("yang","hui",49);
Employee employee03=new Employee("yang","hui",9);
Employee employee04=new Employee("yang","hui",89);
Employee employee05=new Employee("yang","hui",89);
Employee employee06=new Employee("yang","hui",89);
List<Employee> list= Arrays.asList(employee01,employee02,employee03,employee04,employee05,employee06);
//创建Stream
Stream<Employee> stream1 = list.stream();
//对stream1流进行过滤
Stream<Employee> s = stream1.filter((e) -> {
System.out.println("---------------filter------------");
//过滤掉年龄小于19
return e.getAge() >= 19;
});
s.forEach((e-> System.out.println(e)));
}
}
class Employee {
private String id;
private String name;
private int age;

public Employee() {

}

public Employee(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age &&
id.equals(employee.id) &&
name.equals(employee.name);
}

@Override
public String toString() {
return "Employee{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}

运行结果:

skip(n):指跳过Stream中存储的前n条数据(包含第n条数据),返回后n条数据,如果n大于Stream中所有元素的个数,则返回空;(Employee类如上)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ini复制代码package com.ysh.review01_Stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StramTest02 {
public static void main(String[] args) {
Employee employee01=new Employee("yang","hui",9);
Employee employee02=new Employee("yang","hui",49);
Employee employee03=new Employee("yang","hui",9);
Employee employee04=new Employee("yang","hui",89);
Employee employee05=new Employee("yang","hui",89);
Employee employee06=new Employee("yang","hui",89);
List<Employee> list= Arrays.asList(employee01,employee02,employee03,employee04,employee05,employee06);
//创建Stream
Stream<Employee> stream1 = list.stream();
//对stream1流进行过滤
Stream<Employee> s = stream1.filter((e) -> {
System.out.println("---------------filter------------");
//过滤掉年龄小于19
return e.getAge() >= 19;
}).skip(2);
//s=s.skip(5);
s.forEach((e-> System.out.println(e)));
}
}

运行截图:

distinct:筛选重复的元素,通过流生产元素的hashCode()和equals去除重复元素;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ini复制代码package com.ysh.review01_Stream;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

public class StramTest02 {
public static void main(String[] args) {
Employee employee01=new Employee("yang","hui",9);
Employee employee02=new Employee("yang","hui",49);
Employee employee03=new Employee("yang","hui",9);
Employee employee04=new Employee("yang","hui",89);
Employee employee05=new Employee("yang","hui",89);
Employee employee06=new Employee("yang","hui",89);
List<Employee> list= Arrays.asList(employee01,employee02,employee03,employee04,employee05,employee06);
//创建Stream
Stream<Employee> stream1 = list.stream();
//对stream1流进行过滤
Stream<Employee> s = stream1.filter((e) -> {
System.out.println("---------------filter------------");
//过滤掉年龄小于19
return e.getAge() >= 19;
}).skip(2).distinct();
//s=s.skip(5);
s.forEach((e-> System.out.println(e)));
}
}

运行截图:

排序:

sorted(Comparable)–自然排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.ysh.review01_Stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamTest03 {
public static void main(String[] args) {
List<String> list= Arrays.asList("c","bbb","abc","bbbb");
Stream<String> stream = list.stream();
//即通过调用String方法中CompareTo,通过一个一个的比较字符的ASCLL值,首先比较首字符的ASCLL大小,相同的话再比较下一个
stream= stream.sorted();
stream.forEach(System.out::println);
}
}

sorted(Comparator)–定制排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ini复制代码package com.ysh.review01_Stream;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

public class StramTest02 {
public static void main(String[] args) {
Employee employee01=new Employee("yang","哈哈",19);
Employee employee02=new Employee("yang","hui",49);
Employee employee03=new Employee("yang","hui",79);
Employee employee04=new Employee("yang","呵呵呵",79);
Employee employee05=new Employee("yang","hui",39);
Employee employee06=new Employee("yang","hui",29);
List<Employee> list= Arrays.asList(employee01,employee02,employee03,employee04,employee05,employee06);
//创建Stream
Stream<Employee> stream1 = list.stream();
//对stream1流进行过滤
Stream<Employee> s = stream1.filter((e) -> {
System.out.println("---------------filter------------");
//过滤掉年龄小于19
return e.getAge() >= 19;
});
//s=s.skip(5);
/*此处可以使用Lambda表达式,即s.sorted((o1,o2)->{
//升序排序,如果年龄相同,则按照姓名的长度排序
if (o1.getAge()==o2.getAge()){
return o1.getName().length()-o2.getName().length();
}
//按照年龄升序排序
return o1.getAge()-o2.getAge();

})

*/
s=s.sorted(new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
//升序排序,如果年龄相同,则按照姓名的长度排序
if (o1.getAge()==o2.getAge()){
return o1.getName().length()-o2.getName().length();
}
//按照年龄升序排序
return o1.getAge()-o2.getAge();
}
});
s.forEach((e-> System.out.println(e)));
}
}

Stream中的map和flatMap方法:

  • 流中的每一个数据,当做map方法的参数(接口),接口中抽象方法的参数,进行制定操作,最终得到一个结果,最后所有的结果返回去成为一个流
  • 流中的每一个数据当作参数,进行操作,得到的结果必须是一个流,最终会结合成一个流返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ini复制代码package com.ysh.review01_Stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamTest04 {
public static void main(String[] args) {
Stream<String> stream=Stream.of("aaa","bbbb","ccccc");
//map方法是每一个数据当作一个流,即以上{aaa}、{bbbb}、{ccccc}各是一个Stream<Character>流集合,即达到得到多个Stream<Character>流集合
//可以理解为Stream流中包含Stream<Character>流
//因为这里的testCharacter()的方法我返回的是一个Stream<Character>
//Stream<Stream<Character>> streamStream = stream.map((e) -> testCharacter(e));
Stream<Stream<Character>> streamStream = stream.map((e) -> {
List<Character> list = new ArrayList<>();
for (Character c : e.toCharArray()) {
list.add(c);
}
return list.stream();
});
streamStream.forEach((e)->{
e.forEach((e2)->{
System.out.println(e2);
});
});
Stream<String> stm=Stream.of("aaa","bbbb","ccccc");
//flatMap()方法即是把将得到的多个Stream<Character>流集合合并为一个一个Stream<Character>流集合
Stream<Character> stream1=stm.flatMap(StreamTest04::testCharacter);
//streamStream.forEach(System.out::println);
System.out.println("---------------");
stream1.forEach(System.out::println);
}
//返回一个Stream
public static Stream<Character> testCharacter(String str){
List<Character> list=new ArrayList<>();
for (Character c:str.toCharArray()){
list.add(c);
}
Stream<Character> stream=list.stream();
return stream;
}
}

终止操作

查找与匹配

  • allMatch:检查是否匹配所有元素;
  • anyMatch:检查是否至少匹配一个元素;
  • noneMatch:检查是否没有匹配所有元素;
  • findFirst:返回第一个元素;
  • findAny:返回当前流中的任意元素;
  • count:返回流中元素的总个数;
  • max:返回流中最大值;
  • min:返回流中最小值;

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
ini复制代码package com.ysh.review01_Stream;

import java.util.Optional;
import java.util.stream.Stream;

/**
* Stream中的终止操作
*/
public class StreamTest06 {
public static void main(String[] args) {
//获取Stream
Stream<String> stm1=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
//allMatch()方法里面的参数是一个断言式接口,即实现必须重写test()方法
boolean b1 = stm1.allMatch((t) -> {
//检查是否匹配所有元素
return t.length() > 2;
});
Stream<String> stm2=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
boolean b2=stm2.anyMatch((t) -> {
//检查是否至少匹配一个元素
return t.length() > 2;
});
Stream<String> stm3=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
boolean b3=stm3.noneMatch((t) -> {
//检查是否没有匹配所有元素
return t.length() > 2;
});
Stream<String> stm4=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
//得到流中的第一个元素
Optional<String> first = stm4.findFirst();
//输出
System.out.println(first.get());
Stream<String> stm5=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
//返回当前流中的任意元素
Optional<String> any = stm5.findAny();
System.out.println(any.get());
Stream<String> stm6=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
//放回流中元素的总个数
long count = stm6.count();
System.out.println(count);
Stream<String> stm7=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
//返回流中最大值,即长度最长,长度相同则比较ASCLL值大小
Optional<String> max = stm7.max((s1, s2) -> {
if (s1.length()==s2.length()){
return s1.compareTo(s2);
}
return s1.length() - s2.length();
});
System.out.println(max.get());
Stream<String> stm8=Stream.of("aaaaaa","bbbbb","cccccc","dd","eee");
//返回流中最小值,即长度最短,长度相同则比较ASCLL值大小
Optional<String> min = stm8.min((s1, s2) -> {
if (s1.length() == s2.length()) {
return s2.compareTo(s1);
}
return s1.length() - s2.length();
});
System.out.println(min.get());
System.out.println(b3);
}
}

收集:

收集-将流转换为其他形式,接收一个Collertor接口的实现,用于给Stream中元素做 汇总的方法

  • List:把流中所有元素收集到List中,使用.collect(Collectors.toList());
  • Set:把流中所有元素收集到Set中,删除重复项,使用.collect(Collectors.toSet());
  • Map:把流中所有元素收集到Map中,当出现相同的key时会抛异常,使用 .collect(Collectors.toMap());
  • 使用collect方法求流中共有几条数据,使用.collect(Collectors.counting())
  • 使用collect方法求平均数,使用.collect(Collectors.averagingInt();
  • 使用collect方法求某个变量的总和,使用.collect(Collectors.summingDouble());
  • 使用collect方法且某个变量中值的最大值,使用.collect(Collectors.maxBy());

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
ini复制代码package com.ysh.review01_Stream.one;

import java.util.*;
import java.util.stream.Collectors;

public class StreamTest07 {
public static void main(String[] args) {
Student stu1=new Student("1","hhhh",18);
Student stu2=new Student("2","hhhhh",19);
Student stu3=new Student("3","oooooo",19);
Student stu4=new Student("4","aaaaa",19);
List<Student> list = Arrays.asList(stu1,stu2,stu3,stu4);
//获取所有学生的姓名流,并且存储再List集合中
List<String> collect = list.stream().map((e) -> {
return e.getName();
}).collect(Collectors.toList());
System.out.println(collect);
Set<String> set = list.stream().map((e) -> {
return e.getName();
}).collect(Collectors.toSet());
System.out.println(collect);
//将学生的姓名和年龄放入到一个集合中,当出现相同的key是会抛出一个java.lang.IllegalStateException: Duplicate key异常
Map<String, Integer> map = list.stream().collect(Collectors.toMap((e) -> e.getName(), (e2) -> e2.getAge()));
System.out.println(map);
//运用collect输出所有学生的总数
Long count2 = list.stream().collect(Collectors.counting());
System.out.println(count2);
//运用collect方法计算所有学生的平均年龄
Double collect1 = list.stream().collect(Collectors.averagingDouble((n) -> n.getAge()));
System.out.println(collect1);
//运用collect方法求所有学生的年龄之和
int agesum=list.stream().collect(Collectors.summingInt((e)->e.getAge()));
System.out.println(agesum);
//运用collect方法求所有学生中年龄最大的
Optional<Student> max2 = list.stream().collect(Collectors.maxBy((a1, a2) -> a1.getAge() - a2.getAge()));
System.out.println(max2.get());

}
}
class Student {
private String id;
private String name;
private int age;
public Student(){

}

public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
id.equals(student.id) &&
name.equals(student.name);
}

@Override
public int hashCode() {
return Objects.hash(id, name, age);
}

@Override
public String toString() {
return "Employee{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}

最后

感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何让网站和API都支持HTTPS?在Nginx上做文章是个

发表于 2020-10-28

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

随着我们网站用户的增多,我们会逐渐意识到HTTPS加密的重要性。在不修改现有代码的情况下,要从HTTP升级到HTTPS,让Nginx支持HTTPS是个很好的选择。今天我们来讲下如何从Nginx入手,从HTTP升级到HTTPS,同时支持静态网站和SpringBoot应用,希望对大家有所帮助!

生成SSL自签名证书

虽然自签名证书浏览器认为并不是安全的,但是学习下SSL证书的生成还是很有必要的!

  • 首先创建SSL证书私钥,期间需要输入两次用户名和密码,生成文件为blog.key;
1
bash复制代码openssl genrsa -des3 -out blog.key 2048
  • 利用私钥生成一个不需要输入密码的密钥文件,生成文件为blog_nopass.key;
1
bash复制代码openssl rsa -in blog.key -out blog_nopass.key
  • 创建SSL证书签名请求文件,生成SSL证书时需要使用到,生成文件为blog.csr;
1
bash复制代码openssl req -new -key blog.key -out blog.csr
  • 在生成过程中,我们需要输入一些信息,需要注意的是Common Name需要和网站域名一致;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码Enter pass phrase for blog.key:
-----
Country Name (2 letter code) [XX]:CN # 国家代码
State or Province Name (full name) []:jiangsu # 省份
Locality Name (eg, city) [Default City]:jiangsu # 城市
Organization Name (eg, company) [Default Company Ltd]:macrozheng # 机构名称
Organizational Unit Name (eg, section) []:dev # 单位名称
Common Name (eg, your name or your server's hostname) []:blog.macrozheng.com # 网站域名
Email Address []:macrozheng@qq.com # 邮箱

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: # 私钥保护密码,可以不输入直接回车
An optional company name []: # 可选公司名称,可以不输入直接回车
  • 生成SSL证书,有效期为365天,生成文件为blog.crt;
1
bash复制代码openssl x509 -req -days 365 -in blog.csr -signkey blog.key -out blog.crt
  • 其实最终有用的文件是两个,一个是证书文件blog.crt,另一个是不需要输入密码的证书私钥文件blog_nopass.key。

Nginx支持HTTPS

SSL证书生成好了,接下来我们就可以配置Nginx来支持HTTPS了!

安装Nginx

  • 我们还是使用在Docker容器中安装Nginx的方式,先下载Nginx的Docker镜像;
1
bash复制代码docker pull nginx:1.10
  • 下载完成后先运行一次Nginx,由于之后我们要把宿主机的Nginx配置文件映射到Docker容器中去,运行一次方便我们拷贝默认配置;
1
2
3
4
bash复制代码docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-d nginx:1.10
  • 运行成功后将容器中的Nginx配置目录拷贝到宿主机上去;
1
bash复制代码docker container cp nginx:/etc/nginx /mydata/nginx/
  • 将宿主机上的nginx目录改名为conf,要不然/mydata/nginx/nginx这个配置文件目录看着有点别扭;
1
bash复制代码mv /mydata/nginx/nginx /mydata/nginx/conf
  • 创建的Nginx容器复制完配置后就没用了,停止并删除容器;
1
2
bash复制代码docker stop nginx
docker rm nginx
  • 使用Docker命令重新启动Nginx服务,需要映射好配置文件,由于我们要支持HTTPS,还需要开放443端口。
1
2
3
4
5
bash复制代码docker run -p 80:80 -p 443:443 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10

配置支持HTTPS

  • 将我们生成好的SSL证书和私钥拷贝到Nginx的html/ssl目录下;
1
2
bash复制代码cp blog_nopass.key /mydata/nginx/html/ssl/
cp blog.crt /mydata/nginx/html/ssl/
  • 接下来我们需要给blog.macrozheng.com这个域名添加HTTPS支持,在/mydata/nginx/conf/conf.d/目录下添加Nginx配置文件blog.conf,配置文件内容如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ini复制代码server {
listen 80; # 同时支持HTTP
listen 443 ssl; # 添加HTTPS支持
server_name blog.macrozheng.com;

#SSL配置
ssl_certificate /usr/share/nginx/html/ssl/blog/blog.crt; # 配置证书
ssl_certificate_key /usr/share/nginx/html/ssl/blog/blog_nopass.key; # 配置证书私钥
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # 配置SSL协议版本
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; # 配置SSL加密算法
ssl_prefer_server_ciphers on; # 优先采取服务器算法
ssl_session_cache shared:SSL:10m; # 配置共享会话缓存大小
ssl_session_timeout 10m; # 配置会话超时时间

location / {
root /usr/share/nginx/html/www;
index index.html index.htm;
}

location /admin {
alias /usr/share/nginx/html/admin;
index index.html index.htm;
}

location /app {
alias /usr/share/nginx/html/app;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
  • 通过HTTPS访问blog.macrozheng.com这个域名,由于我们使用的是自己签名的SSL证书,浏览器会提示您的连接不是私密连接,点击继续前往可以通过HTTPS正常访问;

  • 我们可以查看下证书的颁发者信息,可以发现正好是之前我们创建SSL证书签名请求文件时录入的信息;

  • 接下来我们需要给api.macrozheng.com这个域名添加HTTPS支持,通过这个域名可以使用HTTPS访问我们的SpringBoot应用,api.crt和api_nopass.key文件需要自行生成,在/mydata/nginx/conf/conf.d/目录下添加Nginx配置文件api.conf,配置文件内容如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
perl复制代码server {
listen 80; # 同时支持HTTP
listen 443 ssl; # 添加HTTPS支持
server_name api.macrozheng.com; #修改域名

#ssl配置
ssl_certificate /usr/share/nginx/html/ssl/api/api.crt; # 配置证书
ssl_certificate_key /usr/share/nginx/html/ssl/api/api_nopass.key; # 配置证书私钥
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # 配置SSL协议版本 # 配置SSL加密算法
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on; # 优先采取服务器算法
ssl_session_cache shared:SSL:10m; # 配置共享会话缓存大小
ssl_session_timeout 10m; # 配置会话超时时间

location / {
proxy_pass http://192.168.3.101:8080; # 设置代理服务访问地址
proxy_set_header Host $http_host; # 设置客户端真实的域名(包括端口号)
proxy_set_header X-Real-IP $remote_addr; # 设置客户端真实IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 设置在多层代理时会包含真实客户端及中间每个代理服务器的IP
proxy_set_header X-Forwarded-Proto $scheme; # 设置客户端真实的协议(http还是https)
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
  • 通过HTTPS访问api.macrozheng.com这个域名,访问地址为:api.macrozheng.com/swagger-ui.…

  • 任意调用一个接口测试下,比如说登录接口,可以发现已经可以通过HTTPS正常访问SpringBoot应用提供的接口。

使用受信任的证书

之前我们使用的是自签名的SSL证书,对于浏览器来说是无效的。使用权威机构颁发的SSL证书浏览器才会认为是有效的,这里给大家推荐两种申请免费SSL证书的方法,一种是从阿里云申请,另一种是从FreeSSL申请。

阿里云证书

  • 阿里云上可以申请的免费证书目前只有支持单个域名的DV级SSL证书。比如说你有blog.macrozheng.com和api.macrozheng.com两个二级域名需要使用HTTPS,就需要申请两个SSL证书。

  • 申请成功后点击下载Nginx证书即可;

  • 下载完成后解压会有下面两个文件;
1
2
bash复制代码blog.macrozheng.com.key # 证书私钥文件
blog.macrozheng.com.pem # 证书文件
  • 拷贝证书文件到Nginx的指定目录下,然后修改配置文件blog.conf,只要修改证书配置路径即可,修改完成后重启Nginx;
1
2
3
bash复制代码#SSL配置
ssl_certificate /usr/share/nginx/html/ssl/blog/blog.macrozheng.com.pem; # 配置证书
ssl_certificate_key /usr/share/nginx/html/ssl/blog/blog.macrozheng.com.key; # 配置证书私钥
  • 再次通过HTTPS访问blog.macrozheng.com这个域名,发现证书已经有效了,连接也是安全的了。

FreeSSL证书

  • 如果你有使用通配符域名的需求,可以上FreeSSL申请SSL证书,不过免费的有效期只有3个月,这就意味着你过3个月就要重新申请一次了。

  • 附上官网地址:freessl.cn/

使用acme.sh自动申请证书

  • acme.sh脚本实现了acme协议, 可以从letsencrypt生成免费的证书。一般我们申请的证书有效期都是1年,过期就要重新申请了,使用acme.sh脚本可以实现到期自动申请,再也不用担心证书过期了!

  • 附上官网地址:github.com/acmesh-offi…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Java 路线」 关于泛型能问的都在这里了(含Kotli

发表于 2020-10-28

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 泛型(Generic Type) 无论在哪一门语言里,都是最难语法的存在,细节之繁杂、理解之困难,令人切齿;
  • 在这个系列里,我将总结Java & Kotlin中泛型的知识点,带你从 语法 & 原理 全面理解泛型。追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!
  • 首先,尝试回答这些面试中容易出现的问题,相信看完这篇文章,这些题目都难不倒你:
1
2
3
4
5
6
7
8
9
10
typescript复制代码1、下列代码中,编译出错的是:
public class MyClass<T> {
private T t0; // 0
private static T t1; // 1
private T func0(T t) { return t; } // 2
private static T func1(T t) { return t; } // 3
private static <T> T func2(T t) { return t; } // 4
}
2、泛型的存在是用来解决什么问题?
3、请说明泛型的原理,什么是泛型擦除机制,具体是怎样实现的?

相关文章

  • 《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》
  • 《Java | 反射:在运行时访问类型信息(含 Kotlin)》
  • 《Java | 请概述一下 Class 文件的结构》
  • 《Java | 深入理解方法调用的本质(含重载与重写区别)》

目录


  1. 泛型基础

  • 问:什么是泛型,有什么作用?

答:在定义类、接口和方法时,可以附带类型参数,使其变成泛型类、泛型接口和泛型方法。与非泛型代码相比,使用泛型有三大优点:更健壮(在编译时进行更强的类型检查)、更简洁(消除强转,编译后自动会增加强转)、更通用(代码可适用于多种类型)

  • 问:什么是类型擦除机制?

答:泛型本质上是 Javac 编译器的一颗 语法糖,这是因为:泛型是 JDK1.5 中引进的新特性,为了 向下兼容,Java 虚拟机和 Class 文件并没有提供泛型的支持,而是让编译器擦除 Code 属性中所有的泛型信息,需要注意的是,泛型信息会保留在类常量池的属性中。

  • 问:类型擦除的具体步骤?

答:类型擦除发生在编译时,具体分为以下 3 个步骤:

  • 1:擦除所有类型参数信息,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object
  • 2:(必要时)插入类型转换,以保持类型安全
  • 3:(必要时)生成桥接方法以在子类中保留多态性

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
typescript复制代码源码:
public class Parent<T> {
public void func(T t){
}
}

public class Child<T extends Number> extends Parent<T> {
public T get() {
return null;
}
public void func(T t){
}
}

void test(){
Child<Integer> child = new Child<>();
Integer i = child.get();
}
---------------------------------------------------------
字节码:
public class Parent {
public void func(Object t){
}
}

public class Child extends Parent {
public Number get() {
return null;
}
public void func(Number t) {
}

桥方法 - synthetic
public void func(Object t){
func((Number)t);
}
}

void test() {
Child<Integer> child = new Child();
// 插入强制类型转换
Integer i = (Integer) child.get();
}

步骤1:Parent 中的类型参数 T 被擦除为 Object,而 Child 中的类型参数 T 被擦除为 Number;

步骤2:child.get(); 插入了强制类型转换

步骤3:在 Child 中生成桥方法,桥方法是编译器生成的,所以会带有 synthetic 标志位。为什么子类中需要增加桥方法呢,可以先思考这个问题:假如没有桥方法,会怎么样?你可以看看下列代码调用的是子类还是父类方法:

1
2
3
4
5
ini复制代码Parent<Integer> child = new Child<>();
Parent<Integer> parent = new Parent<>();

child.func(1); // Parent#func(Object);
parent.func(1); // Parent#func(Object);

这两句代码都会调用到 Parent#func(),如果你看过之前我写过的一篇文章,相信难不到你:《Java | 深入理解方法调用的本质(含重载与重写区别)》。在这里我简单分析下:

1、方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)

2、这两句代码调用的方法符号引用为:

child.func(new Object()) => com/xurui/Child.func(Object)

parent.func(new Object()) => com/xurui/Parent.func(Object)

3、这两句方法调用的字节码指令为 invokevirtual

4、类加载解析阶段解析类的继承关系,生成类的虚方法表

5、调用阶段(动态分派):Child 没有重写 func(Object),所以 Child 的虚方法表中存储的是Parent#func(Object);Parent 的虚方法表中存储的是Parent#func(Object);

可以看到,即使使用对象的实际类型为 Child ,这里调用的依旧是父类的方法。这样就 失去了多态性。 因此,才需要在泛型子类中添加桥方法。

  • 问:为什么擦除后,反编译还是看到类型参数 T ?
1
2
3
4
5
6
7
8
9
csharp复制代码反编译Parent.class,可以看到 T ,不是已经擦除了吗?

public class Parent<T> {
public Parent() {
}

public void func(T t) {
}
}

答:泛型中所谓的类型擦除,其实只是擦除Code 属性中的泛型信息,在类常量池属性(Signature 属性、LocalVariableTypeTable 属性)中其实还保留着泛型信息,这也是在运行时可以反射获取泛型信息的根本依据,我在第 4 节说。

  • 问:泛型的限制 & 类型擦除会带来什么影响?

由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。为了避免程序的运行结果与程序员语义不一致的情况,泛型在使用上存在一些限制。好处是类型擦除不会为每种参数化类型创建新的类,因此泛型不会增大内存消耗。

泛型的限制


  1. Kotlin的实化类型参数

前面我们提到,由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。例如下面的代码是不合法的,因为T并不是一个真正的类型,而仅仅是一个符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:

Java:
<T> List<T> filter(List list) {
List<T> result = new ArrayList<>();
for (Object e : list) {
if (e instanceof T) { // compiler error
result.add(e);
}
}
return result;
}
---------------------------------------------------
Kotlin:
fun <T> filter(list: List<*>): List<T> {
val result = ArrayList<T>()
for (e in list) {
if (e is T) { // cannot check for instance of erased type: T
result.add(e)
}
}
return result
}

在Kotlin中,有一种方法可以突破这种限制,即:带实化类型参数的内联函数:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码Kotlin:
inline fun <reified T> filter(list: List<*>): List<T> {
val result = ArrayList<T>()
for (e in list) {
if (e is T) {
result.add(e)
}
}
return result
}

关键在于inline和reified,这两者的语义是:

  • inline(内联函数): Kotlin编译器将内联函数的字节码插入到每一次调用方法的地方
  • reified(实化类型参数): 在插入的字节码中,使用类型实参的确切类型代替类型实参

规则很好理解,对吧。很明显,当发生方法内联时,方法体字节码就变成了:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码调用:
val list = listOf("", 1, false)
val strList = filter<String>(list)
---------------------------------------------------
内联后:
val result = ArrayList<String>()
for (e in list) {
if (e is String) {
result.add(e)
}
}

需要注意的是,内联函数整个方法体字节码会被插入到调用位置,因此控制内联函数体的大小。如果函数体过大,应该将不依赖于T的代码抽取到单独的非内联函数中。

注意,无法从 Java 代码里调用带实化类型参数的内联函数

实化类型参数的另一个妙用是代替 Class 对象引用,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码fun Context.startActivity(clazz: Class<*>) {
Intent(this, clazz).apply {
startActivity(this)
}
}

inline fun <reified T> Context.startActivity() {
Intent(this, T::class.java).apply {
startActivity(this)
}
}

调用方:
context.startActivity(MainActivity::class.java)
context.startActivity<MainActivity>() // 第二种方式会简化一些

  1. 变型:协变 & 逆变 & 不变

变型(Variant)描述的是相同原始类型的不同参数化类型之间的关系。说起来有点绕,其实就是说:Integer是Number的子类型,问你List<Integer>是不是List<Number>的子类型?

变型的种类具体分为三种:协变型 & 逆变型 & 不变型

  • 协变型(covariant): 子类型关系被保留
  • 逆变型(contravariant): 子类型关系被翻转
  • 不变型(invariant): 子类型关系被消除

在 Java 中,类型参数默认是不变型的,例如:

1
2
3
ini复制代码List<Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // compiler error

相比之下,数组是支持协变型的:

1
2
3
ini复制代码Number[] nums;
Integer[] ints = new Integer[10];
nums = ints; // OK 协变,子类型关系被保留

那么,当我们需要将List<Integer>类型的对象,赋值给List<Number>类型的引用时,应该怎么做呢?这个时候我们需要限定通配符:

  • <? extends> 上界通配符

要想类型参数支持协变,需要使用上界通配符,例如:

1
2
3
ini复制代码List<? extends Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK

但是这会引入一个编译时限制:不能调用参数包含类型参数 E 的方法,也不能设置类型参数的字段,简单来说,就是只能访问不能修改(非严格):

1
2
3
4
5
6
typescript复制代码// ArrayList.java
public boolean add(E e) {
...
}

l1.add(1); // compiler error
  • <? super> 下界通配符

要想类型参数支持逆变,需要使用下界通配符,例如:

1
2
3
ini复制代码List<? super Integer> l1;
List<Number> l2 = new ArrayList<>();
l1 = l2; // OK

同样,这也会引入一个编译时限制,但是与协变相反:不能调用返回值为类型参数的方法,也不能访问类型参数的字段,简单来说,就是只能修改不能访问(非严格):

1
2
3
4
5
6
csharp复制代码// ArrayList.java
public E get(int index) {
...
}

Integer i = l1.get(0); // compiler error
  • <?> 无界通配符

其实很简单,很多资料其实都解释得过于复杂了。 < ?> 其实就是 的缩写。例如:

1
2
3
ini复制代码List<?> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK

理解了这点,这个问题就很好回答了:

  • 问:List 与 List<?>有什么区别?

答:List 是原生类型,可以添加或访问元素,不具备编译期安全性,而 List 其实是 List的缩写,是协变型的(可引出协变型的特点与限制);从语义上,List 表明使用者清楚变量是类型安全的,而不是因为疏忽而使用了原生类型 List。

泛型代码的设计,应遵循PECS原则(Producer extends Consumer super):

  • 如果只需要获取元素,使用 <? extends T>
  • 如果只需要存储,使用<? super T>

举例:

// Collections.java
public static void copy(List<? super T> dest, List<? extends T> src) {
}

在 Kotlin 中,变型写法会有些不同,但是语义是完全一样的:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码协变:
val l0: MutableList<*> 相当于MutableList<out Any?>
val l1: MutableList<out Number>
val l2 = ArrayList<Int>()
l0 = l2 // OK
l1 = l2 // OK
---------------------------------------------------
逆变:
val l1: MutableList<in Int>
val l2 = ArrayList<Number>()
l1 = l2 // OK

另外,Kotlin 的in & out不仅仅可以用在类型实参上,还可以用在泛型类型声明的类型参数上。其实这是一种简便写法,表示类设计者知道类型参数在整个类上只能协变或逆变,避免在每个使用的地方增加,例如 Kotlin 的List被设计为不可修改的协变型:

1
2
3
csharp复制代码public interface List<out E> : Collection<E> {
...
}

注意:在 Java 中,只支持使用点变型,不支持 Kotlin 类似的声明点变型

小结一下:


  1. 使用反射获取泛型信息

前面提到了,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。

获取泛型类型实参:需要利用Type体系

4.1 获取泛型类 & 泛型接口声明

TypeVariable
ParameterizedType
GenericArrayType
WildcardType

Gson TypeToken

Editting….


  1. 总结

  • 应试建议
    • 1、第 1 节非常非常重点,着重记忆:泛型的本质和设计缘由、泛型擦除的三个步骤、限制和优点,已经总结得很精华了,希望能帮到你;
    • 2、着重理解变型(Variant)的概念,以及各种限定符的含义;
    • 3、Kotlin 相关的部分,作为知识积累和思路扩展为主,非应试重点。

参考资料

  • 《Kotlin实战》 (第9、10章)—— [俄] Dmitry Jemerov,Svetlana Isakova 著
  • 《Java编程思想》 (第19、20、23章)—— [美] Bruce Eckel 著
  • 《深入理解Java虚拟机(第3版本)》(第10章)—— 周志明 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

协程中的取消和异常 异常处理详解

发表于 2020-10-27

开发者们通常会在打磨应用的正常功能上花费很多时间,但是当应用出现一些意外情况时,给用户提供合适的体验也同样重要。一方面来讲,对用户来说,目睹应用崩溃是个很糟糕的体验;而另一方面,在用户操作失败时,也必须要能给出正确的提示信息。

正确地处理异常,可以很大程度上改进用户对一个应用的看法。接下来,本文将会解释异常是如何在协程间传播的,以及一些处理它们的方法,从而帮您做到一切尽在掌握。

⚠️ 为了能够更好地理解本文所讲的内容,建议您首先阅读本系列中的第一篇文章: 协程中的取消和异常 | 核心概念介绍。

某个协程突然运行失败怎么办?😱

当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级。接下来,父级会进行下面几步操作:

  • 取消它自己的子级;
  • 取消它自己;
  • 将异常传播并传递给它的父级。

异常会到达层级的根部,而且当前 CoroutineScope 所启动的所有协程都会被取消。

△ 协程中的异常会通过协程的层级不断传播

△ 协程中的异常会通过协程的层级不断传播

虽然在一些情况下这种传播逻辑十分合理,但换一种情况您可能就不这么想了。假设您的应用中有一个与 UI 关联的 CoroutineScope,用于处理与用户的交互 。如果它的子协程抛出了一个异常,就会导致 UI 作用域 (UI scope) 被取消,并且由于被取消的作用域无法开启新的协程,所有的 UI 组件都会变得无法响应。

如果您不希望这种事情发生,可以尝试在创建协程时在 CoroutineScope 的 CoroutineContext 中使用 Job 的另一个扩展: SupervisorJob。

使用 SupervisorJob 来解决问题

使用 SupervisorJob 时,一个子协程的运行失败不会影响到其他子协程。SupervisorJob 不会取消它和它自己的子级,也不会传播异常并传递给它的父级,它会让子协程自己处理异常。

您可以使用这样的代码创建一个 CoroutineScope: val uiScope = CoroutineScope(SupervisorJob()),这样就会像下图中展示的那样,在协程运行失败时也不会传播取消操作。

△ SupervisorJob 不会取消它其他的子级

△ SupervisorJob 不会取消它其他的子级

如果异常没有被处理,而且 CoroutineContext 没有一个 CoroutineExceptionHandler (稍后讲到) 时,异常会到达默认线程的 ExceptionHandler。在 JVM 中,异常会被打印在控制台;而在 Android 中,无论异常在那个 Dispatcher 中发生,都会导致您的应用崩溃。

💥 未被捕获的异常一定会被抛出,无论您使用的是哪种 Job

使用 coroutineScope 和 supervisorScope 也有相同的效果。它们会创建一个子作用域 (使用一个 Job 或 SupervisorJob 作为父级),可以帮助您根据自己的逻辑组织协程 (例如: 您想要进行一组平行计算,并且希望它们之间互相影响或者相安无事的时候)。

注意 : SupervisorJob 只有作为 supervisorScope 或 CoroutineScope(SupervisorJob()) 的一部分时,才会按照上面的描述工作。

**使用 Job 还是 SupervisorJob?**🤔

那么应该在什么时候去使用 Job 或 SupervisorJob 呢?如果您想要在出现错误时不会退出父级和其他平级的协程,那就使用 SupervisorJob 或 supervisorScope。

示例如下:

1
2
3
4
5
6
7
8
9
10
Kotlin复制代码// Scope 控制我的应用中某一层级的协程
val scope = CoroutineScope(SupervisorJob())

scope.launch {
// Child 1
}

scope.launch {
// Child 2
}

在这个示例中如果 Child 1 失败了,无论是 scope 还是 Child 2 都会被取消。

另一个示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Kotlin复制代码// Scope 控制我的应用中某一层级的协程
val scope = CoroutineScope(Job())

scope.launch {
supervisorScope {
launch {
// Child 1
}
launch {
// Child 2
}
}
}

在这个示例中,由于 supervisorScope 使用 SupervisorJob 创建了一个子作用域,如果 Child 1 失败了,Child 2 不会被取消。而如果您在扩展中使用 coroutineScope 代替 supervisorScope ,错误就会被传播,而作用域最终也会被取消。

小测验: 谁是我的父级?🎯

1
2
3
4
5
6
7
8
9
10
11
12
Kotlin复制代码给您下面一段代码,您能指出 Child 1 是用哪种 Job 作为父级的吗?
val scope = CoroutineScope(Job())

scope.launch(SupervisorJob()) {
// new coroutine -> can suspend
launch {
// Child 1
}
launch {
// Child 2
}
}

Child 1 的父级 Job 就只是 Job 类型!希望您答对了。虽然乍一看确实会让人以为是 SupervisorJob,但是因为新的协程被创建时,会生成新的 Job 实例替代 SupervisorJob,所以这里并不是。本例中的 SupervisorJob 是协程的父级通过 scope.launch 创建的,所以真相是,SupervisorJob 在这段代码中完全没用!

△ Child 1 和 Child 2 的父级是 Job 类型,不是 SupervisorJob

这样一来,无论 Child 1 或 Child 2 运行失败,错误都会到达作用域,所有该作用域开启的协程都会被取消。

记住,只有使用 supervisorScope 或 CoroutineScope(SupervisorJob()) 创建 SupervisorJob 时,它才会像前文描述的一样工作。将 SupervisorJob 作为参数传入一个协程的 Builder 不能带来您想要的效果。

工作原理

如果您对 Job 的底层实现感到疑惑,可以查看 JobSupport.kt 文件中对 childCancelled 和 notifyCancelling 方法的扩展。

在 SupervisorJob 的扩展中,childCancelled 方法只是返回 false,意味着它不会传播取消操作,也不会对理异常做任何处理。

处理异常👩‍🚒

协程使用一般的 Kotlin 语法处理异常: try/catch 或内建的工具方法,比如 runCatching (其内部还是使用了 try/catch)

前面讲到,所有未捕获的异常一定会被抛出。但是,不同的协程 Builder 对异常有不同的处理方式。

Launch

使用 launch 时,异常会在它发生的第一时间被抛出,这样您就可以将抛出异常的代码包裹到 try/catch 中,就像下面的示例这样:

1
2
3
4
5
6
7
Kotlin复制代码scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// 处理异常
}
}

使用 launch 时,异常会在它发生的第一时间被抛出

Async

当 async 被用作根协程 (CoroutineScope 实例或 supervisorScope 的直接子协程) 时不会自动抛出异常,而是在您调用 .await() 时才会抛出异常。

当 async 作为根协程时,为了捕获其中抛出的异常,您可以用 try/catch 包裹调用 .await() 的代码:

1
2
3
4
5
6
7
8
9
10
11
Kotlin复制代码supervisorScope {
val deferred = async {
codeThatCanThrowExceptions()
}

try {
deferred.await()
} catch(e: Exception) {
// 处理 async 中抛出的异常
}
}

注意,在本例中,async 永远都不会抛出异常。这就是为什么没有必要将它也包裹进 try/catch 中,await 将会抛出 async 协程中产生的所有异常。

当 async 被用作根协程时,异常将会在您调用 .await 方法时被抛出

另一个需要注意的地方是,这里使用了 supervisorScope 来调用 async 和 await。正如我们之前提到的,SupervisorJob 会让协程自己处理异常;而相对的,Job 则会在层级间自动传播异常,这样一来 catch 部分的代码块就不会被调用:

1
2
3
4
5
6
7
8
9
10
11
Kotlin复制代码coroutineScope {
try {
val deferred = async {
codeThatCanThrowExceptions()
}
deferred.await()
} catch(e: Exception) {
// async 中抛出的异常将不会在这里被捕获
// 但是异常会被传播和传递到 scope
}
}

更进一步的,其他协程所创建的协程中产生的异常总是会被传播,无论协程的 Builder 是什么。例如:

1
2
3
4
5
6
Kotlin复制代码val scope = CoroutineScope(Job())
scope.launch {
async {
// 如果 async 抛出异常,launch 就会立即抛出异常,而不会调用 .await()
}
}

本例中,由于 scope 的直接子协程是 launch,如果 async 中产生了一个异常,这个异常将就会被立即抛出。原因是 async (包含一个 Job 在它的 CoroutineContext 中) 会自动传播异常到它的父级 (launch),这会让异常被立即抛出。

⚠️ 在 coroutineScope builder 或在其他协程创建的协程中抛出的异常不会被 try/catch 捕获!

在前面 SupervisorJob 那节中,我们提到了 CoroutineExceptionHandler 的存在。现在让我们来深入探索一下。

CoroutineExceptionHandler

CoroutineExceptionHandler 是 CoroutineContext 的一个可选元素,它让您可以处理未捕获的异常。

下面是如何声明一个 CoroutineExceptionHandler 的例子。无论哪里有异常被捕获,您都可以通过 handler 获得异常所在的 CoroutineContext 的有关信息以及异常本身:

1
2
3
Kotlin复制代码val handler = CoroutineExceptionHandler {
context, exception -> println("Caught $exception")
}

以下的条件被满足时,异常就会被捕获:

  • 时机 ⏰: 异常是被自动抛出异常的协程所抛出的 (使用 launch,而不是 async 时);
  • 位置 🌍: 在 CoroutineScope 的 CoroutineContext 中或在一个根协程 (CoroutineScope 或者 supervisorScope 的直接子协程) 中。

我们来看一些使用 CoroutineExceptionHandler 的例子。在下面的代码中,异常会被 handler 捕获:

1
2
3
4
5
6
Kotlin复制代码val scope = CoroutineScope(Job())
scope.launch(handler) {
launch {
throw Exception("Failed coroutine")
}
}

在另外一个例子中,handler 被安装给了一个内部协程,那么它将不会捕获异常:

1
2
3
4
5
6
Kotlin复制代码val scope = CoroutineScope(Job())
scope.launch {
launch(handler) {
throw Exception("Failed coroutine")
}
}

异常不会被捕获的原因是因为 handler 没有被安装给正确的 CoroutineContext。内部协程会在异常出现时传播异常并传递给它的父级,由于父级并不知道 handler 的存在,异常就没有被抛出。

优雅地处理程序中的异常是提供良好用户体验的关键,在事情不如预期般发展时尤其如此。

想要避免取消操作在异常发生时被传播,记得使用 SupervisorJob;反之则使用 Job。

没有被捕获的异常会被传播,捕获它们以保证良好的用户体验!

接下来的时间里,我们将继续更新系列文章,感兴趣的读者请继续关注我们的更新。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

VS code——便捷开发Go使用示例 VS code 便捷

发表于 2020-10-27

VS code 便捷开发Go使用示例

主要记录:

  • Go 插件使用
  • 常用开发使用及快捷键操作

Go 插件使用操作方式:

  • 命令式的操作:
1. 选中需要操作的内容
2. 按`ctrl+shift+p`,输入:`go:相关命令`
3. 回车,然后输入对应命令需要的参数。没有参数的就回车生效
  • 手动鼠标点
1. 选中内容
2. 右键弹出相关操作,选择go相关命令,如`Go:Generate Unit Tests For Function`
3. 点击命令,然后输入对应命令需要的参数。没有参数的就生效

自动生成测试用例

命令:Go:Generate Unit Tests For Function

示例

测试代码:

1
2
3
复制代码func Add(a, b int) int {
return a + b
}

操作命令后,会在函数所在文件的同级目录下生成测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码
func TestAdd(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Add() = %v, want %v", got, tt.want)
}
})
}
}

TODO: Add test cases.需要你填写自己的测试用例

不过要值的注意的是,在测试函数中,使用t.Log()函数是不会在终端输出内容的,需要加一个-v参数:

  1. 在扩展商店里搜已经安装了的go插件,点击右下角的 设置图标
  2. 选中扩展设置
  3. 找到Go:Build Flags,添加 -v项,如下:

在这里插入图片描述

在这里插入图片描述

这样在执行TestXXX函数的时候,就能在终端输出log的内容了

自动生成结构体实例化

命令:Go:fill Struct

示例

在上面生成的单元测试中,在需要添加的测试case的地方来自动生成结构体实例化

  1. 先敲一个{},任意结构体的实例化也是这样,如:u := &User{},光标在{}中,然后执行命令操作即可。
  2. 然后命令操作即可

生成的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码tests := []struct {
name string
args args
want int
}{
{
name: "",
args: args{
a: 0,
b: 0,
},
want: 0,
}
}

不过要注意一点:

由于tests的类型是一个结构体切片[]struct,所以在生成的代码后面需要手动补一个逗号,,按保存就自动格式化代码了

这个经常用,我设置了快捷键 alt + f

自动实现接口

命令:Go:Generate Interface Stubs

该命令需要参数,输入三个内容:

  • 方法接受者形参名eg:s
  • 方法接受者名称,即实现类的名称,eg:*Student,你可以指定是值类型还是引用类型
  • 要实现的接口的名称,需要加上包名eg:code.Speaker

完整内容:s *Student code.Speaker

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码package code

type Speaker interface {
// Speak speak action
Speak()
}

type Student struct {
}

// Speak speak action
func (s *Student) Speak() {
panic("not implemented") // TODO: Implement
}

可以看到,连注释也顺便帮忙生成了。

自动增加/删除tag

增加命令:Go:Add Tags To Struct Fileds

删除命令:Go:Remove Tags From Struct Fileds

选中你要生成tag的字段,执行命令(增加和删除都是同理,需要选中字段,只会执行已选中的字段)

默认是只生成 json tag,可以自定义。在setting.json,加入go.addTags设置即可

设置示例:

1
2
3
4
5
6
7
8
9
10
复制代码 // 结构体tag 设置
"go.addTags": {
// 可配置多个tag json,orm
"tags": "json,form",
// options 可以填入json=omitempty
"options": "",
"promptForTags": false,
// snakecase:下划线分隔, camelcase:驼峰命名
"transform": "camelcase"
},

这个我也常用,设置快捷键alt+a,删除不常用,未设置。

快速导入包

命令:Go:Add import

一般情况下,写代码智能提示或者保存的时候就会自动引入包。但是有时候vscode在引入同名的包时,会引入错误的包,这种情况主要是本地pkg->mod目录下有了重名的库,vscode无法知道是哪个。

这时候需要我们手动引入,执行命令,然后输入包名,从展示列表中选中你想要的回车即可,支持模糊搜索

这个常用,我也设置了快捷键:alt+i

查找接口的具体实现

  1. 鼠标移到接口定义处
  2. 快捷ctrl+f12
  3. 或者右键,选中:Go to implementations

该操作会出现两种情况:

  • 只有一个实现,那么直接跳转到实现的结构体上
  • 如果有多个实现,那么会弹出一个界面,左侧框展示实现的代码,右侧框展示实现的列表,二者是联动的。然后选择你想看的实现,双击就会跳转

重构

重命名

  1. 选中你想要重构的字段、方法、接口名等等
  2. 按F2,然后输入你想要的命名

这个操作会将凡是引用该字段、方法、接口的地方全部重命名,并且支持跨文件

字段提取

命令:Go:Extract to variable

字段提取主要用于判断条件复杂的场景,如果该条件判断在多个地方使用,最好是抽离出来,提取成一个变量

操作:

  • 选中需要提取的内容
  • 执行命令
  • 输入要生成的变量名

等待1s,生成代码

函数提取

命令:Go:Extract to function

函数提取主要用于逻辑可复用的场景。把同一段逻辑抽离成一个函数

操作:

  • 选中需要提取的内容
  • 执行命令
  • 输入要生成的函数名

等待1s,生成代码

示例

原代码:

1
2
3
4
5
6
7
8
复制代码func ExtractFuncTest(a, b, c int) {
if a > 0 {
}
if b > 0 {
}
if c > 0 {
}
}

选中里面的逻辑,函数提取后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码func ExtractFuncTest(a, b, c int) {
flag(a, b, c)
}

func flag(a int, b int, c int) {
if a > 0 {
}
if b > 0 {
}
if c > 0 {
}
}

第三方库加入到当前的workspace

一般vscode左侧只会展示当前项目的目录,引入的第三方库并不会展示出来。并不像Goland一样,有个External Libary目录

这个功能不太好用,目前我只能导入一些go的库,如fmt,errors库,第三方库并不行。待解决

设置Go相关命令,右键就能显示常用的

在settings.json中,输入go.editorContextMenuCommands,事实上,输入前几个字母就会智能提示,回车后就会生成配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码"go.editorContextMenuCommands": {
"toggleTestFile": true,
"addTags": false,
"removeTags": true,
"testAtCursor": true,
"testFile": true,
"testPackage": false,
"generateTestForFunction": true,
"generateTestForFile": false,
"generateTestForPackage": false,
"addImport": false,
"testCoverage": true,
"playground": true,
"debugTestAtCursor": true
},

true表示开启,设置完后,重启VS code即可

参考:

B站——【教程】vscode-go插件的这些用法,你真的知道么?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java安全编码指南之 文件IO操作 简介 创建文件的时候指

发表于 2020-10-27

简介

对于文件的IO操作应该是我们经常会使用到的,因为文件的复杂性,我们在使用File操作的时候也有很多需要注意的地方,下面我一起来看看吧。

创建文件的时候指定合适的权限

不管是在windows还是linux,文件都有权限控制的概念,我们可以设置文件的owner,还有文件的permission,如果文件权限没有控制好的话,恶意用户就有可能对我们的文件进行恶意操作。

所以我们在文件创建的时候就需要考虑到权限的问题。

很遗憾的是,java并不是以文件操作见长的,所以在JDK1.6之前,java的IO操作是非常弱的,基本的文件操作类,比如FileOutputStream和FileWriter并没有权限的选项。

1
java复制代码Writer out = new FileWriter("file");

那么怎么处理呢?

在JDK1.6之前,我们需要借助于一些本地方法来实现权限的修改功能。

在JDK1.6之后,java引入了NIO,可以通过NIO的一些特性来控制文件的权限功能。

我们看一下Files工具类的createFile方法:

1
2
3
4
5
6
java复制代码    public static Path createFile(Path path, FileAttribute<?>... attrs)
throws IOException
{
newByteChannel(path, DEFAULT_CREATE_OPTIONS, attrs).close();
return path;
}

其中FileAttribute就是文件的属性,我们看一下怎么指定文件的权限:

1
2
3
4
5
6
7
8
java复制代码    public void createFileWithPermission() throws IOException {
Set<PosixFilePermission> perms =
PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr =
PosixFilePermissions.asFileAttribute(perms);
Path file = new File("/tmp/www.flydean.com").toPath();
Files.createFile(file,attr);
}

注意检查文件操作的返回值

java中很多文件操作是有返回值的,比如file.delete(),我们需要根据返回值来判断文件操作是否完成,所以不要忽略了返回值。

删除使用过后的临时文件

如果我们使用到不需要永久存储的文件时,就可以很方便的使用File的createTempFile来创建临时文件。临时文件的名字是随机生成的,我们希望在临时文件使用完毕之后将其删除。

怎么删除呢?File提供了一个deleteOnExit方法,这个方法会在JVM退出的时候将文件删除。

注意,这里的JVM一定要是正常退出的,如果是非正常退出,文件不会被删除。

我们看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码    public void wrongDelete() throws IOException {
File f = File.createTempFile("tmpfile",".tmp");
FileOutputStream fop = null;
try {
fop = new FileOutputStream(f);
String str = "Data";
fop.write(str.getBytes());
fop.flush();
} finally {
// 因为Stream没有被关闭,所以文件在windows平台上面不会被删除
f.deleteOnExit(); // 在JVM退出的时候删除临时文件

if (fop != null) {
try {
fop.close();
} catch (IOException x) {
// Handle error
}
}
}
}

上面的例子中,我们创建了一个临时文件,并且在finally中调用了deleteOnExit方法,但是因为在调用该方法的时候,Stream并没有关闭,所以在windows平台上会出现文件没有被删除的情况。

怎么解决呢?

NIO提供了一个DELETE_ON_CLOSE选项,可以保证文件在关闭之后就被删除:

1
2
3
4
5
6
7
8
9
java复制代码    public void correctDelete() throws IOException {
Path tempFile = null;
tempFile = Files.createTempFile("tmpfile", ".tmp");
try (BufferedWriter writer =
Files.newBufferedWriter(tempFile, Charset.forName("UTF8"),
StandardOpenOption.DELETE_ON_CLOSE)) {
// Write to file
}
}

上面的例子中,我们在writer的创建过程中加入了StandardOpenOption.DELETE_ON_CLOSE,那么文件将会在writer关闭之后被删除。

释放不再被使用的资源

如果资源不再被使用了,我们需要记得关闭他们,否则就会造成资源的泄露。

但是很多时候我们可能会忘记关闭,那么该怎么办呢?JDK7中引入了try-with-resources机制,只要把实现了Closeable接口的资源放在try语句中就会自动被关闭,很方便。

注意Buffer的安全性

NIO中提供了很多非常有用的Buffer类,比如IntBuffer, CharBuffer 和 ByteBuffer等,这些Buffer实际上是对底层的数组的封装,虽然创建了新的Buffer对象,但是这个Buffer是和底层的数组相关联的,所以不要轻易的将Buffer暴露出去,否则可能会修改底层的数组。

1
2
3
4
java复制代码    public CharBuffer getBuffer(){
char[] dataArray = new char[10];
return CharBuffer.wrap(dataArray);
}

上面的例子暴露了CharBuffer,实际上也暴露了底层的char数组。

有两种方式对其进行改进:

1
2
3
4
java复制代码    public CharBuffer getBuffer1(){
char[] dataArray = new char[10];
return CharBuffer.wrap(dataArray).asReadOnlyBuffer();
}

第一种方式就是将CharBuffer转换成为只读的。

第二种方式就是创建一个新的Buffer,切断Buffer和数组的联系:

1
2
3
4
5
6
java复制代码    public CharBuffer getBuffer2(){
char[] dataArray = new char[10];
CharBuffer cb = CharBuffer.allocate(dataArray.length);
cb.put(dataArray);
return cb;
}

注意 Process 的标准输入输出

java中可以通过Runtime.exec()来执行native的命令,而Runtime.exec()是有返回值的,它的返回值是一个Process对象,用来控制和获取native程序的执行信息。

默认情况下,创建出来的Process是没有自己的I/O stream的,这就意味着Process使用的是父process的I/O(stdin, stdout, stderr),Process提供了下面的三种方法来获取I/O:

1
2
3
java复制代码getOutputStream()
getInputStream()
getErrorStream()

如果是使用parent process的IO,那么在有些系统上面,这些buffer空间比较小,如果出现大量输入输出操作的话,就有可能被阻塞,甚至是死锁。

怎么办呢?我们要做的就是将Process产生的IO进行处理,以防止Buffer的阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码public class StreamProcesser implements Runnable{
private final InputStream is;
private final PrintStream os;

StreamProcesser(InputStream is, PrintStream os){
this.is=is;
this.os=os;
}

@Override
public void run() {
try {
int c;
while ((c = is.read()) != -1)
os.print((char) c);
} catch (IOException x) {
// Handle error
}
}

public static void main(String[] args) throws IOException, InterruptedException {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("vscode");

Thread errorGobbler
= new Thread(new StreamProcesser(proc.getErrorStream(), System.err));

Thread outputGobbler
= new Thread(new StreamProcesser(proc.getInputStream(), System.out));

errorGobbler.start();
outputGobbler.start();

int exitVal = proc.waitFor();
errorGobbler.join();
outputGobbler.join();
}
}

上面的例子中,我们创建了一个StreamProcesser来处理Process的Error和Input。

InputStream.read() 和 Reader.read()

InputStream和Reader都有一个read()方法,这两个方法的不同之处就是InputStream read的是Byte,而Reader read的是char。

虽然Byte的范围是-128到127,但是InputStream.read()会将读取到的Byte转换成0-255(0x00-0xff)范围的int。

Char的范围是0x0000-0xffff,Reader.read()将会返回同样范围的int值:0x0000-0xffff。

如果返回值是-1,表示的是Stream结束了。这里-1的int表示是:0xffffffff。

我们在使用的过程中,需要对读取的返回值进行判断,以用来区分Stream的边界。

我们考虑这样的一个问题:

1
2
3
4
java复制代码FileInputStream in;
byte data;
while ((data = (byte) in.read()) != -1) {
}

上面我们将InputStream的read结果先进行byte的转换,然后再判断是否等于-1。会有什么问题呢?

如果Byte本身的值是0xff,本身是一个-1,但是InputStream在读取之后,将其转换成为0-255范围的int,那么转换之后的int值是:0x000000FF, 再次进行byte转换,将会截取最后的Oxff, Oxff == -1,最终导致错误的判断Stream结束。

所以我们需要先做返回值的判断,然后再进行转换:

1
2
3
4
5
6
7
java复制代码FileInputStream in;
int inbuff;
byte data;
while ((inbuff = in.read()) != -1) {
data = (byte) inbuff;
// ...
}

拓展阅读:

这段代码的输出结果是多少呢? (int)(char)(byte)-1

首先-1转换成为byte:-1是0xffffffff,转换成为byte直接截取最后几位,得到0xff,也就是-1.

然后byte转换成为char:0xff byte是有符号的,转换成为2个字节的char需要进行符号位扩展,变成0xffff,但是char是无符号的,对应的十进制是65535。

最后char转换成为int,因为char是无符号的,所以扩展成为0x0000ffff,对应的十进制数是65535.

同样的下面的例子中,如果提前使用char对int进行转换,因为char的范围是无符号的,所以永远不可能等于-1.

1
2
3
4
5
java复制代码FileReader in;
char data;
while ((data = (char) in.read()) != -1) {
// ...
}

write() 方法不要超出范围

在OutputStream中有一个很奇怪的方法,就是write,我们看下write方法的定义:

1
java复制代码    public abstract void write(int b) throws IOException;

write接收一个int参数,但是实际上写入的是一个byte。

因为int和byte的范围不一样,所以传入的int将会被截取最后的8位来转换成一个byte。

所以我们在使用的时候一定要判断写入的范围:

1
2
3
4
5
6
7
8
java复制代码    public void writeInt(int value){
int intValue = Integer.valueOf(value);
if (intValue < 0 || intValue > 255) {
throw new ArithmeticException("Value超出范围");
}
System.out.write(value);
System.out.flush();
}

或者有些Stream操作是可以直接writeInt的,我们可以直接调用。

注意带数组的read的使用

InputStream有两种带数组的read方法:

1
java复制代码public int read(byte b[]) throws IOException

和

1
java复制代码public int read(byte b[], int off, int len) throws IOException

如果我们使用了这两种方法,那么一定要注意读取到的byte数组是否被填满,考虑下面的一个例子:

1
2
3
4
5
6
7
java复制代码    public String wrongRead(InputStream in) throws IOException {
byte[] data = new byte[1024];
if (in.read(data) == -1) {
throw new EOFException();
}
return new String(data, "UTF-8");
}

如果InputStream的数据并没有1024,或者说因为网络的原因并没有将1024填充满,那么我们将会得到一个没有填充满的数组,那么我们使用起来其实是有问题的。

怎么正确的使用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    public String readArray(InputStream in) throws IOException {
int offset = 0;
int bytesRead = 0;
byte[] data = new byte[1024];
while ((bytesRead = in.read(data, offset, data.length - offset))
!= -1) {
offset += bytesRead;
if (offset >= data.length) {
break;
}
}
String str = new String(data, 0, offset, "UTF-8");
return str;
}

我们需要记录实际读取的byte数目,通过记载偏移量,我们得到了最终实际读取的结果。

或者我们可以使用DataInputStream的readFully方法,保证读取完整的byte数组。

little-endian和big-endian的问题

java中的数据默认是以big-endian的方式来存储的,DataInputStream中的readByte(), readShort(), readInt(), readLong(), readFloat(), 和 readDouble()默认也是以big-endian来读取数据的,如果在和其他的以little-endian进行交互的过程中,就可能出现问题。

我们需要的是将little-endian转换成为big-endian。

怎么转换呢?

比如,我们想要读取一个int,可以首先使用read方法读取4个字节,然后再对读取的4个字节做little-endian到big-endian的转换。

1
2
3
4
5
6
7
8
9
10
11
java复制代码    public void method1(InputStream inputStream) throws IOException {
try(DataInputStream dis = new DataInputStream(inputStream)) {
byte[] buffer = new byte[4];
int bytesRead = dis.read(buffer); // Bytes are read into buffer
if (bytesRead != 4) {
throw new IOException("Unexpected End of Stream");
}
int serialNumber =
ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
}
}

上面的例子中,我们使用了ByteBuffer提供的wrap和order方法来对Byte数组进行转换。

当然我们也可以自己手动进行转换。

还有一个最简单的方法,就是调用JDK1.5之后的reverseBytes() 直接进行小端到大端的转换。

1
2
3
java复制代码    public  int reverse(int i) {
return Integer.reverseBytes(i);
}

本文的代码:

learn-java-base-9-to-20/tree/master/security

本文已收录于 www.flydean.com/java-securi…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android 使用 ContentProvider 无

发表于 2020-10-27

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 在 Android 中,使用三方库或二方库时,经常需要使用 Context 进行初始化,一般的做法是调用仓库的初始化方法,并传入合适的 Context 对象;
  • 在这篇文章里,我将介绍基于 ContentProvider 启动机制实现的 无侵入获取 Context 的方法。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

相关文章

  • 《Android | 使用 ContentProvider 无侵入获取 Context》
  • 《Android | 一个进程有多少个 Context 对象(答对的不多)》
  • 《Android | 食之无味!App Startup 可能比你想象中要简单》

目录


  1. 获取 Context 的常规方法

首先,我们回顾一下 Context 以及它的子类,在之前的这篇文章里,我们曾经讨论过:《Android | 一个进程有多少个 Context 对象(答对的不多)》。简单来说:Context 使用了装饰模式,除了 ContextImpl 外,其他 Context 都是 ContextWrapper 的子类。

我们熟悉的 Activity & Service & Application,都是 ContextWrapper 的子类。

1.1 获取 Application 对象

Application 对象的生命周期是最长的,经常被用来初始化第三方库,可以使用一个静态方法将对象暴露出去,也可以在 Application#onCreate()中初始化第三方库:

MainApplication.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码class MainApplication : Application() {

companion object {
lateinit var application: Application
get
}

override fun onCreate() {
super.onCreate()
application = this

初始化第三方库...
}
}

1.2 获取 Activity & Service 对象

同样地,Activity & Service 也是 Context 的实现类,那么我们就可以在程序运行过程中,可以按需初始化第三方库。例如使用 Glide 时,并不需要一开始就调用Glide#with(Context),只需要在显示图片的时候调用即可;

1.3 小结

  • 优点
    最常用的方式,实现简单,没有性能 / 稳定性风险;可以按需初始化第三方库 & 懒加载;
  • 缺点
    需要获取 ApplicationContext / Context (依赖方与库代码强耦合),不利于组件化。

下面,我将介绍两种无侵入获取 Context 的方法,将涉及到 Android 进程的启动流程,若还不了解,请务必阅读文章:《Android | 带你理解 Application 的创建过程》


  1. 反射 ActivityThread 获得 ApplicationContext(不推荐)

这一节介绍一种通过 ActivityThread.java 获得 Application 的方法,具体如下:

2.1 源码分析

我们都知道,在启动四大组件(Activity、Service、ContentProvider, BroadcastReceiver)时,如果对应的进程未启动,就需要先创建进程,相应地也会创建一个 Application对象,即:

  • 在system_server进程,通过AMS#getProcessRecordLocked(...)获取进程信息 (ProcessRecord);
  • 若不存在,则调用AMS#startProcessLocked(...)创建进程;
  • 在 Zygote 孵化目标进程之后,在目标进程反射执行ActivityThread#main(),并最终在ActivityThread#handleBindApplication(...)中创建 Application 对象。

ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码Application mInitialApplication;

public Application getApplication() {
return mInitialApplication;
}

private void handleBindApplication(AppBindData data) {
// ...
Application app;
// data.info 为 LoadedApk.java
app = data.info.makeApplication(data.restrictedBackupMode, null);
// ...
mInitialApplication = app;
// ...
}

可以看到,创建 Application 对象之后会保存在mInitialApplication属性中,那么如果我们可以访问到这个属性,是不是就可以获得 Application 对象了呢?

首先,我们需要获得 ActivityThread 对象,那么我们先在源码中寻找创建 ActivityThread 对象的地方:

ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码private static volatile ActivityThread sCurrentActivityThread;

public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}

// (简化)
public static void main(String[] args) {
Looper.prepareMainLooper();

// 创建 ActivityThread 对象
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

Looper.loop();
}

private void attach(boolean system, long startSeq) {
sCurrentActivityThread = this;
}

可以看到,ActivityThread 对象存储在静态变量sCurrentActivityThread中,那么我们就可以写反射代码了。

2.2 使用步骤

Context.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码// 新建文件 
private var application: Context? = null

fun context(): Context {
if (null == application) {
try {
val activityThread: Any
val clazz = Class.forName("android.app.ActivityThread")
val currentActivityThread = clazz.getMethod("currentActivityThread").apply {
isAccessible = true
}
val getApplication = clazz.getMethod("getApplication").apply {
isAccessible = true
}
activityThread = currentActivityThread.invoke(null)
application= getApplication.invoke(activityThread) as Context
} catch (e: Throwable) {
// 存在未适配的风险
}
}
return application!!
}

运行测试一下,context()的返回结果:

1
css复制代码android.app.Application@c12661f

2.3 小结

  • 优点:
  • 依赖方不需要传递 Context 对象给库进行初始化,减少了代码耦合,有利于组件化*;
  • 缺点:
  • 需要反射调用私有 API ,存在系统版本适配风险;使用反射有一定性能损耗*。

  1. 使用 ContentProvider 获取 ApplicationContext

这一节介绍一种通过ContentProvider.java获得 Application 的方法。ContentProvider 通常的用法是为当前进程 / 远程进程提供内容服务,它们会在应用启动的时候初始化,正因如此,我们可以利用 ContentProvider 来获得 Context 。

3.1 源码分析

ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ini复制代码private void handleBindApplication(AppBindData data) {
// ...
1、实例化 Application 对象
Application app;
app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;

2、初始化所有 ContentProvider
installContentProviders(app, data.providers);

3、调用 Application#onCreate()
mInstrumentation.callApplicationOnCreate(app);
...
}

-> 1、实例化 Application 对象
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
...
1.1 Context 基础对象
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);

1.2 实例化,内部会调用 Context#attachBaseContext(...)
app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);

1.3 Context 基础对象持有代理类
appContext.setOuterContext(app);
...
}

-> 2、初始化所有 ContentProvider
private void installContentProviders(Context context, List<ProviderInfo> providers) {
final ArrayList<ContentProviderHolder> results = new ArrayList<>();

for (ProviderInfo cpi : providers) {
// 依次初始化 ContentProvider
ContentProviderHolder cph = installProvider(context, null, cpi,
false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
if (cph != null) {
cph.noReleaseNeeded = true;
results.add(cph);
}
}
// ...
}

可以看到,installContentProviders()将安装属于当前进程的 ContentProvider,具体的调用时机在Context#attachBaseContext(...)与Application#onCreate()之间:

3.2 使用步骤

了解 ContentProvider 启动机制之后,我们就可以利用 ContentProvider 来拿到 ApplicationContext。具体步骤如下:

步骤一:实现 ContentProvider 子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码// ContextProvider.kt

internal class ContextProvider : ContentProvider(){

override fun onCreate(): Boolean {
init(context!!)
return true
}

// 其他方法直接 return
}
// Context.kt
private lateinit var application : Context

fun init(context : Context){
application= context
}

fun context() : Context{
return application
}
步骤二:在 AndroidManifest 中配置
1
2
3
4
5
6
7
8
ini复制代码// AndroidManifest.xml

<application>
<provider
android:name=".Contextprovider"
android:authorities="${applicationId}.contextprovider"
android:exported="false" />
</application>
步骤三:使用
1
less复制代码Toast.makeText(context(),"",Toast.LENGTH_SHORT).show()

3.3 小结

  • 优点:
  • 依赖方不需要传递 Context 对象给库进行初始化,减少了代码耦合,有利于组件化*
  • 缺点:
  • 在 App 启动时就初始化 ContentProvider,不是懒初始化*;如果应用的 ContentProvider 过多,会增加应用的启动时间;
  • 风险:
  • 应保证初始化非常轻量,否则会降低 App 的启动速度*

  1. 案例

下面举出一些基于 ContentProvider 机制实现无侵入地获取 Context 的例子:

  • LeakCanary 2.4

AppWatcherInstaller.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码internal sealed class AppWatcherInstaller : ContentProvider() {

internal class MainProcess : AppWatcherInstaller()

internal class LeakCanaryProcess : AppWatcherInstaller()

override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}

// 其他方法直接 return
}
  • AutoSize 1.1.2

InitProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码public class InitProvider extends ContentProvider {
@Override
public boolean onCreate() {
AutoSizeConfig.getInstance()
.setLog(true)
.init((Application) getContext().getApplicationContext())
.setUseDeviceSize(false);
return true;
}

// 其他方法直接 return
}
  • Picasso 2.7

PicassoProvider.java

1
2
3
4
5
6
7
8
9
10
11
scala复制代码public final class PicassoProvider extends ContentProvider {

@SuppressLint("StaticFieldLeak") static Context context;

@Override public boolean onCreate() {
context = getContext();
return true;
}

// 其他方法直接 return
}

推荐阅读

  • 密码学 | Base64是加密算法吗?
  • 算法面试题 | 回溯算法解题框架
  • 算法面试题 | 链表问题总结
  • 计算机网络 | 图解 DNS & HTTPDNS 原理
  • Android | 说说从 android:text 到 TextView 的过程
  • Android | 面试必问的 Handler,你确定不看看?
  • Android | 带你探究 LayoutInflater 布局解析原理
  • Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…771772773…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%