1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Mybatis自定義攔截器與插件開發(fā)

        共 14700字,需瀏覽 30分鐘

         ·

        2021-12-09 13:51

        點擊關(guān)注公眾號,Java干貨及時送達??

        在Spring中我們經(jīng)常會使用到攔截器,在登錄驗證、日志記錄、性能監(jiān)控等場景中,通過使用攔截器允許我們在不改動業(yè)務(wù)代碼的情況下,執(zhí)行攔截器的方法來增強現(xiàn)有的邏輯。在mybatis中,同樣也有這樣的業(yè)務(wù)場景,有時候需要我們在不侵入原有業(yè)務(wù)代碼的情況下攔截sql,執(zhí)行特定的某些邏輯。那么這個過程應(yīng)該怎么實現(xiàn)呢,同樣,在mybatis中也為開發(fā)者預(yù)留了攔截器接口,通過實現(xiàn)自定義攔截器這一功能,可以實現(xiàn)我們自己的插件,允許用戶在不改動mybatis的原有邏輯的條件下,實現(xiàn)自己的邏輯擴展。

        本文將按下面的結(jié)構(gòu)進行mybatis攔截器學(xué)習(xí):

        本文結(jié)構(gòu)

        1、攔截器核心對象

        2、工作流程

        3、攔截器能實現(xiàn)什么

        4、插件定義與注冊

        5、攔截器使用示例

        6、總結(jié)

        攔截器核心對象

        在實現(xiàn)攔截器之前,我們首先看一下攔截器的攔截目標(biāo)對象是什么,以及攔截器的工作流程是怎樣的。mybatis攔截器可以對下面4種對象進行攔截:

        1、Executor:mybatis的內(nèi)部執(zhí)行器,作為調(diào)度核心負責(zé)調(diào)用StatementHandler操作數(shù)據(jù)庫,并把結(jié)果集通過ResultSetHandler進行自動映射

        2、StatementHandler: 封裝了JDBC Statement操作,是sql語法的構(gòu)建器,負責(zé)和數(shù)據(jù)庫進行交互執(zhí)行sql語句

        3、ParameterHandler:作為處理sql參數(shù)設(shè)置的對象,主要實現(xiàn)讀取參數(shù)和對PreparedStatement的參數(shù)進行賦值

        4、ResultSetHandler:處理Statement執(zhí)行完成后返回結(jié)果集的接口對象,mybatis通過它把ResultSet集合映射成實體對象

        工作流程

        在mybatis中提供了一個Interceptor接口,通過實現(xiàn)該接口就能夠自定義攔截器,接口中定義了3個方法:

        public?interface?Interceptor?{
        ??Object?intercept(Invocation?invocation)?throws?Throwable;
        ??default?Object?plugin(Object?target)?{
        ????return?Plugin.wrap(target,?this);
        ??}
        ??default?void?setProperties(Properties?properties)?{
        ????//?NOP
        ??}
        }
        • intercept:在攔截目標(biāo)對象的方法時,實際執(zhí)行的增強邏輯,我們一般在該方法中實現(xiàn)自定義邏輯

        • plugin:用于返回原生目標(biāo)對象或它的代理對象,當(dāng)返回的是代理對象的時候,會調(diào)用intercept方法

        • setProperties:可以用于讀取配置文件中通過property標(biāo)簽配置的一些屬性,設(shè)置一些屬性變量

        看一下plugin方法中的wrap方法源碼:

        public?static?Object?wrap(Object?target,?Interceptor?interceptor)?{
        ??Map,?Set>?signatureMap?=?getSignatureMap(interceptor);
        ??Class?type?=?target.getClass();
        ??Class[]?interfaces?=?getAllInterfaces(type,?signatureMap);
        ??if?(interfaces.length?>?0)?{
        ????return?Proxy.newProxyInstance(
        ????????type.getClassLoader(),
        ????????interfaces,
        ????????new?Plugin(target,?interceptor,?signatureMap));
        ??}
        ??return?target;
        }

        可以看到,在wrap方法中,通過使用jdk動態(tài)代理的方式,生成了目標(biāo)對象的代理對象,在執(zhí)行實際方法前,先執(zhí)行代理對象中的邏輯,來實現(xiàn)的邏輯增強。以攔截Executorquery方法為例,在實際執(zhí)行前會執(zhí)行攔截器中的intercept方法:

        在mybatis中,不同類型的攔截器按照下面的順序執(zhí)行:

        Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler

        以執(zhí)行query 方法為例對流程進行梳理,整體流程如下:

        1、Executor執(zhí)行query()方法,創(chuàng)建一個StatementHandler對象

        2、StatementHandler 調(diào)用ParameterHandler對象的setParameters()方法

        3、StatementHandler 調(diào)用 Statement對象的execute()方法

        4、StatementHandler 調(diào)用ResultSetHandler對象的handleResultSets()方法,返回最終結(jié)果

        攔截器能實現(xiàn)什么

        在對mybatis攔截器有了初步的認識后,來看一下攔截器被普遍應(yīng)用在哪些方面:

        • sql 語句執(zhí)行監(jiān)控

          可以攔截執(zhí)行的sql方法,可以打印執(zhí)行的sql語句、參數(shù)等信息,并且還能夠記錄執(zhí)行的總耗時,可供后期的sql分析時使用

        • sql 分頁查詢

          mybatis中使用的RowBounds使用的內(nèi)存分頁,在分頁前會查詢所有符合條件的數(shù)據(jù),在數(shù)據(jù)量大的情況下性能較差。通過攔截器,可以做到在查詢前修改sql語句,提前加上需要的分頁參數(shù)

        • 公共字段的賦值

          在數(shù)據(jù)庫中通常會有createTime,updateTime等公共字段,這類字段可以通過攔截統(tǒng)一對參數(shù)進行的賦值,從而省去手工通過set方法賦值的繁瑣過程

        • 數(shù)據(jù)權(quán)限過濾

          在很多系統(tǒng)中,不同的用戶可能擁有不同的數(shù)據(jù)訪問權(quán)限,例如在多租戶的系統(tǒng)中,要做到租戶間的數(shù)據(jù)隔離,每個租戶只能訪問到自己的數(shù)據(jù),通過攔截器改寫sql語句及參數(shù),能夠?qū)崿F(xiàn)對數(shù)據(jù)的自動過濾

        除此之外,攔截器通過對上述的4個階段的介入,結(jié)合我們的實際業(yè)務(wù)場景,還能夠?qū)崿F(xiàn)很多其他功能。

        插件定義與注冊

        在我們自定義的攔截器類實現(xiàn)了Interceptor接口后,還需要在類上添加@Intercepts 注解,標(biāo)識該類是一個攔截器類。注解中的內(nèi)容是一個@Signature對象的數(shù)組,指明自定義攔截器要攔截哪一個類型的哪一個具體方法。其中type指明攔截對象的類型,method是攔截的方法,argsmethod執(zhí)行的參數(shù)。通過這里可以了解到 mybatis 攔截器的作用目標(biāo)是在方法級別上進行攔截,例如要攔截Executorquery方法,就在類上添加:

        @Intercepts({
        ????????@Signature(type?=?Executor.class,method?=?"query",?args?=?{?MappedStatement.class,?Object.class,
        ????????????????RowBounds.class,?ResultHandler.class?})
        })

        如果要攔截多個方法,可以繼續(xù)以數(shù)組的形式往后追加。這里通過添加參數(shù)可以確定唯一的攔截方法,例如在Executor中存在兩個query方法,通過上面的參數(shù)可以確定要攔截的是下面的第2個方法:

        ?List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?cacheKey,?BoundSql?boundSql);
        ?List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler);

        當(dāng)編寫完成我們自己的插件后,需要向mybatis中注冊插件,有兩種方式可以使用,第一種直接在SqlSessionFactory中配置:

        @Bean
        public?SqlSessionFactory?sqlSessionFactory(DataSource?dataSource)?throws?Exception?{
        ????SqlSessionFactoryBean?sqlSessionFactoryBean?=?new?SqlSessionFactoryBean();
        ????sqlSessionFactoryBean.setDataSource(dataSource);
        ????sqlSessionFactoryBean.setPlugins(new?Interceptor[]{new?ExecutorPlugin()});
        ????return?sqlSessionFactoryBean.getObject();
        }

        第2種是在mybatis-config.xml中對自定義插件進行注冊:

        <configuration>
        ????<plugins>
        ????????<plugin?interceptor="com.cn.plugin.interceptor.MyPlugin">
        ?????????<property?name="text"?value="hello"/>
        ????????plugin>
        ????????<plugin?interceptor="com.cn.plugin.interceptor.MyPlugin2">plugin>
        ????????<plugin?interceptor="com.cn.plugin.interceptor.MyPlugin3">plugin>
        ????plugins>
        configuration>

        在前面我們了解了不同類型攔截器執(zhí)行的固定順序,那么對于同樣類型的多個自定義攔截器,它們的執(zhí)行順序是怎樣的呢?分別在plugin方法和intercept中添加輸出語句,運行結(jié)果如下:

        從結(jié)果可以看到,攔截順序是按照注冊順序執(zhí)行的,但代理邏輯的執(zhí)行順序正好相反,最后注冊的會被最先執(zhí)行。這是因為在mybatis中有一個類InterceptorChain,在它的pluginAll()方法中,會對原生對象target進行代理,如果有多個攔截器的話,會對代理類再次進行代理,最終實現(xiàn)一層層的增強target對象,因此靠后被注冊的攔截器的增強邏輯會被優(yōu)先執(zhí)行。從下面的圖中可以直觀的看出代理的嵌套關(guān)系:

        xml中注冊完成后,在application.yml中啟用配置文件,這樣插件就可以正常運行了:

        mybatis:
        ??config-location:?classpath:mybatis-config.xml

        在了解了插件的基礎(chǔ)概念與運行流程之后,通過代碼看一下應(yīng)用不同的攔截器能夠?qū)崿F(xiàn)什么功能。

        攔截器使用示例

        Executor

        通過攔截Executorqueryupdate方法實現(xiàn)對sql的監(jiān)控,在攔截方法中,打印sql語句、執(zhí)行參數(shù)、實際執(zhí)行時間:

        @Intercepts({
        ????????@Signature(type?=?Executor.class,method?=?"update",?args?=?{MappedStatement.class,?Object.class}),
        ????????@Signature(type?
        =?Executor.class,method?=?"query",?args?=?{?MappedStatement.class,?Object.class,
        ????????????????RowBounds.class,?ResultHandler.class?})})
        public?class?ExecutorPlugin?implements?Interceptor?
        {
        ????@Override
        ????public?Object?intercept(Invocation?invocation)?throws?Throwable?{
        ????????System.out.println("Executor?Plugin?攔截?:"+invocation.getMethod());
        ????????Object[]?queryArgs?=?invocation.getArgs();
        ????????MappedStatement?mappedStatement?=?(MappedStatement)?queryArgs[0];
        ????????//獲取?ParamMap
        ????????MapperMethod.ParamMap?paramMap?=?(MapperMethod.ParamMap)?queryArgs[1];
        ????????//?獲取SQL
        ????????BoundSql?boundSql?=?mappedStatement.getBoundSql(paramMap);
        ????????String?sql?=?boundSql.getSql();
        ????????log.info("==>?ORIGIN?SQL:?"+sql);
        ????????long?startTime?=?System.currentTimeMillis();
        ????????Configuration?configuration?=?mappedStatement.getConfiguration();
        ????????String?sqlId?=?mappedStatement.getId();

        ????????Object?proceed?=?invocation.proceed();
        ????????long?endTime=System.currentTimeMillis();
        ????????long?time?=?endTime?-?startTime;
        ????????printSqlLog(configuration,boundSql,sqlId,time);
        ????????return?proceed;
        ????}

        ????public?static?void?printSqlLog(Configuration?configuration,?BoundSql?boundSql,?String?sqlId,?long?time){
        ????????Object?parameterObject?=?boundSql.getParameterObject();
        ????????List?parameterMappings?=?boundSql.getParameterMappings();
        ????????String?sql=?boundSql.getSql().replaceAll("[\\s]+",?"?");
        ????????StringBuffer?sb=new?StringBuffer("==>?PARAM:");
        ????????if?(parameterMappings.size()>0?&&?parameterObject!=null){
        ????????????TypeHandlerRegistry?typeHandlerRegistry?=?configuration.getTypeHandlerRegistry();
        ????????????if?(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass()))?{
        ????????????????sql?=?sql.replaceFirst("\\?",?parameterObject.toString());
        ????????????}?else?{
        ????????????????MetaObject?metaObject?=?configuration.newMetaObject(parameterObject);
        ????????????????for?(ParameterMapping?parameterMapping?:?parameterMappings)?{
        ????????????????????String?propertyName?=?parameterMapping.getProperty();
        ????????????????????if?(metaObject.hasGetter(propertyName))?{
        ????????????????????????Object?obj?=?metaObject.getValue(propertyName);
        ????????????????????????String?parameterValue?=?obj.toString();
        ????????????????????????sql?=?sql.replaceFirst("\\?",?parameterValue);
        ????????????????????????sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
        ????????????????????}?else?if?(boundSql.hasAdditionalParameter(propertyName))?{
        ????????????????????????Object?obj?=?boundSql.getAdditionalParameter(propertyName);
        ????????????????????????String?parameterValue?=?obj.toString();
        ????????????????????????sql?=?sql.replaceFirst("\\?",?parameterValue);
        ????????????????????????sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
        ????????????????????}
        ????????????????}
        ????????????}
        ????????????sb.deleteCharAt(sb.length()-1);
        ????????}
        ????????log.info("==>?SQL:"+sql);
        ????????log.info(sb.toString());
        ????????log.info("==>?SQL?TIME:"+time+"?ms");
        ????}
        }

        執(zhí)行代碼,日志輸出如下:

        在上面的代碼中,通過Executor攔截器獲取到了BoundSql對象,進一步獲取到sql的執(zhí)行參數(shù),從而實現(xiàn)了對sql執(zhí)行的監(jiān)控與統(tǒng)計。

        StatementHandler

        下面的例子中,通過改變StatementHandler對象的屬性,動態(tài)修改sql語句的分頁:

        @Intercepts({
        ????????@Signature(type?=?StatementHandler.class,?method?=?"prepare",?args?=?{Connection.class,?Integer.class})})
        public?class?StatementPlugin?implements?Interceptor?
        {
        ????@Override
        ????public?Object?intercept(Invocation?invocation)?throws?Throwable?{????????
        ????????StatementHandler?statementHandler?=?(StatementHandler)?invocation.getTarget();
        ????????MetaObject?metaObject?=?SystemMetaObject.forObject(statementHandler);????????????
        ????????metaObject.setValue("delegate.rowBounds.offset",?0);
        ????????metaObject.setValue("delegate.rowBounds.limit",?2);
        ????????return?invocation.proceed();
        ????}
        }

        MetaObject是mybatis提供的一個用于方便、優(yōu)雅訪問對象屬性的對象,通過將實例對象作為參數(shù)傳遞給它,就可以通過屬性名稱獲取對應(yīng)的屬性值。雖然說我們也可以通過反射拿到屬性的值,但是反射過程中需要對各種異常做出處理,會使代碼中堆滿難看的try/catch,通過MetaObject可以在很大程度上簡化我們的代碼,并且它支持對Bean、CollectionMap三種類型對象的操作。

        對比執(zhí)行前后:

        可以看到這里通過改變了分頁對象RowBounds的屬性,動態(tài)的修改了分頁參數(shù)。

        ResultSetHandler

        ResultSetHandler 會負責(zé)映射sql語句查詢得到的結(jié)果集,如果在生產(chǎn)環(huán)境中存在一些保密數(shù)據(jù),不想在外部系統(tǒng)中展示,那么可能就需要在查詢到結(jié)果后做一下數(shù)據(jù)的脫敏處理,這時候就可以使用ResultSetHandler對結(jié)果集進行改寫。

        @Intercepts({
        ????????@Signature(type=?ResultSetHandler.class,method?=?"handleResultSets",args?=?{Statement.class})})
        public?class?ResultSetPlugin?implements?Interceptor?
        {
        ????@Override
        ????public?Object?intercept(Invocation?invocation)?throws?Throwable?{
        ????????System.out.println("Result?Plugin?攔截?:"+invocation.getMethod());
        ????????Object?result?=?invocation.proceed();
        ????????if?(result?instanceof?Collection)?{
        ????????????Collection?objList=?(Collection)?result;
        ????????????List?resultList=new?ArrayList<>();
        ????????????for?(Object?obj?:?objList)?{
        ????????????????resultList.add(desensitize(obj));
        ????????????}
        ????????????return?resultList;
        ????????}else?{
        ????????????return?desensitize(result);
        ????????}
        ????}
        ?//脫敏方法,將加密字段變?yōu)樾翘?/span>
        ????private?Object?desensitize(Object?object)?throws?InvocationTargetException,?IllegalAccessException?{
        ????????Field[]?fields?=?object.getClass().getDeclaredFields();
        ????????for?(Field?field?:?fields)?{
        ????????????Confidential?confidential?=?field.getAnnotation(Confidential.class);
        ????????????if?(confidential==null){
        ????????????????continue;
        ????????????}
        ????????????PropertyDescriptor?ps?=?BeanUtils.getPropertyDescriptor(object.getClass(),?field.getName());
        ????????????if?(ps.getReadMethod()?==?null?||?ps.getWriteMethod()?==?null)?{
        ????????????????continue;
        ????????????}
        ????????????Object?value?=?ps.getReadMethod().invoke(object);
        ????????????if?(value?!=?null)?{
        ????????????????ps.getWriteMethod().invoke(object,?"***");
        ????????????}
        ????????}
        ????????return?object;
        ????}
        }

        運行上面的代碼,查看執(zhí)行結(jié)果:

        {"id":1358041517788299266,"orderNumber":"***","money":122.0,"status":3,"tenantId":2}

        在上面的例子中,在執(zhí)行完sql語句得到結(jié)果對象后,通過反射掃描結(jié)果對象中的屬性,如果實體的屬性上帶有自定義的@Confidential注解,那么在脫敏方法中將它轉(zhuǎn)化為星號再返回結(jié)果,從而實現(xiàn)了數(shù)據(jù)的脫敏處理。

        ParameterHandler

        mybatis可以攔截ParameterHandler注入?yún)?shù),下面的例子中我們將結(jié)合前面介紹的其他種類的對象,通過組合攔截器的方式,實現(xiàn)一個簡單的多租戶攔截器插件,實現(xiàn)多租戶下的查詢邏輯。

        @Intercepts({
        ????????@Signature(type?=?Executor.class,method?=?"query",?args?=?{?MappedStatement.class,?Object.class,RowBounds.class,?ResultHandler.class?}),
        ????????@Signature(type?
        =?StatementHandler.class,?method?=?"prepare",?args?=?{Connection.class,?Integer.class}),
        ????????@Signature(type?
        =?ParameterHandler.class,?method?=?"setParameters",?args?=?PreparedStatement.class),
        })
        public?class?TenantPlugin?implements?Interceptor?
        {
        ????private?static?final?String?TENANT_ID?=?"tenantId";

        ????@Override
        ????public?Object?intercept(Invocation?invocation)?throws?Throwable?{
        ????????Object?target?=?invocation.getTarget();
        ????????String?methodName?=?invocation.getMethod().getName();
        ????????if?(target?instanceof?Executor?&&??methodName.equals("query")?&&?invocation.getArgs().length==4)?{
        ????????????return?doQuery(invocation);
        ????????}
        ????????if?(target?instanceof?StatementHandler){
        ????????????return?changeBoundSql(invocation);
        ????????}
        ????????if?(target?instanceof?ParameterHandler){
        ????????????return?doSetParameter(invocation);
        ????????}
        ????????return?null;
        ????}

        ????private?Object?doQuery(Invocation?invocation)?throws?Exception{
        ????????Executor?executor?=?(Executor)?invocation.getTarget();
        ????????MappedStatement?ms=?(MappedStatement)?invocation.getArgs()[0];
        ????????Object?paramObj?=?invocation.getArgs()[1];
        ????????RowBounds?rowBounds?=?(RowBounds)?invocation.getArgs()[2];

        ????????if?(paramObj?instanceof?Map){
        ????????????MapperMethod.ParamMap?paramMap=?(MapperMethod.ParamMap)?paramObj;
        ????????????if?(!paramMap.containsKey(TENANT_ID)){
        ????????????????Long?tenantId=1L;
        ????????????????paramMap.put("param"+(paramMap.size()/2+1),tenantId);
        ????????????????paramMap.put(TENANT_ID,tenantId);
        ????????????????paramObj=paramMap;
        ????????????}
        ????????}
        ????????//直接執(zhí)行query,不用proceed()方法
        ????????return?executor.query(ms,?paramObj,rowBounds,null);
        ????}

        ????private?Object?changeBoundSql(Invocation?invocation)?throws?Exception?{
        ????????StatementHandler?statementHandler?=?(StatementHandler)?invocation.getTarget();
        ????????MetaObject?metaObject?=?SystemMetaObject.forObject(statementHandler);
        ????????PreparedStatementHandler?preparedStatementHandler?=?(PreparedStatementHandler)?metaObject.getValue("delegate");
        ????????String?originalSql?=?(String)?metaObject.getValue("delegate.boundSql.sql");
        ????????metaObject.setValue("delegate.boundSql.sql",originalSql+?"?and?tenant_id=?");
        ????????return?invocation.proceed();
        ????}

        ????private?Object?doSetParameter(Invocation?invocation)?throws?Exception?{
        ????????ParameterHandler?parameterHandler?=?(ParameterHandler)?invocation.getTarget();
        ????????PreparedStatement?ps?=?(PreparedStatement)?invocation.getArgs()[0];
        ????????MetaObject?metaObject?=?SystemMetaObject.forObject(parameterHandler);
        ????????BoundSql?boundSql=?(BoundSql)?metaObject.getValue("boundSql");

        ????????List?parameterMappings?=?boundSql.getParameterMappings();
        ????????boolean?hasTenantId=false;
        ????????for?(ParameterMapping?parameterMapping?:?parameterMappings)?{
        ????????????if?(parameterMapping.getProperty().equals(TENANT_ID))?{
        ????????????????hasTenantId=true;
        ????????????}
        ????????}
        ????????//添加參數(shù)
        ????????if?(!hasTenantId){
        ????????????Configuration?conf=?(Configuration)?metaObject.getValue("configuration");
        ????????????ParameterMapping?parameterMapping=?new?ParameterMapping.Builder(conf,TENANT_ID,Long.class).build();
        ????????????parameterMappings.add(parameterMapping);
        ????????}
        ????????parameterHandler.setParameters(ps);
        ????????return?null;
        ????}
        }

        在上面的過程中,攔截了sql執(zhí)行的三個階段,來實現(xiàn)多租戶的邏輯,邏輯分工如下:

        • 攔截Executorquery方法,在查詢的參數(shù)Map中添加租戶的屬性值,這里只是簡單的對Map的情況作了判斷,沒有對Bean的情況進行設(shè)置
        • 攔截StatementHandlerprepare方法,改寫sql語句對象BoundSql,在sql語句中拼接租戶字段的查詢條件
        • 攔截ParameterHandlersetParameters方法,動態(tài)設(shè)置參數(shù),將租戶id添加到要設(shè)置到參數(shù)列表中

        最終通過攔截不同執(zhí)行階段的組合,實現(xiàn)了基于租戶的條件攔截。

        總結(jié)

        總的來說,mybatis攔截器通過對Executor、StatementHandler、ParameterHandler、ResultSetHandler 這4種接口中的方法進行攔截,并生成代理對象,在執(zhí)行方法前先執(zhí)行代理對象的邏輯,來實現(xiàn)我們自定義的邏輯增強。從上面的例子中,可以看到通過靈活使用mybatis攔截器開發(fā)插件能夠幫助我們解決很多問題,但是同樣它也是一把雙刃劍,在實際工作中也不要濫用插件、定義過多的攔截器,因為通過學(xué)習(xí)我們知道m(xù)ybatis插件在執(zhí)行中使用到了代理模式和責(zé)任鏈模式,在執(zhí)行sql語句前會經(jīng)過層層代理,如果代理次數(shù)過多將會消耗額外的性能,并增加響應(yīng)時間。

        1.?ELK不香了!我用Graylog

        2.?阿里面試這樣問:Nacos配置中心交互模型是 push 還是 pull ?(原理+源碼分析)

        3.?什么是 APM 系統(tǒng)?如何設(shè)計與實現(xiàn)?

        4.?帶著8個問題5分鐘教你學(xué)會 Arthas 診斷工具

        最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

        獲取方式:點“在看”,關(guān)注公眾號并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

        文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

        謝謝支持喲 (*^__^*)

        瀏覽 54
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

          <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            在线播放无码视频 | 青青嫩草影院在线视频 | 91精品国产情侣高潮露脸仙踪林 | 乱伦小视频91 | 少妇又色又紧又爽又刺激视频 | 骚逼网址| 无码一区一区 | 淫色视频网站 | 小黄片视频免费 | 女人被爽到呻吟的床戏不忠 |