首页 文章 精选 留言 我的

精选列表

搜索[面试],共4912篇文章
优秀的个人博客,低调大师

Android面试,View绘制流程以及invalidate()等相关方法分析

整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为 根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否重新需要安置视图的位置(layout)、以及是否需要重绘 (draw),其框架过程如下: measure()过程 主要作用:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。 具体的调用链如下: ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下: 1、设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth) ; 2 、如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。 2.1 对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。 整个measure调用流程就是个树形的递归过程 measure函数原型为 View.java 该函数不能被重载 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { //.... //回调onMeasure()方法 onMeasure(widthMeasureSpec, heightMeasureSpec); //more } 为了大家更好的理解,采用“二B程序员”的方式利用伪代码描述该measure流程 //回调View视图里的onMeasure过程 private void onMeasure(int height , int width){ //设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight) //1、该方法必须在onMeasure调用,否者报异常。 setMeasuredDimension(h , l) ; //2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程 int childCount = getChildCount() ; for(int i=0 ;i<childCount ;i++){ //2.1、获得每个子View对象引用 View child = getChildAt(i) ; //整个measure()过程就是个递归过程 //该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法都 measureChildWithMargins(child , h, i) ; //其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下: //child.measure(h, l) } } //该方法具体实现在ViewGroup.java里 。 protected void measureChildWithMargins(View v, int height , int width){ v.measure(h,l) } layout布局过程 主要作用 :为将整个根据子视图的大小以及布局参数将View树放到合适的位置上 具体的调用链如下: host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下 1 、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ; 2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。 layout函数原型为 ,位于View.java /* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴 * @param l Left position, relative to parent * @param t Top position, relative to parent * @param r Right position, relative to parent * @param b Bottom position, relative to parent */ public final void layout(int l, int t, int r, int b) { boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴 if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); } onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局 mPrivateFlags &= ~LAYOUT_REQUIRED; } mPrivateFlags &= ~FORCE_LAYOUT; } 同样地, 将上面layout调用流程,用伪代码描述如下: // layout()过程 ViewRoot.java // 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout() private void performTraversals(){ //... View mView ; mView.layout(left,top,right,bottom) ; //.... } //回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现 private void onLayout(int left , int top , right , bottom){ //如果该View不是ViewGroup类型 //调用setFrame()方法设置该控件的在父视图上的坐标轴 setFrame(l ,t , r ,b) ; //-------------------------- //如果该View是ViewGroup类型,则对它的每个子View进行layout()过程 int childCount = getChildCount() ; for(int i=0 ;i<childCount ;i++){ //2.1、获得每个子View对象引用 View child = getChildAt(i) ; //整个layout()过程就是个递归过程 child.layout(l, t, r, b) ; } } draw()绘图过程 由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。 调用流程 : mView.draw()开始绘制,draw()方法实现的功能如下: 1 、绘制该View的背景 2 、为显示渐变框做一些准备操作(大多数情况下,不需要改渐变框) 3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法) 4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。 4.1 dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个 地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。 绘制滚动条 伪代码: // draw()过程 ViewRoot.java // 发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法, 该方法会继续调用draw()方法开始绘图 private void draw(){ //... View mView ; mView.draw(canvas) ; //.... } //回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现 private void draw(Canvas canvas){ //该方法会做如下事情 //1 、绘制该View的背景 //2、为绘制渐变框做一些准备操作 //3、调用onDraw()方法绘制视图本身 //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。 // 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。 //5、绘制渐变框 } //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法 @Override protected void dispatchDraw(Canvas canvas) { // //其实现方法类似如下: int childCount = getChildCount() ; for(int i=0 ;i<childCount ;i++){ View child = getChildAt(i) ; //调用drawChild完成 drawChild(child,canvas) ; } } //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法 protected void drawChild(View child,Canvas canvas) { // .... //简单的回调View对象的draw()方法,递归就这么产生了。 child.draw(canvas) ; //......... } invalidate()方法 说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。 一般引起invalidate()操作的函数如下: 1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。 2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。 3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。 4 、setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。 requestLayout()方法 会导致调用measure()过程 和 layout()过程 。 说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。 一般引起invalidate()操作的函数如下: 1、setVisibility()方法: 当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。 同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。 requestFocus() 说明:请求View树的draw()过程,但只绘制“需要重绘”的视图。 View中的draw和onDraw的区别 1.大概扫一下源码就可以明白,draw()这个函数本身会做很多事情, * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance)在第三步的时候,它就会调用onDraw()方法,来绘制view的内容。也就是draw会调用onDraw。所以看需要,一般情况下,直接用onDraw绘制view的content就可以了,如果绘制多一点的内容,可以调用draw(),不过Android官方推荐用只用onDraw就可以了。“When implementing a view, do not override this method; instead, you should implement onDraw” 2.View组件的绘制会调用draw(Canvas canvas)方法,draw过程中主要是先画Drawable背景,对drawable调用setBounds(),然后是draw(Canvas c)方法.有点注意的是背景drawable的实际大小会影响view组件的大小,drawable的实际大小通过getIntrinsicWidth()和getIntrinsicHeight()获取,当背景比较大时view组件大小等于背景drawable的大小,画完背景后,draw过程会调用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,或者自定制一个Drawable,重写它的draw(Canvas c)和 getIntrinsicWidth(),getIntrinsicHeight()方法,然后设为背景。 3.ondraw() 和dispatchdraw()的区别 绘制VIew本身的内容,通过调用View.onDraw(canvas)函数实现 绘制自己的孩子通过dispatchDraw(canvas)实现 本文转自我爱物联网博客园博客,原文链接:http://www.cnblogs.com/yydcdut/p/3960826.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

Android面试,BroadCastReceiver的两种注册方式的异同

在Android手机应用程序中开发中,需要用到BroadcastReceiver来监听广播的消息。在自定义好BroadcastReceiver ,需要对其进行注册,注册有两种方法: 一种是在代码当中注册,注册的方法是registerReceiver(receiver,filter)(用Activity的实例来调用),取消注册的方法:unregisterReceiver(receiver),如果一个BroadcastReceiver用于更新UI(User Interface),那么通常会使用这种方法进行注册,在Activity启动的时候进行注册,在Activity不可见后取消注册; 另一种就是在AndroidManifest当中进行注册。 使用代码进行注册 IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED"); IncomingSMSReceiver receiver = new IncomingSMSReceiver(); registerReceiver(receiver, filter); 在AndroidManifest.xml文件中的<application>节点里进行注册 <receiver android:name=".IncomingSMSReceiver"> <intent-filter> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> </intent-filter> </receiver> 注册完之后即可以发送广播,使用Context.sendBroadcast()、Context.sendOrderedBroadcast()或者Context.sendStickyBroadcast()来实现,接收端代码: public class IncomingSMSReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //todo...接收广播,做自己的业务 } } 区别 在AndroidManifest中进行注册后,不管改应用程序是否处于活动状态,都会进行监听,比如某个程序时监听 内存 的使用情况的,当在手机上安装好后,不管改应用程序是处于什么状态,都会执行改监听方法中的内容。 在代码中进行注册后,当应用程序关闭后,就不再进行监听。我们读知道,应用程序是否省电,决定了该应用程序的受欢迎程度,所以,对于那些没必要在程序关闭后仍然进行监听的Receiver,在代码中进行注册,无疑是一个明智的选择。 本文转自我爱物联网博客园博客,原文链接:http://www.cnblogs.com/yydcdut/p/3909619.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

面试官:spring中定义bean的方法有哪些? 我一口气说完12种,彻底征服面试官.

2 --> 前言 在庞大的java体系中,spring有着举足轻重的地位,它给每位开发者带来了极大的便利和惊喜。我们都知道spring是创建和管理bean的工厂,它提供了多种定义bean的方式,能够满足我们日常工作中的多种业务场景。 那么问题来了,你知道spring中有哪些方式可以定义bean? 最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。 BAT大佬写的刷题笔记,让我offer拿到手软 我估计很多人会说出以下三种: 没错,但我想说的是以上三种方式只是开胃小菜,实际上spring的功能远比你想象中更强大。 各位看官如果不信,请继续往下看。 1. xml文件配置bean 我们先从xml配置bean开始,它是spring最早支持的方式。后来,随着springboot越来越受欢迎,该方法目前已经用得很少了,但我建议我们还是有必要了解一下。 1.1 构造器 如果你之前有在bean.xml文件中配置过bean的经历,那么对如下的配置肯定不会陌生: <bean > </bean> 这种方式是以前使用最多的方式,它默认使用了无参构造器创建bean。 当然我们还可以使用有参的构造器,通过<constructor-arg>标签来完成配置。 <bean > <constructor-arg index="0" value="susan"></constructor-arg> <constructor-arg index="1" ref="baseInfo"></constructor-arg> </bean> 其中: index表示下标,从0开始。 value表示常量值 ref表示引用另一个bean 1.2 setter方法 除此之外,spring还提供了另外一种思路:通过setter方法设置bean所需参数,这种方式耦合性相对较低,比有参构造器使用更为广泛。 先定义Person实体: @Data public class Person { private String name; private int age; } 它里面包含:成员变量name和age,getter/setter方法。 然后在bean.xml文件中配置bean时,加上<property>标签设置bean所需参数。 <bean > <property name="name" value="susan"></constructor-arg> <property name="age" value="18"></constructor-arg> </bean> 1.3 静态工厂 这种方式的关键是需要定义一个工厂类,它里面包含一个创建bean的静态方法。例如: public class SusanBeanFactory { public static Person createPerson(String name, int age) { return new Person(name, age); } } 接下来定义Person类如下: @AllArgsConstructor @NoArgsConstructor @Data public class Person { private String name; private int age; } 它里面包含:成员变量name和age,getter/setter方法,无参构造器和全参构造器。 然后在bean.xml文件中配置bean时,通过factory-method参数指定静态工厂方法,同时通过<constructor-arg>设置相关参数。 <bean factory-method="createPerson"> <constructor-arg index="0" value="susan"></constructor-arg> <constructor-arg index="1" value="18"></constructor-arg> </bean> 1.4 实例工厂方法 这种方式也需要定义一个工厂类,但里面包含非静态的创建bean的方法。 public class SusanBeanFactory { public Person createPerson(String name, int age) { return new Person(name, age); } } Person类跟上面一样,就不多说了。 然后bean.xml文件中配置bean时,需要先配置工厂bean。然后在配置实例bean时,通过factory-bean参数指定该工厂bean的引用。 <bean > </bean> <bean factory-bean="susanBeanFactory" factory-method="createPerson"> <constructor-arg index="0" value="susan"></constructor-arg> <constructor-arg index="1" value="18"></constructor-arg> </bean> 1.5 FactoryBean 不知道大家有没有发现,上面的实例工厂方法每次都需要创建一个工厂类,不方面统一管理。 这时我们可以使用FactoryBean接口。 public class UserFactoryBean implements FactoryBean<User> { @Override public User getObject() throws Exception { return new User(); } @Override public Class<?> getObjectType() { return User.class; } } 在它的getObject方法中可以实现我们自己的逻辑创建对象,并且在getObjectType方法中我们可以定义对象的类型。 然后在bean.xml文件中配置bean时,只需像普通的bean一样配置即可。 <bean > </bean> 轻松搞定,so easy。 注意:getBean("userFactoryBean");获取的是getObject方法中返回的对象。而getBean("&userFactoryBean");获取的才是真正的UserFactoryBean对象。 我们通过上面五种方式,在bean.xml文件中把bean配置好之后,spring就会自动扫描和解析相应的标签,并且帮我们创建和实例化bean,然后放入spring容器中。 虽说基于xml文件的方式配置bean,简单而且非常灵活,比较适合一些小项目。但如果遇到比较复杂的项目,则需要配置大量的bean,而且bean之间的关系错综复杂,这样久而久之会导致xml文件迅速膨胀,非常不利于bean的管理。 2. Component注解 为了解决bean太多时,xml文件过大,从而导致膨胀不好维护的问题。在spring2.5中开始支持:@Component、@Repository、@Service、@Controller等注解定义bean。 如果你有看过这些注解的源码的话,就会惊奇得发现:其实后三种注解也是@Component。 @Component系列注解的出现,给我们带来了极大的便利。我们不需要像以前那样在bean.xml文件中配置bean了,现在只用在类上加Component、Repository、Service、Controller,这四种注解中的任意一种,就能轻松完成bean的定义。 @Component系列注解的出现,给我们带来了极大的便利。我们不需要像以前那样在bean.xml文件中配置bean了,现在只用在类上加Component、Repository、Service、Controller,这四种注解中的任意一种,就能轻松完成bean的定义。 @Service public class PersonService { public String get() { return "data"; } } 不过,需要特别注意的是,通过这种@Component扫描注解的方式定义bean的前提是:需要先配置扫描路径。 目前常用的配置扫描路径的方式如下: 在applicationContext.xml文件中使用<context:component-scan>标签。例如: <context:component-scan base-package="com.sue.cache" /> 在springboot的启动类上加上@ComponentScan注解,例如: @ComponentScan(basePackages = "com.sue.cache") @SpringBootApplication public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args); } } 直接在SpringBootApplication注解上加,它支持ComponentScan功能: @SpringBootApplication(scanBasePackages = "com.sue.cache") public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args); } } 当然,如果你需要扫描的类跟springboot的入口类,在同一级或者子级的包下面,无需指定scanBasePackages参数,spring默认会从入口类的同一级或者子级的包去找。 @SpringBootApplication public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args); } } 此外,除了上述四种@Component注解之外,springboot还增加了@RestController注解,它是一种特殊的@Controller注解,所以也是@Component注解。 @RestController还支持@ResponseBody注解的功能,即将接口响应数据的格式自动转换成json。 @Component系列注解已经让我们爱不释手了,它目前是我们日常工作中最多的定义bean的方式。 3. JavaConfig @Component系列注解虽说使用起来非常方便,但是bean的创建过程完全交给spring容器来完成,我们没办法自己控制。 spring从3.0以后,开始支持JavaConfig的方式定义bean。它可以看做spring的配置文件,但并非真正的配置文件,我们需要通过编码java代码的方式创建bean。例如: @Configuration public class MyConfiguration { @Bean public Person person() { return new Person(); } } 在JavaConfig类上加@Configuration注解,相当于配置了<beans>标签。而在方法上加@Bean注解,相当于配置了<bean>标签。 此外,springboot还引入了一些列的@Conditional注解,用来控制bean的创建。 @Configuration public class MyConfiguration { @ConditionalOnClass(Country.class) @Bean public Person person() { return new Person(); } } @ConditionalOnClass注解的功能是当项目中存在Country类时,才实例化Person类。换句话说就是,如果项目中不存在Country类,就不实例化Person类。 这个功能非常有用,相当于一个开关控制着Person类,只有满足一定条件才能实例化。 spring中使用比较多的Conditional还有: ConditionalOnBean ConditionalOnProperty ConditionalOnMissingClass ConditionalOnMissingBean ConditionalOnWebApplication 如果你对这些功能比较感兴趣,可以看看《》,这是我之前写的一篇文章,里面做了更详细的介绍。 下面用一张图整体认识一下@Conditional家族: 恭喜你,这是个好问题,因为@Import注解也支持。 4. Import注解 通过前面介绍的@Configuration和@Bean相结合的方式,我们可以通过代码定义bean。但这种方式有一定的局限性,它只能创建该类中定义的bean实例,不能创建其他类的bean实例,如果我们想创建其他类的bean实例该怎么办呢? 这时可以使用@Import注解导入。 4.1 普通类 spring4.2之后@Import注解可以实例化普通类的bean实例。例如: 先定义了Role类: @Data public class Role { private Long id; private String name; } 接下来使用@Import注解导入Role类: @Import(Role.class) @Configuration public class MyConfig { } 然后在调用的地方通过@Autowired注解注入所需的bean。 @RequestMapping("/") @RestController public class TestController { @Autowired private Role role; @GetMapping("/test") public String test() { System.out.println(role); return "test"; } } 聪明的你可能会发现,我没有在任何地方定义过Role的bean,但spring却能自动创建该类的bean实例,这是为什么呢? 这也许正是@Import注解的强大之处。 此时,有些朋友可能会问:@Import注解能定义单个类的bean,但如果有多个类需要定义bean该怎么办呢? 恭喜你,这是个好问题,因为@Import注解也支持 @Import({Role.class, User.class}) @Configuration public class MyConfig { } 甚至,如果你想偷懒,不想写这种MyConfig类,springboot也欢迎。 @Import({Role.class, User.class}) @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class}) public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args); } } 可以将@Import加到springboot的启动类上。 这样也能生效? springboot的启动类一般都会加@SpringBootApplication注解,该注解上加了@SpringBootConfiguration注解。 而@SpringBootConfiguration注解,上面又加了@Configuration注解 所以,springboot启动类本身带有@Configuration注解的功能。 意不意外?惊不惊喜? 4.2 Configuration类 上面介绍了@Import注解导入普通类的方法,它同时也支持导入Configuration类。 先定义一个Configuration类: @Configuration public class MyConfig2 { @Bean public User user() { return new User(); } @Bean public Role role() { return new Role(); } } 然后在另外一个Configuration类中引入前面的Configuration类: @Import({MyConfig2.class}) @Configuration public class MyConfig { } 这种方式,如果MyConfig2类已经在spring指定的扫描目录或者子目录下,则MyConfig类会显得有点多余。因为MyConfig2类本身就是一个配置类,它里面就能定义bean。 但如果MyConfig2类不在指定的spring扫描目录或者子目录下,则通过MyConfig类的导入功能,也能把MyConfig2类识别成配置类。这就有点厉害了喔。 其实下面还有更高端的玩法。 swagger作为一个优秀的文档生成框架,在spring项目中越来越受欢迎。接下来,我们以swagger2为例,介绍一下它是如何导入相关类的。 众所周知,我们引入swagger相关jar包之后,只需要在springboot的启动类上加上@EnableSwagger2注解,就能开启swagger的功能。 其中@EnableSwagger2注解中导入了Swagger2DocumentationConfiguration类。 该类是一个Configuration类,它又导入了另外两个类: SpringfoxWebMvcConfiguration SwaggerCommonConfiguration SpringfoxWebMvcConfiguration类又会导入新的Configuration类,并且通过@ComponentScan注解扫描了一些其他的路径。 SwaggerCommonConfiguration同样也通过@ComponentScan注解扫描了一些额外的路径。 4.3 ImportSelector 还有什么好说的,狂起点赞,简直完美。 4.3 ImportSelector 上面提到的Configuration类,它的功能非常强大。但怎么说呢,它不太适合加复杂的判断条件,根据某些条件定义这些bean,根据另外的条件定义那些bean。 那么,这种需求该怎么实现呢? 这时就可以使用ImportSelector接口了。 首先定义一个类实现ImportSelector接口: public class DataImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"com.sue.async.service.User", "com.sue.async.service.Role"}; } } 重写selectImports方法,在该方法中指定需要定义bean的类名,注意要包含完整路径,而非相对路径。 然后在MyConfig类上@Import导入这个类即可: @Import({DataImportSelector.class}) @Configuration public class MyConfig { } 朋友们是不是又发现了一个新大陆? 不过,这个注解还有更牛逼的用途。 @EnableAutoConfiguration注解中导入了AutoConfigurationImportSelector类,并且里面包含系统参数名称:spring.boot.enableautoconfiguration。 AutoConfigurationImportSelector类实现了ImportSelector接口。 除此之外,spring还提供了专门注册bean的接口:BeanDefinitionRegistryPostProcessor。 该方法会根据ENABLED_OVERRIDE_PROPERTY的值来作为判断条件。 而这个值就是spring.boot.enableautoconfiguration。 换句话说,这里能根据系统参数控制bean是否需要被实例化,优秀。 我个人认为实现ImportSelector接口的好处主要有以下两点: 把某个功能的相关类,可以放到一起,方便管理和维护。 重写selectImports方法时,能够根据条件判断某些类是否需要被实例化,或者某个条件实例化这些bean,其他的条件实例化那些bean等。我们能够非常灵活的定制化bean的实例化。 4.4 ImportBeanDefinitionRegistrar 我们通过上面的这种方式,确实能够非常灵活的自定义bean。 但它的自定义能力,还是有限的,它没法自定义bean的名称和作用域等属性。 有需求,就有解决方案。 接下来,我们一起看看ImportBeanDefinitionRegistrar接口的神奇之处。 先定义CustomImportSelector类实现ImportBeanDefinitionRegistrar接口: public class CustomImportSelector implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class); registry.registerBeanDefinition("role", roleBeanDefinition); RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class); userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE); registry.registerBeanDefinition("user", userBeanDefinition); } } 重写registerBeanDefinitions方法,在该方法中我们可以获取BeanDefinitionRegistry对象,通过它去注册bean。不过在注册bean之前,我们先要创建BeanDefinition对象,它里面可以自定义bean的名称、作用域等很多参数。 然后在MyConfig类上导入上面的类 @Import({CustomImportSelector.class}) @Configuration public class MyConfig { } 我们所熟悉的fegin功能,就是使用ImportBeanDefinitionRegistrar接口实现的: 5. PostProcessor 除此之外,spring还提供了专门注册bean的接口:BeanDefinitionRegistryPostProcessor。 该接口的方法postProcessBeanDefinitionRegistry上有这样一段描述: 修改应用程序上下文的内部bean定义注册表标准初始化。所有常规bean定义都将被加载,但是还没有bean被实例化。这允许进一步添加在下一个后处理阶段开始之前定义bean。 如果用这个接口来定义bean,我们要做的事情就变得非常简单了。只需定义一个类实现BeanDefinitionRegistryPostProcessor接口。 @Component public class MyRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class); registry.registerBeanDefinition("role", roleBeanDefinition); RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class); userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE); registry.registerBeanDefinition("user", userBeanDefinition); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } } 重写postProcessBeanDefinitionRegistry方法,在该方法中能够获取BeanDefinitionRegistry对象,它负责bean的注册工作。 不过细心的朋友可能会发现,里面还多了一个postProcessBeanFactory方法,没有做任何实现。 这个方法其实是它的父接口:BeanFactoryPostProcessor里的方法。 在应用程序上下文的标准bean工厂之后修改其内部bean工厂初始化。所有bean定义都已加载,但没有bean将被实例化。这允许重写或添加属性甚至可以初始化bean。 @Component public class MyPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { DefaultListableBeanFactory registry = (DefaultListableBeanFactory)beanFactory; RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class); registry.registerBeanDefinition("role", roleBeanDefinition); RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class); userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE); registry.registerBeanDefinition("user", userBeanDefinition); } } 既然这两个接口都能注册bean,那么他们有什么区别? BeanDefinitionRegistryPostProcessor 更侧重于bean的注册 BeanFactoryPostProcessor 更侧重于对已经注册的bean的属性进行修改,虽然也可以注册bean。 此时,有些朋友可能会问:既然拿到BeanDefinitionRegistry对象就能注册bean,那通过BeanFactoryAware的方式是不是也能注册bean呢? 从下面这张图能够看出DefaultListableBeanFactory就实现了BeanDefinitionRegistry接口。 这样一来,我们如果能够获取DefaultListableBeanFactory对象的实例,然后调用它的注册方法,不就可以注册bean了? 说时迟那时快,定义一个类实现BeanFactoryAware接口: @Component public class BeanFactoryRegistry implements BeanFactoryAware { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { DefaultListableBeanFactory registry = (DefaultListableBeanFactory) beanFactory; RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(User.class); registry.registerBeanDefinition("user", rootBeanDefinition); RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class); userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE); registry.registerBeanDefinition("user", userBeanDefinition); } } 重写setBeanFactory方法,在该方法中能够获取BeanFactory对象,它能够强制转换成DefaultListableBeanFactory对象,然后通过该对象的实例注册bean。 当你满怀喜悦的运行项目时,发现竟然报错了: spring中bean的创建过程顺序大致如下: BeanFactoryAware接口是在bean创建成功,并且完成依赖注入之后,在真正初始化之前才被调用的。在这个时候去注册bean意义不大,因为这个接口是给我们获取bean的,并不建议去注册bean,会引发很多问题。 最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。 BAT大佬写的刷题笔记,让我offer拿到手软

优秀的个人博客,低调大师

分布式系统架构,回顾2020年常见面试知识点梳理(每次面试都会问到其中某一块知识点)

分布式分为分布式缓存(Redis)、分布式锁(Redis 或 Zookeeper)、分布式服务(Dubbo 或 SpringCloud)、分布式服务协调(Zookeeper)、分布式消息队列(Kafka 、RabbitMq)、分布式 Session 、分布式事务、分布式搜索(Elasticsearch)等。不可能所有分布式内容都熟悉,一定要在某个领域有所专长。 分布式理论 问:分布式有哪些理论? CAP 、BASE。分布式 CAP 理论,任何一个分布式系统都无法同时满足 Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性) 这三个基本需求。最多只能满足其中两项。而 Partition tolerance(分区容错性) 是必须的,因此一般是 CP ,或者 AP。 问:你怎么理解分布式一致性? 数据一致性通常指关联数据之间的逻辑关系是否正确和完整。在分布式系统中,数据一致性往往指的是由于数据的复制,不同数据节点中的数据内容是否完整并且相同。 一致性还分为强一致性,弱一致性,还有最终一致性。强一致性就是马上就保持一致。 最终一致性是指经过一段时间后,可以保持一致。 分布式事务 问:你怎么理解分布式事务?分布式事务的协议有哪些? 分布式事务是指会涉及到操作多个数据库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务类型:二阶段提交 2PC ,三阶段提交 3PC。 2PC :第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。 3PC :三个阶段:CanCommit 、PreCommit 、DoCommit。 问:分布式事务的解决方案有哪些? 分布式事务解决方案:补偿机制 TCC 、XA 、消息队列 MQ。 问:讲一下 TCC。 T(Try)锁资源:锁定某个资源,设置一个预备类的状态,冻结部分数据。 比如,订单的支付状态,先把状态修改为"支付中(PAYING)"。 比如,本来库存数量是 100 ,现在卖出了 2 个,不要直接扣减这个库存。在一个单独的冻结库存的字段,比如 prepare _ remove _ stock 字段,设置一个 2。也就是说,有 2 个库存是给冻结了。 积分服务的也是同理,别直接给用户增加会员积分。你可以先在积分表里的一个预增加积分字段加入积分。 比如:用户积分原本是 1190 ,现在要增加 10 个积分,别直接 1190 + 10 = 1200 个积分啊!你可以保持积分为 1190 不变,在一个预增加字段里,比如说 prepare _ add _ credit 字段,设置一个 10 ,表示有 10 个积分准备增加。 C(Confirm):在各个服务里引入了一个 TCC 分布式事务的框架,事务管理器可以感知到各个服务的 Try 操作是否都成功了。假如都成功了, TCC 分布式事务框架会控制进入 TCC 下一个阶段,第一个 C 阶段,也就是 Confirm 阶段。此时,需要把 Try 阶段锁住的资源进行处理。 比如,把订单的状态设置为“已支付(Payed)”。 比如,扣除掉相应的库存。 比如,增加用户积分。 C(Cancel):在 Try 阶段,假如某个服务执行出错,比如积分服务执行出错了,那么服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进行回滚。 TCC 分布式事务框架只要感知到了任何一个服务的 Try 逻辑失败了,就会跟各个服务内的 TCC 分布式事务框架进行通信,然后调用各个服务的 Cancel 逻辑。也就是说,会执行各个服务的第二个 C 阶段, Cancel 阶段。 比如,订单的支付状态,先把状态修改为" closed "状态。 比如,冻结库存的字段, prepare _ remove _ stock 字段,将冻结的库存 2 清零。 比如,预增加积分的字段, prepare _ add _ credit 字段,将准备增加的积分 10 清零。 问:事务管理器宕掉了,怎么办? 做冗余,设置多个事务管理器,一个宕掉了,其他的还可以用。 问:怎么保证分布式系统的幂等性? 状态机制。版本号机制。 Redis 问:Redis 有哪些优势? 速度快,因为数据存在内存中。 支持丰富数据类型,支持 string、list、set 、sorted set、hash。 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行。 丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除。 单线程,单进程,采用 IO 多路复用技术。 问:Redis 的存储结构是怎样的? key-value 键值对。 问:Redis 支持哪些数据结构? string(字符串), hash(哈希), list(队列), set(集合)及 zset(sorted set 有序集合)。 问:Redis 的数据结构,有哪些应用场景? string:简单地 get / set 缓存。 hash:可以缓存用户资料。比如命令:hmset user1 name "lin" sex "male" age "25" ,缓存用户 user1 的资料,姓名为 lin ,性别为男,年龄 25。 list:可以做队列。往 list 队列里面 push 数据,然后再 pop 出来。 zset:可以用来做排行榜。 问:Redis 的数据结构,底层分别是由什么实现的? Redis 字符串,却不是 C 语言中的字符串(即以空字符 ’\0’ 结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string , SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。 Redi List ,底层是 ZipList ,不满足 ZipList 就使用双向链表。ZipList 是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。 问:Redis 怎么保证可靠性?Redis 的持久化方式有哪些?有哪些优缺点? 一个可靠安全的系统,肯定要考虑数据的可靠性,尤其对于内存为主的 Redis ,就要考虑一旦服务器挂掉,启动之后,如何恢复数据的问题,也就是说数据如何持久化的问题。 AOF 就是备份操作记录。AOF 由于是备份操作命令,备份快、恢复慢。 AOF 的优点:AOF 更好保证数据不会被丢失,最多只丢失一秒内的数据。另外重写操作保证了数据的有效性,即使日志文件过大也会进行重写。AOF 的日志文件的记录可读性非常的高。 AOF 的缺点:对于相同数量的数据集而言, AOF 文件通常要大于 RDB 文件。 RDB 就是备份所有数据,使用了快照。RDB 恢复数据比较快。 问:AOF 文件过大,怎么处理? 会进行 AOF 文件重写。 随着 AOF 文件越来越大,里面会有大部分是重复命令或者可以合并的命令。 重写的好处:减少 AOF 日志尺寸,减少内存占用,加快数据库恢复时间。 执行一个 AOF 文件重写操作,重写会创建一个当前 AOF 文件的体积优化版本。 问:讲一下 Redis 的事务。 先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令。如果想放弃这个事务,可以使用 DISCARD 命令。 问:Redis 事务无法回滚,那怎么处理? 问:怎么设置 Redis 的 key 过期时间? key 的的过期时间通过 EXPIRE key seconds 命令来设置数据的过期时间。返回 1 表明设置成功,返回 0 表明 key 不存在或者不能成功设置过期时间。 问:Redis 的过期策略有哪些? 惰性删除:当读/写一个已经过期的 key 时,会触发惰性删除策略,直接删除掉这个过期 key ,并按照 key 不存在去处理。惰性删除,对内存不太好,已经过期的 key 会占用太多的内存。 定期删除:每隔一段时间,就会对 Redis 进行检查,主动删除一批已过期的 key。 问:为什么 Redis 不使用定时删除? 定时删除,就是在设置 key 的过期时间的同时,创建一个定时器,让定时器在过期时间来临时,立即执行对 key 的删除操作。 定时删会占用 CPU ,影响服务器的响应时间和性能。 问:Redis 的内存回收机制都有哪些? 当前已用内存超过 maxmemory 限定时,会触发主动清理策略,也就是 Redis 的内存回收策略。 LRU 、TTL。 noeviction :默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息,此时 Redis 只响应读操作。 volatitle - lru :根据 LRU 算法删除设置了超时属性的键,知道腾出足够空间为止。如果没有可删除的键对象,回退到 noeviction 策略。 allkeys - lru :根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。 allkeys - random :随机删除所有键,知道腾出足够空间为止。 volatitle - random :随机删除过期键,知道腾出足够空间为止。 volatitle - ttl :根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。 问:手写一下 LRU 算法。 问:Redis 的搭建有哪些模式? 主从模式、哨兵模式、Cluster(集群)模式。最好是用集群模式。 问:你用过的 Redis 是多主多从的,还是一主多从的?集群用到了多少节点?用到了多少个哨兵? 集群模式。三主三从。 问:Redis 采用多主多从的集群模式,各个主节点的数据是否一致? 问:Redis 集群有哪些特性 master 和 slaver。主从复制。读写分离。哨兵模式。 问:Redis 是怎么进行水平扩容的? 问:Redis 集群数据分片的原理是什么? Redis 数据分片原理是哈希槽(hash slot)。 Redis 集群有 16384 个哈希槽。每一个 Redis 集群中的节点都承担一个哈希槽的子集。 哈希槽让在集群中添加和移除节点非常容易。例如,如果我想添加一个新节点 D ,我需要从节点 A 、B、C 移动一些哈希槽到节点 D。同样地,如果我想从集群中移除节点 A ,我只需要移动 A 的哈希槽到 B 和 C。当节点 A 变成空的以后,我就可以从集群中彻底删除它。因为从一个节点向另一个节点移动哈希槽并不需要停止操作,所以添加和移除节点,或者改变节点持有的哈希槽百分比,都不需要任何停机时间(downtime)。 问:讲一下一致性 Hash 算法。 一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环, 我们对 key 进行哈希计算,使用哈希后的结果对 2 ^ 32 取模,hash 环上必定有一个点与这个整数对应。依此确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。 一致性 Hash 算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。 比如,集群有四个节点 Node A 、B 、C 、D ,增加一台节点 Node X。Node X 的位置在 Node B 到 Node C 直接,那么受到影响的仅仅是 Node B 到 Node X 间的数据,它们要重新落到 Node X 上。 所以一致性哈希算法对于容错性和扩展性有非常好的支持。 问:为什么 Redis Cluster 分片不使用 Redis 一致性 Hash 算法? 一致性哈希算法也有一个严重的问题,就是数据倾斜。 如果在分片的集群中,节点太少,并且分布不均,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。 问:集群的拓扑结构有没有了解过?集群是怎么连接的? 无中心结构。Redis-Cluster 采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。 问:讲一下 Redis 主从复制的过程。 从机发送 SYNC(同步)命令,主机接收后会执行 BGSAVE(异步保存)命令备份数据。 主机备份后,就会向从机发送备份文件。主机之后还会发送缓冲区内的写命令给从机。 当缓冲区命令发送完成后,主机执行一条写命令,就会往从机发送同步写入命令。 问:讲一下 Redis 哨兵机制。 下面是 Redis 官方文档对于哨兵功能的描述: 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。 自动故障转移(Automatic Failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。 配置提供者(Configuration Provider):客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。 通知(Notification):哨兵可以将故障转移的结果发送给客户端。 问:讲一下布隆过滤器。 布隆过滤器的主要是由一个很长的二进制向量和若干个(k 个)散列映射函数组成。因为每个元数据的存储信息值固定,而且总的二进制向量固定。所以在内存占用和查询时间上都远远超过一般的算法。当然存在一定的不准确率(可以控制)和不容易删除样本数据。 布隆过滤器的优点:大批量数据去重,特别的占用内存。但是用布隆过滤器(Bloom Filter)会非常的省内存。 布隆过滤器的特点:当布隆过滤器说某个值存在时,那可能就不存在,如果说某个值不存在时,那肯定就是不存在了。 布隆过滤器的应用场景:新闻推送(不重复推送)。解决缓存穿透的问题。 缓存 问:缓存雪崩是什么? 如果缓存数据设置的过期时间是相同的,并且 Redis 恰好将这部分数据全部删光了。这就会导致在这段时间内,这些缓存同时失效,全部请求到数据库中。这就是缓存雪崩。 问:怎么解决缓存雪崩? 解决方法:在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。 问:缓存穿透是什么? 缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。 问:怎么解决缓存穿透? 问:什么是缓存与数据库双写一致问题? 问:如何保证缓存与数据库的一致性? 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 先删除缓存,再更新数据库。 问:为什么是先删除缓存,而不是先更新缓存? 问:先更新数据库,再删除缓存,会有什么问题? 先更新数据库,再删除缓存。可能出现以下情况: 如果更新完数据库, Java 服务提交了事务,然后挂掉了,那 Redis 还是会执行,这样也会不一致。 如果更新数据库成功,删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。 先删除缓存,再更新数据库。 如果删除缓存失败,那就不更新数据库,缓存和数据库的数据都是旧数据,数据是一致的。 如果删除缓存成功,而数据库更新失败了,那么数据库中是旧数据,缓存中是空的,数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。 问:先删除缓存,在写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致,怎么处理? 分布式锁 问:Redis 如何实现分布式锁? 使用 set key value ex nx 命令。 当 key 不存在时,将 key 的值设为 value ,返回 1。若给定的 key 已经存在,则 setnx 不做任何动作,返回 0。 当 setnx 返回 1 时,表示获取锁,做完操作以后 del key ,表示释放锁,如果 setnx 返回 0 表示获取锁失败。 详细的命令如下: set key value [EX seconds] [PX milliseconds] [NX|XX]EX seconds:设置失效时长,单位秒PX milliseconds:设置失效时长,单位毫秒NX:key不存在时设置value,成功返回OK,失败返回(nil)XX:key存在时设置value,成功返回OK,失败返回(nil)。 复制代码 示例如下: set name fenglin ex 100 nx 复制代码 问:为什么不先 set nx ,然后再使用 expire 设置超时时间? 我们需要保证 setnx 命令和 expire 命令以原子的方式执行,否则如果客户端执行 setnx 获得锁后,这时客户端宕机了,那么这把锁没有设置过期时间,导致其他客户端永远无法获得锁了。 问:使用 Redis 分布式锁, key 和 value 分别设置成什么? value 可以使用 json 格式的字符串,示例: { "count":1, "expireAt":147506817232, "jvmPid":22224, "mac":"28-D2-44-0E-0D-9A", "threadId":14} 复制代码 问:Redis 实现的分布式锁,如果某个系统获取锁后,宕机了怎么办? 系统模块宕机的话,可以通过设置过期时间(就是设置缓存失效时间)解决。系统宕机时锁阻塞,过期后锁释放。 问:设置缓存失效时间,那如果前一个线程把这个锁给删除了呢? 问:如果加锁和解锁之间的业务逻辑执行的时间比较长,超过了锁过期的时间,执行完了,又删除了锁,就会把别人的锁给删了。怎么办? 这两个属于锁超时的问题。 可以将锁的 value 设置为 Json 字符串,在其中加入线程的 id 或者请求的 id ,在删除之前, get 一下这个 key ,判断 key 对应的 value 是不是当前线程的。只有是当前线程获取的锁,当前线程才可以删除。 问:Redis 分布式锁,怎么保证可重入性? 可以将锁的 value 设置为 Json 字符串,在其中加入线程的 id 和 count 变量。 当 count 变量的值为 0 时,表示当前分布式锁没有被线程占用。 如果 count 变量的值大于 0 ,线程 id 不是当前线程,表示当前分布式锁已经被其他线程占用。 如果 count 变量的值大于 0 ,线程 id 是当前线程的 id ,表示当前线程已经拿到了锁,不必阻塞,可以直接重入,并将 count 变量的值加一即可。 这种思路,其实就是参考了 ReentrantLock 可重入锁的机制。 问:Redis 做分布式锁, Redis 做了主从,如果设置锁之后,主机在传输到从机的时候挂掉了,从机还没有加锁信息,如何处理? 可以使用开源框架 Redisson ,采用了 redLock。 问:讲一下 Redis 的 redLock。 问:Zookeeper 是怎么实现分布式锁的? 分布式锁:基于 Zookeeper 一致性文件系统,实现锁服务。锁服务分为保存独占及时序控制两类。 保存独占:将 Zookeeper 上的一个 znode 看作是一把锁,通过 createznode 的方式来实现。所有客户端都去创建 / distribute _ lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除自己创建的 distribute _ lock 节点就释放锁。 时序控制:基于/ distribute _ lock 锁,所有客户端在它下面创建临时顺序编号目录节点,和选 master 一样,编号最小的获得锁,用完删除,依次方便。 更详细的回答如下: 其实基于 Zookeeper ,就是使用它的临时有序节点来实现的分布式锁。 原理就是:当某客户端要进行逻辑的加锁时,就在 Zookeeper 上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用 exist() 方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。 当释放锁的时候,只需将这个临时节点删除即可。 Zookeeper 问:Zookeeper 的原理是什么? 问:Zookeeper 是怎么保证一致性的? zab 协议。 zab 协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后, zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。 问:Zookeeper 有哪些应用场景? Zookeeper 可以作为服务协调的注册中心。还可以做分布式锁(如果没有用过分布式锁就不要说)。 问:Zookeeper 为什么能做注册中心? Zookeeper 的数据模型是树型结构,由很多数据节点组成, zk 将全量数据存储在内存中,可谓是高性能,而且支持集群,可谓高可用。另外支持事件监听(watch 命令)。 Zookeeper 可以作为一个数据发布/订阅系统。 问:Zookeeper 的节点有哪些类型?有什么区别? 临时节点,永久节点。更加细分就是临时有序节点、临时无序节点、永久有序节点、永久无序节点。 临时节点:当创建临时节点的程序停掉之后,这个临时节点就会消失,存储的数据也没有了。 问:Zookeeper 做为注册中心,主要存储哪些数据?存储在哪里? IP、端口、还有心跳机制。数据存储在 Zookeeper 的节点上面。 问:心跳机制有什么用? 问:Zookeeper 的广播模式有什么缺陷? 广播风暴。 问:讲一下 Zookeeper 的读写机制。 Leader 主机负责读和写。 Follower 负责读,并将写操作转发给 Leader。Follower 还参与 Leader 选举投票,参与事务请求 Proposal 投票。 Observer 充当观察者的角色。Observer 和 Follower 的唯一区别在于:Observer 不参与任何投票。 问:讲一下 Zookeeper 的选举机制。 Leader 不可用时,会重新选举 Leader。超过半数的 Follower 选举投票即可,Observer 不参与投票。 问:你们的 Zookeeper 集群配置了几个节点? 3 个节点。注意:Zookeeper 集群节点,最好是奇数个的。 集群中的 Zookeeper 节点需要超过半数,整个集群对外才可用。 这里所谓的整个集群对外才可用,是指整个集群还能选出一个 Leader 来, Zookeeper 默认采用 quorums 来支持 Leader 的选举。 如果有 2 个 Zookeeper,那么只要有 1 个死了 Zookeeper 就不能用了,因为 1 没有过半,所以 2 个 Zookeeper 的死亡容忍度为 0 ;同理,要是有 3 个 Zookeeper,一个死了,还剩下 2 个正常的,过半了,所以 3 个 Zookeeper 的容忍度为 1 ;同理你多列举几个:2 -> 0 ; 3 -> 1 ; 4 -> 1 ; 5 -> 2 ; 6 -> 2 会发现一个规律, 2n 和 2n - 1 的容忍度是一样的,都是 n - 1 ,所以为了更加高效,何必增加那一个不必要的 Zookeeper 呢。 问:Zookeeper 的集群节点,如果不是奇数可能会出现什么问题? 可能会出现脑裂。 假死:由于心跳超时(网络原因导致的)认为 master 死了,但其实 master 还存活着。 脑裂:由于假死会发起新的 master 选举,选举出一个新的 master ,但旧的 master 网络又通了,导致出现了两个 master ,有的客户端连接到老的 master 有的客户端链接到新的 master。 消息队列 问:为什么使用消息队列?消息队列有什么优点和缺点?Kafka 、ActiveMQ 、RabbitMq 、RocketMQ 都有什么优点和缺点? 消息队列解耦,削峰,限流。 问:如何保证消息队列的高可用?(多副本) 问:如何保证消息不被重复消费?(如何保证消息消费的幂等性) 问:如何保证消息的可靠性传输?(如何处理消息丢失的问题) 问:如何保证消息的顺序性? 问:如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? 问:如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。 答案 Kafka 问:讲一下 Kafka。 Kafka 的简单理解 问:Kafka 相对其他消息队列,有什么特点? 持久化:Kafka 的持久化能力比较好,通过磁盘持久化。而 RabbitMQ 是通过内存持久化的。 吞吐量:Rocket 的并发量非常高。 消息处理:RabbitMQ 的消息不支持批量处理,而 RocketMQ 和 Kafka 支持批量处理。 高可用:RabbitMQ 采用主从模式。Kafka 也是主从模式,通过 Zookeeper 管理,选举 Leader ,还有 Replication 副本。 事务:RocketMQ 支持事务,而 Kafka 和 RabbitMQ 不支持。 问:Kafka 有哪些模式? 如果一个生产者或者多个生产者产生的消息能够被多个消费者同时消费的情况,这样的消息队列称为"发布订阅模式"的消息队列。 问:Kafka 作为消息队列,有哪些优势? 分布式的消息系统。 高吞吐量。即使存储了许多 TB 的消息,它也保持稳定的性能。 数据保留在磁盘上,因此它是持久的。 问:Kafka 为什么处理速度会很快?kafka 的吞吐量为什么高? 零拷贝:Kafka 实现了"零拷贝"原理来快速移动数据,避免了内核之间的切换。 消息压缩、分批发送:Kafka 可以将数据记录分批发送,从生产者到文件系统(Kafka 主题日志)到消费者,可以端到端的查看这些批次的数据。 批处理能够进行更有效的数据压缩并减少 I / O 延迟。 顺序读写:Kafka 采取顺序写入磁盘的方式,避免了随机磁盘寻址的浪费。 问:讲一下 Kafka 中的零拷贝。 数据的拷贝从内存拷贝到 kafka 服务进程那块,又拷贝到 socket 缓存那块,整个过程耗费的时间比较高, kafka 利用了 Linux 的 sendFile 技术(NIO),省去了进程切换和一次数据拷贝,让性能变得更好。 问:Kafka 的偏移量是什么? 消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置。等到下次消费时,他会接着上次位置继续消费 问:Kafka 的生产者,是如何发送消息的? 生产者的消息是先被写入分区中的缓冲区中,然后分批次发送给 Kafka Broker。 生产者的消息发送机制,有同步发送和异步发送。 同步发送消息都有个问题,那就是同一时间只能有一个消息在发送,这会造成许多消息。 无法直接发送,造成消息滞后,无法发挥效益最大化。 异步发送消息的同时能够对异常情况进行处理,生产者提供了 Callback 回调。 问:Kafka 生产者发送消息,有哪些分区策略? Kafka 的分区策略指的就是将生产者发送到哪个分区的算法。有顺序轮询、随机轮询、key - ordering 策略。 key - ordering 策略:Kafka 中每条消息都会有自己的 key ,一旦消息被定义了 Key ,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略。 问:Kafka 为什么要分区? 实现负载均衡和水平扩展。Kafka 可以将主题(Topic)划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个分区中,只要如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡和水平扩展。另外,多个订阅者可以从一个或者多个分区中同时消费数据,以支撑海量数据处理能力。 问:Kafka 是如何在 Broker 间分配分区的? 在 broker 间平均分布分区副本。 假设有 6 个 broker ,打算创建一个包含 10 个分区的 Topic ,复制系数为 3 ,那么 Kafka 就会有 30 个分区副本,它可以被分配给这 6 个 broker ,这样的话,每个 broker 可以有 5 个副本。 要确保每个分区的每个副本分布在不同的 broker 上面: 假设 Leader 分区 0 会在 broker1 上面, Leader 分区 1 会在 broker2 上面, Leder 分区 2 会在 broker3 上面。 接下来会分配跟随者副本。如果分区 0 的第一个 Follower 在 broker2 上面,第二个 Follower 在 broker3 上面。分区 1 的第一个 Follower 在 broker3 上面,第二个 Follower 在 broker4 上面。 问:Kafka 如何保证消息的顺序性? Kafka 可以保证同一个分区里的消息是有序的。也就是说消息发送到一个 Partition 是有顺序的。 问:Kafka 的消费者群组 Consumer Group 订阅了某个 Topic ,假如这个 Topic 接收到消息并推送,那整个消费者群组能收到消息吗? Kafka 官网中有这样一句" Consumers label themselves with a consumer group name , and each record published to a topic is delivered to one consumer instance within each subscribing consumer group . " 表示推送到 topic 上的 record ,会被传递到已订阅的消费者群组里面的一个消费者实例。 问:如何提高 Kafka 的消费速度? 问:Kafka 出现消息积压,有哪些原因?怎么解决? 出现消息积压,可能是因为消费的速度太慢。 扩容消费者。之所以消费延迟大,就是消费者处理能力有限,可以增加消费者的数量。 扩大分区。一个分区只能被消费者群组中的一个消费者消费。消费者扩大,分区最好多随之扩大。 问:Kafka 消息消费者宕机了,怎么确认有没有收到消息? ACK 机制,如果接收方收到消息后,会返回一个确认字符。 问:讲一下 Kafka 的 ACK 机制。 acks 参数指定了要有多少个分区副本接收消息,生产者才认为消息是写入成功的。此参数对消息丢失的影响较大。 如果 acks = 0 ,就表示生产者也不知道自己产生的消息是否被服务器接收了,它才知道它写成功了。如果发送的途中产生了错误,生产者也不知道,它也比较懵逼,因为没有返回任何消息。这就类似于 UDP 的运输层协议,只管发,服务器接受不接受它也不关心。 如果 acks = 1 ,只要集群的 Leader 接收到消息,就会给生产者返回一条消息,告诉它写入成功。如果发送途中造成了网络异常或者 Leader 还没选举出来等其他情况导致消息写入失败,生产者会受到错误消息,这时候生产者往往会再次重发数据。因为消息的发送也分为 同步 和 异步, Kafka 为了保证消息的高效传输会决定是同步发送还是异步发送。如果让客户端等待服务器的响应(通过调用 Future 中的 get() 方法),显然会增加延迟,如果客户端使用回调,就会解决这个问题。 如果 acks = all ,这种情况下是只有当所有参与复制的节点都收到消息时,生产者才会接收到一个来自服务器的消息。不过,它的延迟比 acks = 1 时更高,因为我们要等待不只一个服务器节点接收消息。 问:Kafka 如何避免消息丢失? 1、生产者丢失消息的情况 生产者(Producer) 调用 send 方法发送消息之后,消息可能因为网络问题并没有发送过去。 所以,我们不能默认在调用 send 方法发送消息之后消息消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。可以采用为其添加回调函数的形式,获取回调结果。 如果消息发送失败的话,我们检查失败的原因之后重新发送即可!可以设置 Producer 的 retries(重试次数)为一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。 设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。 2、消费者丢失消息的情况 当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。手动关闭闭自动提交 offset ,每次在真正消费完消息之后之后再自己手动提交 offset 。 3 、Kafka 丢失消息 a、假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。因此可以设置 ack = all。 b、设置 replication . factor >= 3 。为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 replication . factor >= 3。这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。 问:Kafka 怎么保证可靠性? 多副本以及 ISR 机制。 在 Kafka 中主要通过 ISR 机制来保证消息的可靠性。 ISR(in sync replica):是 Kafka 动态维护的一组同步副本,在 ISR 中有成员存活时,只有这个组的成员才可以成为 leader ,内部保存的为每次提交信息时必须同步的副本(acks = all 时),每当 leader 挂掉时,在 ISR 集合中选举出一个 follower 作为 leader 提供服务,当 ISR 中的副本被认为坏掉的时候,会被踢出 ISR ,当重新跟上 leader 的消息数据时,重新进入 ISR。 问:什么是 HW ? HW(high watermark):副本的高水印值, replica 中 leader 副本和 follower 副本都会有这个值,通过它可以得知副本中已提交或已备份消息的范围, leader 副本中的 HW ,决定了消费者能消费的最新消息能到哪个 offset。 问:什么是 LEO ? LEO(log end offset):日志末端位移,代表日志文件中下一条待写入消息的 offset ,这个 offset 上实际是没有消息的。不管是 leader 副本还是 follower 副本,都有这个值。 问:Kafka 怎么保证一致性?(存疑) 一致性定义:若某条消息对 client 可见,那么即使 Leader 挂了,在新 Leader 上数据依然可以被读到。 HW - HighWaterMark : client 可以从 Leader 读到的最大 msg offset ,即对外可见的最大 offset , HW = max(replica . offset) 对于 Leader 新收到的 msg , client 不能立刻消费, Leader 会等待该消息被所有 ISR 中的 replica 同步后,更新 HW ,此时该消息才能被 client 消费,这样就保证了如果 Leader fail ,该消息仍然可以从新选举的 Leader 中获取。 对于来自内部 Broker 的读取请求,没有 HW 的限制。同时, Follower 也会维护一份自己的 HW , Folloer . HW = min(Leader . HW , Follower . offset). 问:Kafka 怎么处理重复消息?怎么避免重复消费? 偏移量 offset :消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置。等到下次消费时,他会接着上次位置继续消费。 一般情况下, Kafka 重复消费都是由于未正常提交 offset 造成的,比如网络异常,消费者宕机之类的。 使用的是 spring-Kafka ,所以把 Kafka 消费者的配置 enable.auto. commit 设为 false ,禁止 Kafka 自动提交 offset ,从而使用 spring-Kafka 提供的 offset 提交策略。 sprin-Kafka 中的 offset 提交策略可以保证一批消息数据没有完成消费的情况下,也能提交 offset ,从而避免了提交失败而导致永远重复消费的问题。 问:怎么避免重复消费? 将消息的唯一标识保存起来,每次消费时判断是否处理过即可。 问:如何保证消息不被重复消费?(如何保证消息消费的幂等性) 怎么保证消息队列消费的幂等性?其实还是得结合业务来思考,有几个思路: 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了, update 一下好吧。 比如你是写 Redis ,那没问题了,反正每次都是 set ,天然幂等性。 如果是复杂一点的业务,那么每条消息加一个全局唯一的 id ,类似订单 id 之类的东西,然后消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗? 如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。 问:Kafka 消息是采用 pull 模式,还是 push 模式? pull 模式。 问:pull 模式和 push 模式,各有哪些特点? pull 模式,准确性?可以较大保证消费者能获取到消息。 push 模式,即时性?可以在 broker 获取消息后马上送达消费者。 问:Kafka 是如何存储消息的? Kafka 使用日志文件的方式来保存生产者和发送者的消息,每条消息都有一个 offset 值来表示它在分区中的偏移量。 Kafka 中存储的一般都是海量的消息数据,为了避免日志文件过大, 一个分片并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录。 数据存储设计的特点在于以下几点: Kafka 把主题中一个分区划分成多个分段的小文件段,通过多个小文件段,就容易根据偏移量查找消息、定期清除和删除已经消费完成的数据文件,减少磁盘容量的占用; 采用稀疏索引存储的方式构建日志的偏移量索引文件,并将其映射至内存中,提高查找消息的效率,同时减少磁盘 IO 操作; Kafka 将消息追加的操作逻辑变成为日志数据文件的顺序写入,极大的提高了磁盘 IO 的性能; 问:讲一下 Kafka 集群的 Leader 选举机制。 Kafka 在 Zookeeper 上针对每个 Topic 都维护了一个 ISR(in - sync replica ---已同步的副本)的集合,集合的增减 Kafka 都会更新该记录。如果某分区的 Leader 不可用, Kafka 就从 ISR 集合中选择一个副本作为新的 Leader。 分库分表 问:数据库如何处理海量数据? 分库分表,主从架构,读写分离。 问:数据库分库分表,何时分?怎么分? 水平分库/分表,垂直分库/分表。 水平分库/表,各个库和表的结构一模一样。 垂直分库/表,各个库和表的结构不一样。 问:读写分离怎么做? 主机负责写,从机负责读。 系统设计 1、分布式、高并发场景 遇到高并发场景,可以使用 Redis 缓存、Redis 限流、MQ 异步、MQ 削峰等。 问:在实践中,遇到过哪些并发的业务场景? 秒杀。比如抢商品,抢红包。 2、秒杀 问:如何设计一个秒杀/抢券系统? 可以通过队列配合异步处理实现秒杀。 使用 redis 的 list ,将商品 push 进队列, pop 出队列。 异步操作不会阻塞,不会消耗太多时间。 问:如何提高抢券系统的性能? 使用多个 list。 使用多线程从队列中拉取数据。 集群提高可用性。 MQ 异步处理,削峰。 问:秒杀怎么避免少卖或超卖? redis 是单进程单线程的,操作具有原子性,不会导致少卖或者超卖。另外,也可以设置一个版本号 version ,乐观锁机制。 问:考勤打卡,假如高峰期有几万人同时打卡,那么怎么应对这种高并发? 使用 Redis 缓存。员工点击签到,可以在缓存中 set 状态。将工号作为 key ,打卡状态作为 value ,打卡成功为 01 ,未打卡或者打卡失败为 00 ,然后再将数据异步地写入到数据库里面就可以了。 问:如何应对高峰期的超高并发量? Redis 限流。Redis 可以用计数器限流。使用 INCR 命令,每次都加一,处理完业务逻辑就减一。然后设置一个最大值,当达到最大值后就直接返回,不处理后续的逻辑。 Redis 还可以用令牌桶限流。使用 Redis 队列,每十个数据中 push 一个令牌桶,每个请求进入后会先从队列中 pop 数据,如果是令牌就可以通行,不是令牌就直接返回。 3、短链接 问:如何将长链接转换成短链接,并发送短信? 短 URL 从生成到使用分为以下几步: 有一个服务,将要发送给你的长 URL 对应到一个短 URL 上.例如 www.baidu.com -> www.t.cn/1。 把短 url 拼接到短信等的内容上发送。 用户点击短 URL ,浏览器用 301 / 302 进行重定向,访问到对应的长 URL。 展示对应的内容。 问:长链接和短链接如何互相转换? 思路是建立一个发号器。每次有一个新的长 URL 进来,我们就增加一。并且将新的数值返回.第一个来的 url 返回"www.x.cn/0",第二个返回"www.x.cn/1". 问:长链接和短链接的对应关系如何存储? 如果数据量小且 QPS 低,直接使用数据库的自增主键就可以实现。 还可以将最近/最热门的对应关系存储在 K-V 数据库中,这样子可以节省空间的同时,加快响应速度。 系统架构与设计 问:如何提高系统的并发能力? 使用分布式系统。 部署多台服务器,并做负载均衡。 使用缓存(Redis)集群。 数据库分库分表 + 读写分离。 引入消息中间件集群。 问:设计一个红包系统,需要考虑哪些问题,如何解决?(本质上也是秒杀系统) 问:如果让你设计一个消息队列,你会怎么设计? 项目经验及数据量 问:这个项目的亮点、难点在哪里? 问:如果这个模块挂掉了怎么办? 问:你们的项目有多少台机器? 问:你们的项目有多少个实例? 4 个实例。 问:你们的系统 QPS(TPS)是多少? QPS ,每秒查询量。QPS 为几百/几千,已经算是比较高的了。 TPS ,每秒处理事务数。TPS 即每秒处理事务数,包括:”用户请求服务器”、”服务器自己的内部处理”、”服务器返回给用户”,这三个过程,每秒能够完成 N 个这三个过程, TPS 也就是 3。 问:一个接口,多少秒响应才正常? 快的话几毫秒。慢的话 1-2 秒。异常情况可能会 10 几秒;最好保证 99 %以上的请求是正常的。 问:这个接口的请求时间,大概多久?主要耗时在哪里? 问:系统的数据量多少?有没有分库分表? 正常情况下,几百万的数据量没有必要分库分表。只有超过几千万才需要分库分表。 问:插入/更新一条数据要多久?更新十万/百万条数据要多久? 插入/更新一条数据一般要几毫秒;更新十万条数据最好在 10 秒以内; 百万条数据最好在 50-100 秒以内。

优秀的个人博客,低调大师

面试题解答:Spring Lifecycle 和 SmartLifecycle 有何区别?

当我们想在 Spring 容器启动或者关闭的时候,做一些初始化操作或者对象销毁操作,我们可以怎么做? 注意我这里说的是容器启动或者关闭的时候,不是某一个 Bean 初始化或者销毁的时候~ 1. Lifecycle 对于上面提到的问题,如果小伙伴们稍微研究过 Spring,应该是了解其里边有一个 Lifecycle 接口,通过这个接口,我们可以在 Spring 容器启动或者关闭的时候,做一些自己需要的事情。 我们先来看下 Lifecycle 接口: public interface Lifecycle { void start(); void stop(); boolean isRunning(); } 这个接口一共就三个方法: start:启动组件,该方法在执行之前,先调用 isRunning 方法判断组件是否已经启动了,如果已经启动了,就不重复启动了。 stop:停止组件,该方法在执行之前,先调用 isRunning 方法判断组件是否已经停止运行了,如果已经停止运行了,就不再重复停止了。 isRunning:这个是返回组件是否已经处于运行状态了,对于容器来说,只有当容器中的所有适用组件都处于运行状态时,这个方法返回 true,否则返回 false。 如果我们想自定义一个 Lifecycle,方式如下: @Component public class MyLifeCycle implements Lifecycle { private volatile boolean running; @Override public void start() { running = true; System.out.println("start"); } @Override public void stop() { running = false; System.out.println("stop"); } @Override public boolean isRunning() { return running; } } 需要自定义一个 running 变量,该变量用来描述当前组件是否处于运行/停止状态,因为系统在调用 start 和 stop 方法的时候,都会先调用 isRunning 方法,用以确认是否需要真的调用 start/stop 方法。 接下来创建配置类,扫描上述组件: @Configuration @ComponentScan public class JavaConfig { } 最后我们启动容器: public class Demo { public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class); ctx.start(); ctx.stop(); } } 启动之后,我们就可以看到控制台打印出来的信息: [外链图片转存中...(img-jWhjCQzV-1697683960574)] 可以看到,在容器启动和停止的时候,相应的方法会被触发。 不过 Lifecycle 有一个问题,就是必须显式的调用 start 或者 stop 方法才会触发 Lifecycle 中的方法。当然,如果你没有调用 stop 方法,而是调用了 close 方法,那么在 close 方法内部也会触发 stop 方法。 如果我们想要 start 方法被自动触发呢?那就得一个更加智能的 Lifecycle 了--- SmartLifecycle。 2. SmartLifecycle 相比于 LifeCycle,SmartLifecycle 中多了几个方法: public interface SmartLifecycle extends Lifecycle, Phased { int DEFAULT_PHASE = Integer.MAX_VALUE; default boolean isAutoStartup() { return true; } default void stop(Runnable callback) { stop(); callback.run(); } @Override default int getPhase() { return DEFAULT_PHASE; } } 大家看一下,这里首先多了一个 isAutoStartup 方法,这个方法就表示是否自动执行 startup 方法,这个方法返回 true,则 startup 方法会被自动触发,这个方法要是返回 false,则 startup 方法就不会被自动触发(那么效果就等同于 LifeCycle 了)。 这里多了一个重载的 stop 方法,这个重载的 stop 方法会传入一个线程对象,然后在 stop 中触发,这个 callback 回调是为了告诉容器,我们销毁组件的工作已经完成了。如果使用了 SmartLifecycle,那么 Lifecycle 中的 stop 方法就不会被直接触发了,除非我们在 SmartLifecycle#stop 中手动去触发 Lifecycle#stop 方法。 另外这里还有一个 getPhase 方法,这个当存在多个 SmartLifecycle 实例的时候,我们需要为其执行顺序排序,getPhase 方法就是返回执行顺序,数字越小,优先级越高,默认优先级最小。 我们来写一个 SmartLifecycle 的案例来试下: @Component public class MyLifeCycle implements SmartLifecycle { private volatile boolean running; @Override public void start() { running = true; System.out.println("start"); } @Override public void stop() { running = false; System.out.println("stop"); } @Override public boolean isRunning() { return running; } @Override public boolean isAutoStartup() { return SmartLifecycle.super.isAutoStartup(); } @Override public void stop(Runnable callback) { stop(); callback.run(); } @Override public int getPhase() { return 0; } } 3. 原理分析 那么 Lifecycle 到底是如何被触发的呢?我们来分析一下源码。 由于系统中可能存在多个 Lifecycle,因此这多个 Lifecycle 需要一个统一的管理,这个管理者就是 LifecycleProcessor,这也是一个接口,这个接口中只有两个方法: public interface LifecycleProcessor extends Lifecycle { void onRefresh(); void onClose(); } onRefresh:这个是在上下文刷新的时候被触发,例如在容器启动的时候这个方法被触发。 onClose:这个是在上下文关闭的时候被触发,例如在容器停止运行的时候这个方法被触发。 LifecycleProcessor 只有一个实现类 DefaultLifecycleProcessor,所以很好分析,这个 DefaultLifecycleProcessor 中,重写了上面的 onRefresh 和 onClose 两个方法: @Override public void onRefresh() { startBeans(true); this.running = true; } @Override public void onClose() { stopBeans(); this.running = false; } 3.1 start 小伙伴们看到,在容器启动的时候,这里会去调用 startBeans 方法,在这个方法中就会触发 Lifecycle#start 方法: private void startBeans(boolean autoStartupOnly) { Map<string, lifecycle> lifecycleBeans = getLifecycleBeans(); Map<integer, lifecyclegroup> phases = new TreeMap&lt;&gt;(); lifecycleBeans.forEach((beanName, bean) -&gt; { if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle &amp;&amp; smartLifecycle.isAutoStartup())) { int phase = getPhase(bean); phases.computeIfAbsent( phase, p -&gt; new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) ).add(beanName, bean); } }); if (!phases.isEmpty()) { phases.values().forEach(LifecycleGroup::start); } } 在这个方法中,首先调用 getLifecycleBeans 方法,这个方法的作用是去 Spring 容器中查找所有 Lifecycle 类型的 Bean,并把查找结果封装到一个 Map 集合中返回。 接下来就去遍历这个 Map,遍历的时候由于 autoStartupOnly 变量传进来的时候是 true,取反之后就是 false 了,所以就会去判断这个 Bean 是否为 SmartLifecycle 类型,如果是该类型并且 isAutoStartup 方法返回 true,就表示要自动执行 start 方法。 如果确定是 SmartLifecycle 类型的 Bean,那么就调用 getPhase 方法获取其 phase,这个表示执行的优先级,然后将之存入到 phases 集合中,存储的时候,phase 是 key,value 则是一个 LifecycleGroup,phases 是一个 TreeMap,小伙伴们知道,TreeMap 是有序的,也就是存入进去的数据,会自动按照 phase 进行排序。LifecycleGroup 是将 phase 相同的 SmartLifecycle 分组之后的对象。 > 经过上面的分析,相信大家已经明白了为什么直接实现 Lifecycle 接口,就一定需要手动调用 start 方法(因为上面 if 中的条件不满足)。 最后就是遍历 phases,调用每一个 LifecycleGroup 中的 start 方法。 public void start() { if (this.members.isEmpty()) { return; } Collections.sort(this.members); for (LifecycleGroupMember member : this.members) { doStart(this.lifecycleBeans, member.name, this.autoStartupOnly); } } private void doStart(Map<string, ? extends lifecycle> lifecycleBeans, String beanName, boolean autoStartupOnly) { Lifecycle bean = lifecycleBeans.remove(beanName); if (bean != null &amp;&amp; bean != this) { String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName); for (String dependency : dependenciesForBean) { doStart(lifecycleBeans, dependency, autoStartupOnly); } if (!bean.isRunning() &amp;&amp; (!autoStartupOnly || !(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())) { bean.start(); } } } 在 doStart 方法中,从集合中取出来 Lifecycle,然后查找一下该 Lifecycle 是否有依赖的 Bean,如果有,就继续递归调用 doStart 方法。否则,在 isRunning 返回 false(即该组件还没有运行),且 bean 不是 SmartLifecycle 类型(那就只能是 Lifecycle 类型)或者 bean 是 SmartLifecycle 类型且 isAutoStartup 方法为 true 的情况下,调用 bean 的 start 方法。 > 小伙伴们注意,上面的分析是从 onRefresh 方法开始的,该方法中调用 startBeans 的时候,传入的参数是 true,也就是上面这个判断里边 autoStartupOnly 为 true,取反之后这个条件就不满足了,如果是我们手动调用 start 方法的话,这个参数默认传入的是 false,取反之后上面这个条件就满足了,也就是无论是手动还是自动,最终都是在这个地方触发 start 方法的。 3.2 stop 再来看 stop 方法的逻辑。从 onClose 方法开始,也是先调用 stopBeans 方法: private void stopBeans() { Map<string, lifecycle> lifecycleBeans = getLifecycleBeans(); Map<integer, lifecyclegroup> phases = new HashMap&lt;&gt;(); lifecycleBeans.forEach((beanName, bean) -&gt; { int shutdownPhase = getPhase(bean); LifecycleGroup group = phases.get(shutdownPhase); if (group == null) { group = new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false); phases.put(shutdownPhase, group); } group.add(beanName, bean); }); if (!phases.isEmpty()) { List<integer> keys = new ArrayList&lt;&gt;(phases.keySet()); keys.sort(Collections.reverseOrder()); for (Integer key : keys) { phases.get(key).stop(); } } } 这块的逻辑跟 start 差不多,就是排序的方案有一些差别。这里用了 HashMap,没有用 TreeMap,然后在具体调用的时候,再去给 key 排序的。 这里调用到的也是 LifecycleGroup 的 stop 方法,我们来看下: public void stop() { if (this.members.isEmpty()) { return; } this.members.sort(Collections.reverseOrder()); CountDownLatch latch = new CountDownLatch(this.smartMemberCount); Set<string> countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet&lt;&gt;()); Set<string> lifecycleBeanNames = new HashSet&lt;&gt;(this.lifecycleBeans.keySet()); for (LifecycleGroupMember member : this.members) { if (lifecycleBeanNames.contains(member.name)) { doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames); } else if (member.bean instanceof SmartLifecycle) { // Already removed: must have been a dependent bean from another phase latch.countDown(); } } try { latch.await(this.timeout, TimeUnit.MILLISECONDS); if (latch.getCount() &gt; 0 &amp;&amp; !countDownBeanNames.isEmpty() &amp;&amp; logger.isInfoEnabled()) { logger.info("Failed to shut down " + countDownBeanNames.size() + " bean" + (countDownBeanNames.size() &gt; 1 ? "s" : "") + " with phase value " + this.phase + " within timeout of " + this.timeout + "ms: " + countDownBeanNames); } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } 大家看一下,这里的 doStop 方法,最终就会触发到 Lifecycle 的 stop,这个里边的代码简单,我们就不去细看了。需要提醒大家的时候,这里使用到了这样一个计数器,初始值就是 members 的数量,每当调用一个 member 的 stop 方法之后,这个计数器减一,这样,到下面调用 await 的时候,就刚刚好不用等。 await 方法的等待时间是 this.timeout,这个属性默认值是 30s,也就是如果 stop 方法在子线程中执行,那么执行时间不能超过 30s,否则就会抛出异常。 如果我们想要自定义这个超时时间,可以自己在 Spring 容器中提供如下 Bean: @Configuration @ComponentScan public class JavaConfig { @Bean DefaultLifecycleProcessor lifecycleProcessor() { DefaultLifecycleProcessor processor = new DefaultLifecycleProcessor(); processor.setTimeoutPerShutdownPhase(2000); return processor; } } 上面这个案例中设置了超时时间为 2s。 好啦,这就是关于 Lifecycle 的整体触发流程。 接下来我们来看下自动触发和手动触发分别是在哪里触发的。 3.3 自动触发 先来看自动触发。 经过前面的讲解,现在小伙伴们都知道,Spring 容器初始化的时候,会调用到 refresh 方法,这个刷新要做的事情比较多,其中最后一件事情是调用 finishRefresh 方法,如下: protected void finishRefresh() { // Clear context-level resource caches (such as ASM metadata from scanning). clearResourceCaches(); // Initialize lifecycle processor for this context. initLifecycleProcessor(); // Propagate refresh to lifecycle processor first. getLifecycleProcessor().onRefresh(); // Publish the final event. publishEvent(new ContextRefreshedEvent(this)); } 这里有两个方法跟本文相关,一个是 initLifecycleProcessor,这个是初始化 LifecycleProcessor,就是去 Spring 容器中查找 LifecycleProcessor,找到就用,没找到就创建新的。 然后就是 getLifecycleProcessor().onRefresh(); 方法,这个就是触发了 DefaultLifecycleProcessor#onRefresh 方法,而关于该方法的逻辑,松哥在前面已经介绍过了。 来看下 initLifecycleProcessor 方法是如何做初始化操作的: protected void initLifecycleProcessor() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); } else { DefaultLifecycleProcessor defaultProcessor = new DefaultLifecycleProcessor(); defaultProcessor.setBeanFactory(beanFactory); this.lifecycleProcessor = defaultProcessor; beanFactory.registerSingleton(LIFECYCLE_PROCESSOR_BEAN_NAME, this.lifecycleProcessor); } } 大家注意,LIFECYCLE_PROCESSOR_BEAN_NAME 常量的值是 lifecycleProcessor,为什么要强调这个,如果我们是自定义 DefaultLifecycleProcessor,那么 beanName 必须是 lifecycleProcessor,否则系统会以为我们没有自定义 DefaultLifecycleProcessor。 那么这里的逻辑就是如果用户自定义了 DefaultLifecycleProcessor,那么就使用用户自定义的 DefaultLifecycleProcessor,否则就创建一个新的 DefaultLifecycleProcessor,并注册到 Spring 容器中。 这就是自动触发的逻辑。 3.4 手动触发 手动触发需要我们自己调用 start 方法,start 方法如下: @Override public void start() { getLifecycleProcessor().start(); publishEvent(new ContextStartedEvent(this)); } 相当于直接调用了 DefaultLifecycleProcessor 的 start 方法: @Override public void start() { startBeans(false); this.running = true; } 这个跟 DefaultLifecycleProcessor 的 onRefresh 方法内容基本一致,唯一的区别在于调用 startBeans 的时候,传入的参数为 false,这个参数带来的变化,这个松哥在前面的内容中已经分析过了,这里就不再啰嗦啦。 4. 小结 好啦,这就是松哥和大家分享的 Spring Lifecycle 和 SmartLifecycle 的区别。老实说,我们自己开发需要自定义这两个的场景其实并不多,但是在 Spring Boot 中,SmartLifecycle 的应用还是比较多的,有了今天这个内容作基础,将来小伙伴们分析 Spring Boot 的时候就会容易很多了。</string></string></integer></integer,></string,></string,></integer,></string,>

优秀的个人博客,低调大师

软件测试面试:拿到一个产品(版本)如何开展测试?

产品提测后,如何开展测试? 我们都了解软件测试的执行流程,......提测-冒烟测试-详细测试-提交缺陷报告-回归测试,但软件测试并不总是线性过程,它甚至可能是螺旋结构,不断地试错,不断地迭代,不断地回归,直至最终的可用版本。 那么测试人员拿到提测版本后,如何开展测试?如何进行第一轮、第二轮测试? 第一轮测试: 1、从冒烟测试开始,也就是最简单的测试,如果不是特别复杂的项目,可以直接由基本流+备用流的方式来进行快速测试,也可以认为是可用性测试,能否继续进行下一步取决于冒烟测试结果是否通过,如基本流未通,则可以直接退回。否则,继续下一步 2、在快速测试过程中,可能激发了某些灵感,这时一定要记录下来,或者遇到一个新的问题可能引发其他的问题时,也做好记录;做记录的同时可以去补充测试用例,也可以暂时放在待测试想法列表中,通过后续的步骤时,来决定这些想法是否有进一步测试的必要 3、开始执行用例,测试用例一般情况下会区分正向反向用例的,在这个步骤中,先执行正向用例,若未通过数没有超过规定的比例,再执行反向用例;同时在这个过程中,很可能开发人员已提交过N个版本,那么仍需要不定期进行可用性测试 4、确保版本可用的情况下,且已执行完所有用例(部分阻碍用例除外),此时可以对第一轮测试做一个小结。小结内容包括:是否需要调整测试策略;是否存在重复出现的问题;以及自己经过一轮测试后对版本建立的初步认识等等 第二轮测试: 1、整理一轮测试中的缺陷报告,如果有测试管理平台,可以很方便地通过筛选条件来查看缺陷类型和原因,以及缺陷增长趋势;多数开发人员不会主动分析缺陷,需要测试人员来评估哪些区域或模块需要深度测试,如果有修改过公共代码,哪些模块需要重复测试等等 2、结合一轮测试中的测试小结,及评估所有被退回测试或拒绝的Bug,分析拒绝原因并再次测试,记录好二次测试的结果 3、交叉测试,这一步可以灵活调整,视测试时间充分与否,交叉测试人员可以是同组的,也可以是外组测试,可以重点测试Bug聚集的模块 ,也可以探索测试,但测试完成后要做好小结,以便与第一位测试人员的小结做比对。查漏补缺。 4、回归测试,对所有提交的未关闭缺陷进行回归测试 5、可用性测试+大回归测试,在开发多次迭代的基础上,要进行最后一轮的可用性测试,在走流程的过程中,要重点关注Bug集中的模块或语句,以及重新打开频率较高Bug的代码逻辑,在最后一轮的大回归测试中,建议结对测试效果会更好。 最后,将每个步骤中的测试检查点形成文档,再归结到测试报告中。 以上,即是一般性软件产品的测试步骤,实际工作中,可以根据软件版本的大小,及测试团队的规模来具体规划测试流程。(更多软件测试干货可关注公众号“木蚂蚁”了解)

优秀的个人博客,低调大师

面试官问我:如何减少客户对交付成果的质疑

摘要:对标市面主流产品,更新差异特性,让产品跟随市场变化。 本文分享自华为云社区《项目上线后,如何减少客户对交付成果的质疑》,原文作者:敏捷的小智。 背景 背景描述 早些年前,软件行业刚刚兴起,当时的软件产品功能简单,用途单一,软件研发方法也都遵循“计划-->需求分析-->设计-->编码-->测试-->运维”这样一个流程按部就班的开发,最后产品基本能满足客户的需求。这种研发方法被很多公司沿用至今,可与之前不同的是,客户对项目交付成果的质疑越来越多。 有家公司就问了类似的问题:“项目上线后客户提出质疑,导致交付出现问题,项目管理上如何操作可以避免或减少这种情况的发生?”在交流过程中,我们了解到该公司在使用传统的瀑布模型进行研发,同时也了解了客户主要有哪些方面的质疑。 质疑描述 客户质疑大致有三方面,一方面:交付成果和合同要求对不上:客户认为合同明明说的是A,可是产品做出来的功能却是B,比如地处西北的客户吃饺子,想蘸点老陈醋,签了份合同让公司提供“饺子蘸料”,接单的是东北人,根据“蘸料”开发了一瓶酱油,于是客户认为自己表述的足够清晰,是公司内部管理不善造成功能开发错误;另一方面,产品按需求研发,但是整个研发过程中,客户一直没有接收到研发团队关于产品或研发进度的信息,导致客户对项目的焦虑,从而对产品产生质疑;还有一方面,有的产品按需求正确开发,也定时向客户汇报进度,可交付时客户认为功能不够,在当下市场没有竞争力就像当下做电商不支持移动支付;这些质疑让公司很头疼。 你是否也经历着同样的问题?如何减少客户对交付成果的质疑? 问题分析 上文提到的质疑可以概括为:双方对需求理解不一致,产品功能规划没有随市场变化而刷新和沟通不足引发客户不了解情况而焦虑。接下来我们推测下产生这些质疑的原因。 双方对需求理解不一致 需求被制定后,可能没有做进一步澄清,导致开发人员理解有误,照着自己的想法开发出偏离预想的产品;或者客户想表达的意思是A,但是由于自己表述有问题,需求描述成了B,那自然无法开发出令人满意的交付成果。 还有一种情况更要命:客户在制定需求的时候自己只有一个模糊的想法,具体要做的自己也不清楚,这种情况按计划做出的产品想令客户满意就只能靠运气了。 沟通不足,客户因不了解情况而焦虑 我们试想一种场景:小张在网上买了个手机想要送给女朋友作为生日礼物,眼看生日快到了,商品显示已发货,却始终看不到详细的物流信息,客服也不告知小张商品目前是啥情况,换做你是小张,你急不急,就算商品按时到达,也会因为物流过程不可见而而很难获得好评。 实际生产中也是一样,客户下了订单之后,研发团队一直闷头干活,不与客户沟通项目进度,客户一样会因为不了解项目进展而焦虑,最终对交付成果产生质疑。 产品功能规划没有随市场变化而刷新 正所谓计划不如变化,传统研发模式是计划驱动,而市场是瞬息万变的,想要占领市场,需求变更在所难免。计划就像一张地图,一条路经历世事变迁会发生很多变化,按照一张两年前的地图找上面标注的店铺很可能走了半天也找不到地方;同样,照着两年前制定的计划做出的产品,按交付时的背景去审视它,会给人一种“乃不知有汉,无论魏晋”的感觉,客户难免会对产品提出质疑。 解决措施 传统研发模式的交付类似流水作业:做完计划和需求,就可以按照计划进行开发,然后交付验收。在这种研发模式中,客户参与度类似U型:客户在计划阶段和定义需求阶段参与较多;之后项目进入研发阶段,客户参与度骤降甚至不参与;最后交付阶段客户参与进来,进行验收工作。客户在研发阶段参与度降低,很容易造成双方对产品沟通不到位:比如需求被错误理解没人引导;市场上出现新功能,产品想不到变通等,这些“不到位”最后都会转化成对交付成果的质疑。为避免这种情况,可以尝试做敏捷转型,客户对交付成果的质疑在所难免,但敏捷可以大大减少客户的质疑。 敏捷开发的价值观是:“个体和互动重于流程和工具;可工作的软件重于面面俱到的文档;客户合作重于合同谈判;响应变化重于遵循计划,尽管右侧重要,但左侧更重要”。敏捷按迭代进行交付,每个迭代持续时间不会很长;同时敏捷更注重给客户带来的价值,客户(或客户代表)可以全生命周期的参与并影响整个项目。下图是传统开发和敏捷开发客户在不同生命周期参与度对比。 敏捷具体可以从哪几个方面减少客户对交付成果的质疑呢? 使用标准的用户故事方法分析和记录需求,确保双方理解一致 传统研发模式以计划为导向,使用详细的文档比如:概要设计、详细设计记录需求,这种方法有他的优点,但是缺点也比较明显:首先制作计划需要花费很长的时间,其次需求描述过于产品化,不易解读。 敏捷开发以价值为导向,区别于传统研发模式的文档,敏捷开发使用用户故事记录需求:用户故事是站在用户的角度去描述需求,并且给出用户期待实现的价值,这样开发人员更容易开发出客户真正想要的功能(用户故事细节详见 《 “用户故事等于需求说明”——你一定没有写好用户故事》 )。 举个例子,使用用户故事描述需求:客户吃饺子想要一瓶蘸料,用户故事可以写成:“作为生长在山西的小王,我想要一瓶饺子蘸料,以便于让饺子吃起来更美味”。通过用户故事可以看出,客户是“生长在山西的”,所以饺子蘸料可能是老陈醋而不是酱油,交付起来会比“客户想要一瓶蘸料”准确很多。 另外用户故事并不是写好之后就一成不变的。用户故事的“INVEST”原则中的“N(可商议的)”原则要求用户故事是可以商议的。当开发人员不理解用户故事中的需求,可以将问题抛出来,由产品负责人进行澄清,直到双方对需求的理解达成共识。下图是使用DevCloud编写的用户故事,以及需求分析讨论。 综上所述,使用标准的用户故事记录需求,可以解决双方对需求理解不一致的问题,从而减少客户对交付成果的质疑。 通过评审会等方式与客户保持定期或不定期的沟通交流 敏捷开发方法众多,Scrum是最主流的敏捷方法之一。Scrum中有四个活动:计划会议,每日站会,评审会议,回顾会议,每个活动都帮助着团队更好的践行敏捷,更高质量的交付,各活动详细信息如下: 从表中可以看出,计划会议,每日站会,评审会议都是围绕产品开展的。评审会议在每个迭代即将结束时开展,定期邀请客户参加评审会议是最直接有效的与客户沟通的方法:会上团队向客户演示迭代交付成果,客户通过演示了解产品已经具备哪些功能,哪些功能没有完成,哪些功能和理想中有偏差,对于偏差部分可以和开发团队沟通,后续迭代进行改进。 如果客户很忙或者时间不稳定,不能参与每次评审会议,那么可不定期邀请客户参加每日站会,站会每天早晨都进行,客户可以在有空或有兴趣的时候参与。每日站会不是必须和客户一起开展,但是通过站会客户能了解到小部分交付成果以及团队工作状态,减少焦虑。客户不参加会议的话,可由产品负责人在评审会议结束后整理评审会议纪要,通过拜访,电话,邮件等形式告知客户,让客户了解当前项目进度,减少焦虑,从而减少对交付成果的质疑。 对标市面主流产品,更新差异特性,让产品跟随市场变化 除了澄清需求、增强与客户的沟通等手段之外,我们还可以带着客户,用产品对标市面上其他主流产品,找到差异并更新,减少客户的质疑。比如一款电商产品的结算功能在计划时未考虑移动支付,只支持网银支付,按传统模式运作项目的话,最终交付时产品不会支持移动支付,使用起来会很麻烦;如果使用Scrum运作项目,可以在项目进行过程中,或者评审产品的支付功能时,对标主流电商产品,这时候会发现移动支付是目前最主流的在线支付方式,产品需要支持移动支付,所以可将“结算功能支持移动支付”作为一个优先级高的新需求加入项目,并与产品负责人协商下个迭代或尽可能快的完成这个需求,让产品的支付功能跟随市场变化,增加产品的竞争力。 参考附录 Kenneth S.Rubin:Scrum精髓. 北京:清华大学出版社 Scrum Guide 点击关注,第一时间了解华为云新鲜技术~

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。