太现实了!年薪50万的一个面试题,看着不难,却刷掉了99%的人!

3级缓存对应的代码:

/** 第一级缓存:单例bean的缓存 */

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 第二级缓存:早期暴露的bean的缓存 */

private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

/** 第三级缓存:单例bean工厂的缓存 */

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

下面来看spring中具体的过程,我们一起来分析源码

开始的时候,获取serviceA,会调用下面代码

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

protected  T doGetBean(final String name, @Nullable final Class requiredType,

@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

//1.查看缓存中是否已经有这个bean了

Object sharedInstance = getSingleton(beanName); //@1

if (sharedInstance != null && args == null) {

bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);

}else {

//若缓存中不存在,准备创建这个bean

if (mbd.isSingleton()) {

//2.下面进入单例bean的创建过程

sharedInstance = getSingleton(beanName, () -> {

try {

return createBean(beanName, mbd, args);

}

catch (BeansException ex) {

throw ex;

}

});

bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);

}

}

return (T) bean;

}

@1:查看缓存中是否已经有这个bean了,如下:

public Object getSingleton(String beanName) {

return getSingleton(beanName, true);

}

然后进入下面方法,会依次尝试从3级缓存中查找bean,注意下面的第2个参数,为ture的时候,才会从第3级中查找,否则只会查找1、2级缓存

//allowEarlyReference:是否允许从三级缓存singletonFactories中通过getObject拿到bean

protected Object getSingleton(String beanName, boolean allowEarlyReference) {

//1.先从一级缓存中找

Object singletonObject = this.singletonObjects.get(beanName);

if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {

synchronized (this.singletonObjects) {

//2.从二级缓存中找

singletonObject = this.earlySingletonObjects.get(beanName);

if (singletonObject == null && allowEarlyReference) {

//3.二级缓存中没找到 && allowEarlyReference为true的情况下,从三级缓存中找

ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

if (singletonFactory != null) {

//三级缓存返回的是一个工厂,通过工厂来获取创建bean

singletonObject = singletonFactory.getObject();

//将创建好的bean丢到二级缓存中

this.earlySingletonObjects.put(beanName, singletonObject);

//从三级缓存移除

this.singletonFactories.remove(beanName);

}

}

}

}

return singletonObject;

}

刚开始,3个缓存中肯定是找不到的,会返回null,接着会执行下面代码准备创建serviceA

if (mbd.isSingleton()) {

sharedInstance = getSingleton(beanName, () -> { //@1

try {

return createBean(beanName, mbd, args);

}

catch (BeansException ex) {

destroySingleton(beanName);

throw ex;

}

});

}

@1:进入getSingleton方法,而getSingleton方法代码比较多,为了方便大家理解,无关的代码我给剔除了,如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {

synchronized (this.singletonObjects) {

Object singletonObject = this.singletonObjects.get(beanName);

if (singletonObject == null) {

//单例bean创建之前调用,将其加入正在创建的列表中,上面有提到过,主要用来检测循环依赖用的

beforeSingletonCreation(beanName);

boolean newSingleton = false;

try {

//调用工厂创建bean

singletonObject = singletonFactory.getObject();//@1

newSingleton = true;

}

finally {

//单例bean创建之前调用,主要是将其从正在创建的列表中移除

afterSingletonCreation(beanName);

}

if (newSingleton) {

//将创建好的单例bean放入缓存中

addSingleton(beanName, singletonObject);//@2

}

}

return singletonObject;

}

}

上面@1和@2是关键代码,先来看一下@1,这个是一个ObjectFactory类型的,从外面传入的,如下

红框中的createBean最终会调用下面这个方法

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

其内部主要代码如下:

BeanWrapper instanceWrapper = null;

if (instanceWrapper == null) {

//通过反射调用构造器实例化serviceA

instanceWrapper = createBeanInstance(beanName, mbd, args);

}

//变量bean:表示刚刚同构造器创建好的bean示例

final Object bean = instanceWrapper.getWrappedInstance();

//判断是否需要暴露早期的bean,条件为(是否是单例bean && 当前容器允许循环依赖 && bean名称存在于正在创建的bean名称清单中)

boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&

isSingletonCurrentlyInCreation(beanName));

if (earlySingletonExposure) {

//若earlySingletonExposure为true,通过下面代码将早期的bean暴露出去

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));//@1

}

这里需要理解一下什么是早期bean?

刚刚实例化好的bean就是早期的bean,此时bean还未进行属性填充,初始化等操作

@1:通过addSingletonFactory用于将早期的bean暴露出去,主要是将其丢到第3级缓存中,代码如下:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {

Assert.notNull(singletonFactory, “Singleton factory must not be null”);

synchronized (this.singletonObjects) {

//第1级缓存中不存在bean

if (!this.singletonObjects.containsKey(beanName)) {

//将其丢到第3级缓存中

this.singletonFactories.put(beanName, singletonFactory);

//后面的2行代码不用关注

this.earlySingletonObjects.remove(beanName);

this.registeredSingletons.add(beanName);

}

}

}

上面的方法执行之后,serviceA就被丢到第3级的缓存中了。

后续的过程serviceA开始注入依赖的对象,发现需要注入serviceB,会从容器中获取serviceB,而serviceB的获取又会走上面同样的过程实例化serviceB,然后将serviceB提前暴露出去,然后serviceB开始注入依赖的对象,serviceB发现自己需要注入serviceA,此时去容器中找serviceA,找serviceA会先去缓存中找,会执行getSingleton("serviceA",true),此时会走下面代码:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {

//1.先从一级缓存中找

Object singletonObject = this.singletonObjects.get(beanName);

if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {

synchronized (this.singletonObjects) {

//2.从二级缓存中找

singletonObject = this.earlySingletonObjects.get(beanName);

if (singletonObject == null && allowEarlyReference) {

//3.二级缓存中没找到 && allowEarlyReference为true的情况下,从三级缓存中找

ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

if (singletonFactory != null) {

//三级缓存返回的是一个工厂,通过工厂来获取创建bean

singletonObject = singletonFactory.getObject();

//将创建好的bean丢到二级缓存中

this.earlySingletonObjects.put(beanName, singletonObject);

//从三级缓存移除

this.singletonFactories.remove(beanName);

}

}

}

}

return singletonObject;

}

上面的方法走完之后,serviceA会被放入二级缓存earlySingletonObjects中,会将serviceA返回,此时serviceB中的serviceA注入成功,serviceB继续完成创建,然后将自己返回给serviceA,此时serviceA通过set方法将serviceB注入。

serviceA创建完毕之后,会调用addSingleton方法将其加入到缓存中,这块代码如下:

protected void addSingleton(String beanName, Object singletonObject) {

synchronized (this.singletonObjects) {

//将bean放入第1级缓存中

this.singletonObjects.put(beanName, singletonObject);

//将其从第3级缓存中移除

this.singletonFactories.remove(beanName);

//将其从第2级缓存中移除

this.earlySingletonObjects.remove(beanName);

}

}

到此,serviceA和serviceB之间的循环依赖注入就完成了。

下面捋一捋整个过程:

1.从容器中获取serviceA

2.容器尝试从3个缓存中找serviceA,找不到

3.准备创建serviceA

4.调用serviceA的构造器创建serviceA,得到serviceA实例,此时serviceA还未填充属性,未进行其他任何初始化的操作

5.将早期的serviceA暴露出去:即将其丢到第3级缓存singletonFactories中

6.serviceA准备填充属性,发现需要注入serviceB,然后向容器获取serviceB

7.容器尝试从3个缓存中找serviceB,找不到

8.准备创建serviceB

9.调用serviceB的构造器创建serviceB,得到serviceB实例,此时serviceB还未填充属性,未进行其他任何初始化的操作

10.将早期的serviceB暴露出去:即将其丢到第3级缓存singletonFactories中

11.serviceB准备填充属性,发现需要注入serviceA,然后向容器获取serviceA

12.容器尝试从3个缓存中找serviceA,发现此时serviceA位于第3级缓存中,经过处理之后,serviceA会从第3级缓存中移除,然后会存到第2级缓存中,然后将其返回给serviceB,此时serviceA通过serviceB中的setServiceA方法被注入到serviceB中

13.serviceB继续执行后续的一些操作,最后完成创建工作,然后会调用addSingleton方法,将自己丢到第1级缓存中,并将自己从第2和第3级缓存中移除

14.serviceB将自己返回给serviceA

15.serviceA通过setServiceB方法将serviceB注入进去

16.serviceB继续执行后续的一些操作,最后完成创建工作,然后会调用addSingleton方法,将自己丢到第1级缓存中,并将自己从第2和第3级缓存中移除

循环依赖无法解决的情况


只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。

那就会有下面几种情况需要注意。

还是以2个bean相互依赖为例:serviceA和serviceB

情况1

条件

serviceA:多例

serviceB:多例

结果

此时不管是任何方式都是无法解决循环依赖的问题,最终都会报错,因为每次去获取依赖的bean都会重新创建。

情况2

条件

serviceA:单例

serviceB:多例

结果

若使用构造器的方式相互注入,是无法完成注入操作的,会报错。

若采用set方式注入,所有bean都还未创建的情况下,若去容器中获取serviceB,会报错,为什么?我们来看一下过程:

1.从容器中获取serviceB

2.serviceB由于是多例的,所以缓存中肯定是没有的

3.检查serviceB是在正在创建的bean名称列表中,没有

4.准备创建serviceB

5.将serviceB放入正在创建的bean名称列表中

6.实例化serviceB(由于serviceB是多例的,所以不会提前暴露,必须是单例的才会暴露)

7.准备填充serviceB属性,发现需要注入serviceA

8.从容器中查找serviceA

9.尝试从3级缓存中找serviceA,找不到

10.准备创建serviceA

11.将serviceA放入正在创建的bean名称列表中

12.实例化serviceA

13.由于serviceA是单例的,将早期serviceA暴露出去,丢到第3级缓存中

14.准备填充serviceA的属性,发现需要注入serviceB

15.从容器中获取serviceB

16.先从缓存中找serviceB,找不到

17.检查serviceB是在正在创建的bean名称列表中,发现已经存在了,抛出循环依赖的异常

这个有演示的源码,位置:

com.javacode2018.lesson003.demo2.CircleDependentTest#test2

在这里给大家留个问题,如果此处不是去获取serviceB,而是先去获取serviceA呢,会不会报错?欢迎各位留言。

探讨:为什么需要用3级缓存


问题

如果只使用2级缓存,直接将刚实例化好的bean暴露给二级缓存出是否可以否?

先下个结论吧:不行。

原因

这样做是可以解决:早期暴露给其他依赖者的bean和最终暴露的bean不一致的问题。

若将刚刚实例化好的bean直接丢到二级缓存中暴露出去,如果后期这个bean对象被更改了,比如可能在上面加了一些拦截器,将其包装为一个代理了,那么暴露出去的bean和最终的这个bean就不一样的,将自己暴露出去的时候是一个原始对象,而自己最终却是一个代理对象,最终会导致被暴露出去的和最终的bean不是同一个bean的,将产生意向不到的效果,而三级缓存就可以发现这个问题,会报错。

下面我们通过代码来演示一下效果。

案例

下面来2个bean,相互依赖,通过set方法相互注入,并且其内部都有一个m1方法,用来输出一行日志。

Service1

package com.javacode2018.lesson003.demo2.test3;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

@Component

public class Service1 {

public void m1() {

System.out.println(“Service1 m1”);

}

private Service2 service2;

@Autowired

public void setService2(Service2 service2) {

this.service2 = service2;

}

}

Service2

package com.javacode2018.lesson003.demo2.test3;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

@Component

public class Service2 {

public void m1() {

System.out.println(“Service2 m1”);

this.service1.m1();//@1

}

private Service1 service1;

@Autowired

public void setService1(Service1 service1) {

this.service1 = service1;

}

public Service1 getService1() {

return service1;

}

}

注意上面的@1,service2的m1方法中会调用service1的m1方法。

需求

在service1上面加个拦截器,要求在调用service1的任何方法之前需要先输出一行日志

你好,service1

实现

新增一个Bean后置处理器来对service1对应的bean进行处理,将其封装为一个代理暴露出去。

package com.javacode2018.lesson003.demo2.test3;

import org.springframework.aop.MethodBeforeAdvice;

import org.springframework.aop.framework.ProxyFactory;

import org.springframework.beans.BeansException;

import org.springframework.beans.factory.config.BeanPostProcessor;

import org.springframework.lang.Nullable;

import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component

public class MethodBeforeInterceptor implements BeanPostProcessor {

@Nullable

@Override

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

if (“service1”.equals(beanName)) {

//代理创建工厂,需传入被代理的目标对象

ProxyFactory proxyFactory = new ProxyFactory(bean);

//添加一个方法前置通知,会在方法执行之前调用通知中的before方法

proxyFactory.addAdvice(new MethodBeforeAdvice() {

@Override

public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {

System.out.println(“你好,service1”);

}

});

//返回代理对象

return proxyFactory.getProxy();

}

return bean;

}

}

上面的postProcessAfterInitialization方法内部会在service1初始化之后调用,内部会对service1这个bean进行处理,返回一个代理对象,通过代理来访问service1的方法,访问service1中的任何方法之前,会先输出:你好,service1

代码中使用了ProxyFactory,这块不熟悉的没关系,后面介绍aop的时候会细说。

来个配置类

@ComponentScan

public class MainConfig3 {

}

来个测试用例

@Test

public void test3() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

context.register(MainConfig3.class);

context.refresh();

}

运行:报错了

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘service1’: Bean with name ‘service1’ has been injected into other beans [service2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:624)

可以看出是AbstractAutowireCapableBeanFactory.java:624这个地方整出来的异常,将这块代码贴出来给大家看一下:

if (earlySingletonExposure) {

//@1

Object earlySingletonReference = getSingleton(beanName, false);

if (earlySingletonReference != null) {

//@2

if (exposedObject == bean) {

exposedObject = earlySingletonReference;

}

//@3

else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {

String[] dependentBeans = getDependentBeans(beanName);

Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);

for (String dependentBean : dependentBeans) {

if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {

actualDependentBeans.add(dependentBean);

}

}

if (!actualDependentBeans.isEmpty()) {

throw new BeanCurrentlyInCreationException(beanName,

“Bean with name '” + beanName + “’ has been injected into other beans [” +

StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +

"] in its raw version as part of a circular reference, but has eventually been " +

"wrapped. This means that said other beans do not use the final version of the " +

"bean. This is often the result of over-eager type matching - consider using " +

“‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.”);

}

}

}

}

上面代码主要用来判断当有循环依赖的情况下,早期暴露给别人使用的bean是否和最终的bean不一样的情况下,会抛出一个异常。

我们再来通过代码级别的来解释上面代码:

@1:调用getSingleton(beanName, false)方法,这个方法用来从3个级别的缓存中获取bean,但是注意了,这个地方第二个参数是false,此时只会尝试从第1级和第2级缓存中获取bean,如果能够获取到,说明了什么?说明了第2级缓存中已经有这个bean了,而什么情况下第2级缓存中会有bean?说明这个bean从第3级缓存中已经被别人获取过,然后从第3级缓存移到了第2级缓存中,说明这个早期的bean被别人通过getSingleton(beanName, true)获取过

@2:这个地方用来判断早期暴露的bean和最终spring容器对这个bean走完创建过程之后是否还是同一个bean,上面我们的service1被代理了,所以这个地方会返回false,此时会走到@3

@3:allowRawInjectionDespiteWrapping这个参数用来控制是否允许循环依赖的情况下,早期暴露给被人使用的bean在后期是否可以被包装,通俗点理解就是:是否允许早期给别人使用的bean和最终bean不一致的情况,这个值默认是false,表示不允许,也就是说你暴露给别人的bean和你最终的bean需要是一直的,你给别人的是1,你后面不能将其修改成2了啊,不一样了,你给我用个鸟。

而上面代码注入到service2中的service1是早期的service1,而最终spring容器中的service1变成一个代理对象了,早期的和最终的不一致了,而allowRawInjectionDespiteWrapping又是false,所以报异常了。

那么如何解决这个问题:

很简单,将allowRawInjectionDespiteWrapping设置为true就可以了,下面改一下代码如下:

@Test

public void test4() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

//创建一个BeanFactoryPostProcessor:BeanFactory后置处理器

context.addBeanFactoryPostProcessor(beanFactory -> {

if (beanFactory instanceof DefaultListableBeanFactory) {

//将allowRawInjectionDespiteWrapping设置为true

((DefaultListableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);

}

});

context.register(MainConfig3.class);

context.refresh();

System.out.println(“容器初始化完毕”);

}

上面代码中将allowRawInjectionDespiteWrapping设置为true了,是通过一个BeanFactoryPostProcessor来实现的,后面会有一篇文章来详解BeanFactoryPostProcessor,目前你只需要知道BeanFactoryPostProcessor可以在bean创建之前用来干预BeanFactory的创建过程,可以用来修改BeanFactory中的一些配置。

再次输出

容器初始化完毕

此时正常了,我们继续,看看我们加在service1上的拦截器起效了没有,上面代码中加入下面代码:

//获取service1

Service1 service1 = context.getBean(Service1.class);

//获取service2

Service2 service2 = context.getBean(Service2.class);

System.out.println(“----A-----”);

service2.m1(); //@1

System.out.println(“----B-----”);

service1.m1(); //@2

System.out.println(“----C-----”);

System.out.println(service2.getService1() == service1);

上面为了区分结果,使用了----格式的几行日志将输出结果分开了,来运行一下,输出:

容器初始化完毕

----A-----

Service2 m1

Service1 m1

----B-----

你好,service1

Service1 m1

----C-----

false

从输出中可以看出。

service2.m1()对应输出:

Service2 m1

Service1 m1

service1.m1()对应输出:

你好,service1

Service1 m1

而service2.m1方法中调用了service1.m1,这个里面拦截器没有起效啊,但是单独调用service1.m1方法,却起效了,说明service2中注入的service1不是代理对象,所以没有加上拦截器的功能,那是因为service2中注入的是早期的service1,注入的时候service1还不是一个代理对象,所以没有拦截器中的功能。

再看看最后一行输出为false,说明service2中的service1确实和spring容器中的service1不是一个对象了。

ok,那么这种情况是不是很诧异,如何解决这个问题?

既然最终service1是一个代理对象,那么你提前暴露出去的时候,注入到service2的时候,你也必须得是个代理对象啊,需要确保给别人和最终是同一个对象。

这个怎么整?继续看暴露早期bean的源码,注意了下面是重点:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

注意有个getEarlyBeanReference方法,来看一下这个方法是干什么的,源码如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {

Object exposedObject = bean;

if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {

for (BeanPostProcessor bp : getBeanPostProcessors()) {

if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {

SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;

exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);

}

}

}

return exposedObject;

}

从3级缓存中获取bean的时候,会调用上面这个方法来获取bean,这个方法内部会看一下容器中是否有SmartInstantiationAwareBeanPostProcessor这种处理器,然后会依次调用这种处理器中的getEarlyBeanReference方法,那么思路来了,我们可以自定义一个SmartInstantiationAwareBeanPostProcessor,然后在其getEarlyBeanReference中来创建代理不就可以了,聪明,我们来试试,将MethodBeforeInterceptor代码改成下面这样:

@Component

public class MethodBeforeInterceptor implements SmartInstantiationAwareBeanPostProcessor {

@Override

public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {

if (“service1”.equals(beanName)) {

//代理创建工厂,需传入被代理的目标对象

ProxyFactory proxyFactory = new ProxyFactory(bean);

//添加一个方法前置通知,会在方法执行之前调用通知中的before方法

proxyFactory.addAdvice(new MethodBeforeAdvice() {

@Override

public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {

System.out.println(“你好,service1”);

}

});

//返回代理对象

return proxyFactory.getProxy();

}

return bean;

}

}

对应测试用例

@Test

public void test5() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig4.class);

System.out.println(“容器初始化完毕”);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后如何让自己一步步成为技术专家

说句实话,如果一个打工人不想提升自己,那便没有工作的意义,毕竟大家也没有到养老的年龄。

当你的技术在一步步贴近阿里p7水平的时候,毫无疑问你的薪资肯定会涨,同时你能学到更多更深的技术,交结到更厉害的大牛。

推荐一份Java架构之路必备的学习笔记,内容相当全面!!!

成年人的世界没有容易二字,前段时间刷抖音看到一个程序员连着加班两星期到半夜2点的视频。在这个行业若想要拿高薪除了提高硬实力别无他法。

你知道吗?现在有的应届生实习薪资都已经赶超开发5年的程序员了,实习薪资26K,30K,你没有紧迫感吗?做了这么多年还不如一个应届生,真的非常尴尬!

进了这个行业就不要把没时间学习当借口,这个行业就是要不断学习,不然就只能被裁员。所以,抓紧时间投资自己,多学点技术,眼前困难,往后轻松!

【关注】+【转发】+【点赞】支持我!创作不易!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig4.class);

System.out.println(“容器初始化完毕”);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-WCy9helW-1713630531306)]

[外链图片转存中…(img-CJtlg9HE-1713630531307)]

[外链图片转存中…(img-hHaQciQ6-1713630531307)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后如何让自己一步步成为技术专家

说句实话,如果一个打工人不想提升自己,那便没有工作的意义,毕竟大家也没有到养老的年龄。

当你的技术在一步步贴近阿里p7水平的时候,毫无疑问你的薪资肯定会涨,同时你能学到更多更深的技术,交结到更厉害的大牛。

推荐一份Java架构之路必备的学习笔记,内容相当全面!!!

[外链图片转存中…(img-bgjJKj01-1713630531308)]

成年人的世界没有容易二字,前段时间刷抖音看到一个程序员连着加班两星期到半夜2点的视频。在这个行业若想要拿高薪除了提高硬实力别无他法。

你知道吗?现在有的应届生实习薪资都已经赶超开发5年的程序员了,实习薪资26K,30K,你没有紧迫感吗?做了这么多年还不如一个应届生,真的非常尴尬!

进了这个行业就不要把没时间学习当借口,这个行业就是要不断学习,不然就只能被裁员。所以,抓紧时间投资自己,多学点技术,眼前困难,往后轻松!

【关注】+【转发】+【点赞】支持我!创作不易!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值