Spring,SpringCloud全面解析

2021-06-15 15:13:02 浏览数 (1)

Spring循环依赖是什么

如上图所示,两个或两个以上bean互相持有对方,最终形成闭环,循环依赖的场景有个两种

  • 构造器循环依赖
  • set循环依赖

由于我们获取对象的引用时候,对象的属性可以延迟设置,因此我们可以使用set可以解决循环依赖,而构造器不能解决,因为构造器必须在获取引用之前.

使用set属性解决循环依赖,我们必须要知道三级缓存

代码语言:javascript复制
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
  1. singletonObject,一级缓存,保存完整的生命周期的bean,相当一个成品
  2. earlySingletonObjects,二级缓存,保存的是提前曝光的单例bean,相当一个半成品
  3. singletonFactories,三级缓存,保存对象工厂

再看看重要的几个方法

spring解决循环依赖的过程

  1. 调用doGetBean方法,想要获取beanA,于是调用getSingleton方法从缓存中查找beanA
  2. 在getSingletion方法中,从一级缓存中查找,返回null
  3. doGetBean方法中获取到的bean为null,走对应的逻辑,调用getSinleton的重载方法参数是ObjectFactory
  4. 在getSingleton方法中,先将beanA_Name添加到一个集合中,用于标记该bean正在创建中,然后调用匿名内部类的createBean方法
  5. 进入AbstractAutowireCapableBeanFactory.doCreateBean,先反射调用构造器创建beanA实例,然后判断,是否为单例,是否允许提前暴露引用,是否正在创建中,判断为true则将beanA添加到三级缓存中
  6. 对beanA属性填充,此时检测到beanA依赖beanB,于是开始查找beanB
  7. 调用doGetbean方法,和上面检测到beanA依赖beanB,于是开始查找beanB,没有则创建然后给beanB填充属性
  8. 此时beanB依赖beanA,调用getSingleton获取BeanA,依次从一级,二级三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时singletonObject指向的就是上面在doCreateBean方法实例化的beanA
  9. 这样beanB就获取到了beanA的依赖,于是beanB顺序完成实例化,并将beanA从三级缓存移到二级缓存中
  10. 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也完成了创建,回到getSingleton方法中继续向下执行,将beana从二级缓存移到到一级缓存中

spring IOC的理解

首先我们知道如果没有Ioc,我们是如何开发的,比如,下面例子

代码语言:javascript复制
public class MyController {   
private MyService myService = new MyServiceImp();
private  void   getPost(){
        myService.postImp();
    }    
}

MyController在使用Myservice的时候,我们必须实例化他的实现类,但是当我们在多个地方使用Myservice使用,而当我们修改了Myservice的时候,我们就必须修改多处,这样我就不但要承担工作量,还有一定的风险性,因此Spring IOC就应用而生

Spring IOC是如何处理的呢,如下图所示

正如上面例子,MyService的实例化,以及MyController都交给spring处理,以及实例化,当我们想在MyController引用MyServiec,只要添加对应的属性且加注解,就能引用对应的实现类,如下图

代码语言:javascript复制
@Controller
public class MyController {
@Autowired
private MyService myService;

private  void   getPost(){
        myService.postImp();
    }
}

此时当我需要修改MyService的时候,只要修改对应的实现类即可,spring ioc最大的好处就是实现了类与类之间解耦,实例化所有的bean,管理bean之间的依赖注入,维护代码的时候可以更加的轻松便利

spring AOP理解

我们在开发中经常遇到重复的代码,比如给某个方法添加事物,以及添加日志,我们一般都是重复的写这些代码,因此当我们要把某个方法的之前添加的日志和事务删除,或者修改,涉及到许多地方,因此具有相同的修改的问题,我们都可以使用AOP解决。

比如下面方法,

代码语言:javascript复制
public interface IBuy {
String buy();
}
代码语言:javascript复制
@Component
public class Boy implements IBuy {
@Override
public String buy() {
        System.out.println("男孩买了一个游戏机");
return "游戏机";
    }
}
代码语言:javascript复制
@Component
public class Girl implements IBuy {
@Override
public String buy() {
        System.out.println("女孩买了一件漂亮的衣服");
return "衣服";
    }
}

我们要在Boy和Girl在调用Buy之前添加打印一些其他语句,此时我们就可以使用AOP.我们把IBuy中的buy叫做切点,而我们要实现添加打印其他语句的处理就叫做通知(也就是增强处理),我们把这个切点和通知结合叫做切面,而当在应用执行过程中把增强处理插入到切点的点叫做连接点,而这个添加增强处理的过程就是织入,

我们上面例子的增强处理如下

代码语言:javascript复制
@Aspect
@Component
public class BuyAspectJ {
@Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))")
public void haha(){
        System.out.println("男孩女孩都买自己喜欢的东西");
    }
}

然后开启AOP配置如下

代码语言:javascript复制
@Configuration
@ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}

打印语句如下

可以看到我们并没有修改原有的方法,也没有修改任何代码,然而给相应的接口添加了新的功能,几乎没有任何侵入,这就是AOP.而AOP的底层实现原理就是动态代理

动态代理

动态代理其实就是创建一个代理类,创建这个代理类的实例对象,这个里面引用你真正使用的类,所有的方法调用,都是走代理类的对象,他负责在代码上做一些增强,在去调用你的类

而spring aop 中,比如你对一批类和他们的方法进行做了一个切面,定义好要在这些类的方法增强代码,sprin必然会生成这些类的代理对象,在动态代理中执行你定义的代码

如果你的类是实现某个接口,spring 就会使用JDK动态代理,生一个跟你实现同样接口的一个代理类,构造一个实例对象出来,jdk代理对象,他其实是在你的类在有接口的时候使用

但是很多时候,我们的类并没有实现接口,因此我们就要用到了CGLIB生成代理对象,他是生成类的一个子类,他可以动态生成字节码,覆盖你的方法,在方法中加入增强的代码

JDK动态代理实例代码

代码语言:javascript复制
public class PersonServiceImp implements PersonService {
@Override
public void eat() {
        System.out.println("吃饭");
    }
}
代码语言:javascript复制
public class ProxyService implements InvocationHandler {
private Object target;

public ProxyService(Object target) {
this.target = target;
    }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("洗手");
Object result = method.invoke(target, args);
        System.out.println("洗碗");
return result;
    }

public  Object getProxy(){
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
this.target.getClass().getInterfaces(),this);
    }
}
代码语言:javascript复制
public class ProxyTest {
public static void main(String[] args) {
        PersonService personService = new PersonServiceImp();
        ProxyService proxyService=new ProxyService(personService);
        PersonService proxy = (PersonService) proxyService.getProxy();
        proxy.eat();
    }
}

CGlib动态代理实例代码

代码语言:javascript复制
public class Studtent {

public String study(String subject) {
        System.out.println("study" subject "中......");
return subject;
    }
}
代码语言:javascript复制
public class StudentProxy implements MethodInterceptor {

//    获取代理对象,参数为目标对象的Class类对象
public Object getProxy(Class target){
//        以下四句代码记住即可,反正就是创建代理类对象的方法,底层不要多在意
        Enhancer enhancer=new Enhancer();
        enhancer.setSuperclass(target);
        enhancer.setCallback(this);
return enhancer.create();

    }
//    重写intercept方法
//    参数分别为代理对象,目标对象方法,方法参数,代理对象方法
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//        增强方法
        System.out.println("准备学习了!");
//        利用反射机制,通过代理对象的方法,实现目标对象的方法调用,这里可以有其他代码方式,后面介绍
Object invoke1 = methodProxy.invokeSuper(proxy, args);
//        增强方法
        System.out.println("一个小时过去了,学习完了!");
//        返回方法执行后的返回值
return invoke1;
    }
}
代码语言:javascript复制
public class Test {
public static void main(String[] args) {
        Studtent target = new Studtent();
//        创建代理对象,注意用到了强转
        Studtent student_proxy = (Studtent) new StudentProxy().getProxy(target.getClass());
//        执行目标对象的方法,增强过的,并且有返回值
        String subject1 = student_proxy.study("中文");
//        输出返回值,就是传入的参数
        System.out.println(subject1);
    }
}

spring中的bean是线程安全的吗

答案默认情况下不是线程安全的,首先我们了解一下spring 容器的bean作用域

  • singleton,默认作用于容器中只有一个实例bean
  • prototype,为每一个bean请求提供一个实例
  • request,为每一个网络请求创建一个实例,在完成请求后,bean会失效并垃圾回收
  • session,与request范围类似,确保每个session中有一个bean实例,在session过期后,bean会随之失效
  • global-session,全局会话,所有会话共享一个实例。

对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

对于有状态的bean,Spring官方提供的bean,一般提供了通过ThreadLocal去解决线程安全的方法,比如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等。

注:Spring容器本身并没有提供线程安全的策略,因此是否线程安全完全取决于Bean本身的特性

spring事务实现原理是什么以及传播机制的理解

事务的实现原理,如果说加了一个@Transactional注解,此时就spring会使用AOP是想,对你的这个方法执行之前,先去开启事务,执行完毕之后,根据你的方法是否报错,来决定回滚还是提交事务

  • PROPAGATION_REQUIRED,如果当前没有事务,就创建一个新事务,如果当前存在事务就加入该事务,该设置是最常用的设置
代码语言:javascript复制
@Transactional(propagation=Propagation.REQUIRED)
private void  methodA(){
   //do somethin
   methodB()
}
@Transactional(propagation=Propagation.REQUIRED)
private void  methodB(){
//do somethin
}

当调用methodA里面调用methodB的时候,methodB加入methodA的事务中,而当单独调用methodB就会开启一个新的事务

  • PROPAGATION_SUPPORTS,支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就一非事务执行
代码语言:javascript复制
@Transactional(propagation=Propagation.REQUIRED)
private void  methodA(){
//do somethin
   methodB()
}
@Transactional(propagation=Propagation.SUPPORTS)
private void  methodB(){
//do somethin
}

如果methodA调用methodB的时候,methodB就会加入methodA开始的

事务中,当单独调methodB的时候,methodB就会以非事务执行

  • PROPAGATION_MANDATORY,支持当前事务,如果当前存在事务,就加入该事务,如果不存在,则抛出异常
代码语言:javascript复制
@Transactional(propagation=Propagation.REQUIRED)
private void  methodA(){
//do somethin
   methodB()
}
@Transactional(propagation=Propagation.MANDATORY)
private void  methodB(){
//do somethin
}

如果methodA调用methodB的时候,methodB就会加入methodA开始的事务中,单独调methodB就抛出异常

  • PROPAGATION.REQUIRES_NEW,创建一个新的事务并挂起当前事务
代码语言:javascript复制
@Transactional(propagation=Propagation.REQUIRED)
private void  methodA(){
   dosomethingBefore();
   methodB();
   doSomethingAfter();
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
private void  methodB(){
//do somethin
}

methodA开启一个事物1,然后调用methodB,也会开启一个事务2,两个事务互不干扰,当methodB进行回滚只会回滚自己的方法,当methodA也是会回滚自己的方法

  • PROPAGATION.NOT_SUPPORTED.以非事务方式执行,如果存在事务则抛出异常
代码语言:javascript复制
@Transactional(propagation=Propagation.REQUIRED)
private void  methodA(){  
   methodB();
}
@Transactional(propagation=Propagation.NOT_SUPPORTED)
private void  methodB(){
//do somethin
}

methodA调用methodB就会把当前事务挂起,单独使用methodB就会以非事务执行

  • PROPAGATION.NEVER,以非事务方式执行,如果存在事务,则抛出异常
代码语言:javascript复制
@Transactional(propagation=Propagation.REQUIRED)
private void  methodA(){  
   methodB();
}
@Transactional(propagation=Propagation.NEVER)
private void  methodB(){
//do somethin
}

如果methodA调用methodB就会抛出异常,单独代用methodB以非事务执行

  • PROPAGATION.NESTED,如果当前存在事务,则在嵌套事务内执行,如果当前没有事务则按REQUIRED属性执行
代码语言:javascript复制
@Transactional(propagation=Propagation.REQUIRED)
private void  methodA(){
   dosomethingBefore();
   methodB();
   doSomethingAfter();
}
@Transactional(propagation=Propagation.NESTED)
private void  methodB(){
//do somethin
}

methodA开启事务,执行methodA代码,设置一个回滚点savepoint,

然后调用methodB执行代码,如果methodB抛出异常,此时回滚到之前的savePoint,执行methodA的代码,domethindAfter,提交或回滚事务,但是如果methodA进行回滚也会把methodB的事物进行回滚

也就是外层事务进行回滚会导致内层事务一起回滚,而内层事务只会回滚自己的代码

spring的bean的声明周期

  1. bean的实例化,对于beanFactory一般是延迟实例化,即使getBean方法才会实例化,但是对于ApplicaionContext,当容器初始化完成之后,就完成了所有bean的实例化,实例化对象被包装在beanWrapper对象中,BeanWrapper提供设置对象数据的接口,从而避免了使用反射机制设置属性
  2. 设置对象属性依赖注入,实例化的对象封装在beanWarpper对象中,紧接着,spring根据BeanDefinition中的信息以及通过BeanWarapper提供的设置的属性接口完成依赖注入
  3. 处理Aware接口,如果实现BeanNameAwate接口,则spring调用Bean的setBeanName传入bean的id,如果实现了BeanFactoryAware接口,则spring则setBeanFactory传入当前工作实例的引用,如果实现ApplicatonContextAware接口,则spring调用setApplicationContext传入当前ApplicationContext实例引用
  4. 如果实现了BeanPostProcessor,在初始化实例之前,对bean进行前置处理
  5. 如果实现了initializingBean接口,则调用afterPropertiesSet方法,仅做一些额外的逻辑处理
  6. 此时如果配置文件配置了init-method指定了初始化方法,则调用该初始化方法
  7. 如果实现了BeanPostProcessor,实例化之后,对bean进行后置处理
  8. 如果bean实现了DisposableBean接口,则sprign会调用destory将方法进行销毁,如果配置文件配置了destory-method属性指定了销毁方法,则对该方法对bean进行销毁

spring有哪些设计模式

工厂模式,我们使用的spring IOC使用的就是工厂模式,spring容器把所有的bean进行实例化,管理bean的生命周期

单例模式,spring的默认作用于就是使用的是单例模式,

代码语言:javascript复制
/**
 * 懒汉模式
 */
public class Singleton {
    private static  volatile  Singleton singleton = null;
public Singleton() {
    }
    private Singleton  getSingletonInstance(){
        if (singleton==null) {
            synchronized (Singleton.class){
if (singleton==null) {
                    singleton = new Singleton();
                }
            }
        }
return  singleton;
    }
}

代理模式,如spring AOP的底层原理使用的动态代理。

Spring MVC执行流程

spring cloud核心架构原理

Eureka服务注册原理

服务注册发现

  1. 服务B注册到Eureka的注册表中
  2. Eureka里面有两级缓存,ReadOnly,ReadWrite缓存,这样做的原因是为了防止并发读写
  3. 当服务B注册到了之后,会立刻同步到ReadWrite缓存中,然后后台有一个定时任务,会定时发注册的信息,同步到ReadOnly缓存中
  4. 当注册到ReadOnly缓存中之后,服务A每隔几十秒就会从Readonly拉去注册信息,
  5. 当服务B的另外一个实例启动,注册到Eureka中,而此时就会把同步到ReadWrite中,然后再每隔几十秒就会就会同样同步到ReadOnly缓存
  6. 但是此时服务A,还是不知道服务B另外一个实例上线了
  7. 当过了十几秒之后,服务A发现ReadOnly缓存和ReadWrite缓存都已经没有信息了,此时就会把注册表的信息,同步到ReadWrite和ReadOnly缓存中,服务A就会得到最新的注册信息
  8. 此时服务A就会有服务B的两台实例的信息

服务心跳与故障

  1. 每个服务服务都会定时想Eureka发送心跳
  2. 而Eureka也会有一个线程检测心跳,此时发现服务B有一个实例已经很长时间发送心跳,就会把注册表的信息删除
  3. 但是此时并不是删除ReadWrite缓存的信息,而是清空ReadWrite缓存,然后后台线程个十几秒会更新信息到ReadOnly缓存,此时ReadWrite和ReadOnly的信息都是空,而此时服务A并并不知道服务B的一个实例已经下线,此时还会请求,但是请求失败
  4. 当服务A到了配置的时间,就会拉去readOnly缓存,和ReadWrite缓存发现没有,就会把注册表的信息,同步到ReadOnly和ReadWrite缓存中
  5. 此时服务A也会得到最新的注册信息,此时才会知道原来服务B的一个实例已经下线了。

所以我发现,当我们注册一个实例服务之后,发现很长时间都没有成功发现,就是由于你配置的的时间过长了。

最后我们知道spring cloud对外发布的其实就是http协议的接口,而其实feign,他就是对一个接口加上一个注解,然后对有这个注解的接口生成动态代理,然后针对fegin的动态代理代用他的方法的时候,此时会在底层生成http协议格式的请求,

使用http协议框架组件,httpClient,先得使用ribbon去本地的Eureka注册表缓存获取对方的机器列表,然后进行负载均衡,选择一台机器,接着针对这个机器发送http请求,

而其中zuul,是根据配置的请求路径和服务的关系,然后匹配到对应的服务,在转发请求到对应的服务。

Eureka和zookeeper区别

集群方式

Eureka集群

zookeeper集群

Eureka部署一个集群,但是集群的每个机器是平等的,各个服务可以向任何一个Eureka实例服务注册和服务发现,集群里任何一个Eureka实例接收到写请求之后,会自动同步给其他所有的Eureka实例

zk,是有一个leader节点接受数据,也就是服务注册,其他节点为follower,服务服务发现,leader节点会同步信息到其他机器

CAP

ZK,当leader节点挂了,要重新选举leader,在期间是不能继续写数据的,这里保证了一致性,但是牺牲了可用性,

Eureka,当一个服务注册到了Eureka的A,此时没有同步到其他服务,就挂掉了,此时从其他机器服务拉去的信息是旧的信息,看不到最新的数据,保证了服务的可用性,但是牺牲了一致性,而Eureka通过心跳往其他服务注册信息,保证了最终一致性。

服务注册发现的失效性

对应zk,失效性更好,注册或者宕机,都是秒级别的

而对于Eureka就非常糟糕,上面我们看看Eureka服务发现非常慢的,由于后台使用缓存机制,导致服务上线后,极端情况下要等到2-3分钟,才能感知到服务

容量

其实zk和Eureka都不适合大的容量,比如zk,每一个的服务注册,都会同步到其他机器,且要通知其他服务,若干后台有上万服务,可能就会占用大多数的资源和带宽

而Eureka也是一样,每一个Eureka实例对每一个服务的注册和心跳都是要同步的其他Eureka服务,比如Eureka要对上万台机器进行心跳检测,这个的检测结果同样也会同步到其他Eureka服务,这样服务也是支持不了的

持续关注,下一篇dubbo全面总结,如果对您有一丝丝帮助,麻烦点个关注,也欢迎转发,谢谢

0 人点赞