Android Binder通信原理(五):Java 端的service 注册和获取

news2024/10/6 6:40:16

源码基于:Android R

0. 前言

在阐述相关的知识点,先整理个框架图,后面带着框架图来分析过程:

Java 端对于binder 使用大致分为:

  • Java client Java service
  • Java client native service

Java 端service 的注册使用 ServiceManager.addService()

Java 端service 的获取使用 ServiceManager.getService()

入口都是通过 ServiceManager.java,文件位于frameworks/base/core/java/android/os/ServiceManager.java

在分析addService 和getService 之前,需要了解Binder 最终是会调用到native 端,那么,Java 中间就需要通过JNI,所以在此之前需要了解JNI 的注册过程。

1. Binder JNI 注册

了解zygote 启动过程的同学应该知道 zygote 在启动时首先会创建虚拟机,然后再通过 startReg() 注册系统所需要的JNI 接口。

frameworks/base/core/jni/AndroidRuntime.cpp

/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    ...
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    ...
}

注册的接口定义在:

frameworks/base/core/jni/AndroidRuntime.cpp

static const RegJNIRec gRegJNI[] = {
        REG_JNI(register_com_android_internal_os_RuntimeInit),
        REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
        REG_JNI(register_android_os_SystemClock),
        REG_JNI(register_android_util_EventLog),
        REG_JNI(register_android_util_Log),
        REG_JNI(register_android_util_MemoryIntArray),
        ...
        REG_JNI(register_android_os_Binder),
        ...

数组很长,这里不过多粘贴,binder 相关的注册函数有:

        REG_JNI(register_android_os_Binder),

最终会调用到函数register_android_os_Binder():

frameworks/base/core/jni/android_util_Binder.cpp

int register_android_os_Binder(JNIEnv* env)
{
    if (int_register_android_os_Binder(env) < 0)
        return -1;
    if (int_register_android_os_BinderInternal(env) < 0)
        return -1;
    if (int_register_android_os_BinderProxy(env) < 0)
        return -1;
    ...

剩下来就不过多分析了,感兴趣的同学可以进入详细查看源码流程 (当然,下面遇到也会顺带分析下),这里只需要知道的是Java 端的Binder 需要native 操作的JNI 接口都是在zygote 启动时就注册好了。

2. ServiceManager 的代理

无论是ServiceManager.addService() 还是ServiceManager.getService() 最终都是需要与native 通信,那需要获取到 ServiceManager 的代理。即函数 getIServiceManager()

frameworks/base/core/java/android/os/ServiceManager.java 

    private static IServiceManager getIServiceManager() {
        if (sServiceManager != null) {
            return sServiceManager;
        }

        // Find the service manager
        sServiceManager = ServiceManagerNative
                .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
        return sServiceManager;
    }

这里主要关注两点:

  • BidnerInternal.getcontextObject()
  • ServiceManagerNative.asInterface()

2.1 BinderInternal.getContextObject()

frameworks/base/core/java/com/android/internal/os/BinderInternal.java

public static final native IBinder getContextObject();

这个需要调用 JNI 接口,而这个接口注册在第 1 节已经阐述过,详细看int_register_android_os_BinderInternal()

frameworks/base/core/jin/android_util_Binder.cpp

static const JNINativeMethod gBinderInternalMethods[] = {
     /* name, signature, funcPtr */
    { "getContextObject", "()Landroid/os/IBinder;", (void*)android_os_BinderInternal_getContextObject },
    ...
    }
};
static int int_register_android_os_BinderInternal(JNIEnv* env)
{
    ...
    return RegisterMethodsOrDie(
env, kBinderInternalPathName,
 gBinderInternalMethods, NELEM(gBinderInternalMethods));
}

通过注册 gBinderInternalMethods 数组得知,最终调用接口android_os_BinderInternal_getContextObject():

frameworks/base/core/jin/android_util_Binder.cpp

static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
{
    sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
    return javaObjectForIBinder(env, b);
}

这里很熟悉了,获取的是 context 为 0 的servicemanager 的BpBinder,并将BpBinder 通过函数javaObjectForIBinder() 转换成Java 的 BinderProxy 对象返回。

2.1.1 javaObjectForIBinder()

frameworks/base/core/jin/android_util_Binder.cpp

jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
{
    ...
    BinderProxyNativeData* nativeData = new BinderProxyNativeData();
    nativeData->mOrgue = new DeathRecipientList;
    nativeData->mObject = val;

    jobject object = env->CallStaticObjectMethod(gBinderProxyOffsets.mClass,
            gBinderProxyOffsets.mGetInstance, (jlong) nativeData, (jlong) val.get());
    if (env->ExceptionCheck()) {
        // In the exception case, getInstance still took ownership of nativeData.
        return NULL;
    }
    ...

    return object;
}

这里主要通过CallStaticObjectMethod() 调用BinderProxy 中的 geInstance(),而这里的全局对象 gBinderProxyOffsets 是在 int_register_android_os_BinderProxy() 中指定的:

frameworks/base/core/jin/android_util_Binder.cpp

static int int_register_android_os_BinderProxy(JNIEnv* env)
{
    gErrorOffsets.mError = MakeGlobalRefOrDie(env, FindClassOrDie(env, "java/lang/Error"));
    gErrorOffsets.mOutOfMemory =
        MakeGlobalRefOrDie(env, FindClassOrDie(env, "java/lang/OutOfMemoryError"));
    gErrorOffsets.mStackOverflow =
        MakeGlobalRefOrDie(env, FindClassOrDie(env, "java/lang/StackOverflowError"));

    jclass clazz = FindClassOrDie(env, kBinderProxyPathName);
    gBinderProxyOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gBinderProxyOffsets.mGetInstance = GetStaticMethodIDOrDie(env, clazz, "getInstance",
            "(JJ)Landroid/os/BinderProxy;");
    gBinderProxyOffsets.mSendDeathNotice =
            GetStaticMethodIDOrDie(env, clazz, "sendDeathNotice",
                                   "(Landroid/os/IBinder$DeathRecipient;Landroid/os/IBinder;)V");
    gBinderProxyOffsets.mNativeData = GetFieldIDOrDie(env, clazz, "mNativeData", "J");

    clazz = FindClassOrDie(env, "java/lang/Class");
    gClassOffsets.mGetName = GetMethodIDOrDie(env, clazz, "getName", "()Ljava/lang/String;");

    return RegisterMethodsOrDie(
        env, kBinderProxyPathName,
        gBinderProxyMethods, NELEM(gBinderProxyMethods));
}

javaObjectForIBinder() 需要通过 CallStaticObjectMethod() 函数调用 BinderProxy 的 getInstance() 接口,来看下:

frameworks/base/core/java/android/os/BinderProxy.java

    private static BinderProxy getInstance(long nativeData, long iBinder) {
        BinderProxy result;
        synchronized (sProxyMap) {
            try {
                result = sProxyMap.get(iBinder);
                if (result != null) {
                    return result;
                }
                result = new BinderProxy(nativeData);
            } catch (Throwable e) {
                ...
            }
            ...
        }
        return result;
    }

至此,可知 BidnerInternal.getcontextObject() 获取的是context 为0 的 BinderProxy 对象。

2.2 ServiceManagerNative.asInterface()

framworks/base/core/java/android/os/ServiceManagerNative.java

    public static IServiceManager asInterface(IBinder obj) {
        if (obj == null) {
            return null;
        }

        // ServiceManager is never local
        return new ServiceManagerProxy(obj);
    }

BinderProxy 对象获取到后,通过asInterface,创建ServiceManager 的Java 端代理ServiceManagerProxy:

framworks/base/core/java/android/os/ServiceManagerNative.java

class ServiceManagerProxy implements IServiceManager {
    public ServiceManagerProxy(IBinder remote) {
        mRemote = remote;
        mServiceManager = IServiceManager.Stub.asInterface(remote);
    }
    ...
    public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
            throws RemoteException {
        mServiceManager.addService(name, service, allowIsolated, dumpPriority);
    }
    ...
    private IBinder mRemote;
    private IServiceManager mServiceManager;
}

mRemote 为BinderProxy 对象,mServiceManager 则是所有接口的入口,ServiceManager的真正proxy,下面会继续分析。

3. ServiceManager.addService()

通过第 2 节,我们得知,Java 端进行binder 通信,可能是client 通过ServiceManager.getService() 获取到的server 代理,也可能是server 端通过ServiceManager.addService() 将service 注册到ServiceManager 中。但无论哪个接口,都是通过ServiceManager 在java 端的代码 BinderProxy 进行操作的。

这一节分析下addService():

frameworks/base/core/java/android/os/ServiceManager.java 

    public static void addService(String name, IBinder service, boolean allowIsolated,
            int dumpPriority) {
        try {
            getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
        } catch (RemoteException e) {
            Log.e(TAG, "error in addService", e);
        }
    }

getIServiceManager() 就是上面第 2.2 节返回的ServiceManagerProxy 对象,如果上面 2.2 节代码所示,addService() 最终调用的是 mServiceManager.addService(),而这个mServiceManager 则是将上文讲到的context 0 的BinderProxy 对象作为参数带入通过IServiceManager 获取的代理:

 mServiceManager = IServiceManager.Stub.asInterface(remote);

继续分析。

通常andorid 都会将 *.aidl 文件编译成对应的 java 文件,而最终的 java 源码是在out/soong/.intermediates/ 下,详细看本系列文章中的第七篇:Java 下的C-S

aidl 编译后的JAVA 文件都会最终打包到aidl*.srcjar 中,那具体的文件怎么确定呢?需要依赖frameworks 文件夹或者system 文件夹的 *.aidl.d 文件。

如果aidl 文件是定义在frameworks 下,那么就可以到framework 下对应的目录查找对应的 *.aidl.d 文件。

这个的 IServiceManager 就是 IServiceManager.aidl 编译后的类名

      @Override public void addService(java.lang.String name, android.os.IBinder service, boolean allowIsolated, int dumpPriority) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          _data.writeStrongBinder(service);
          ...
          boolean _status = mRemote.transact(Stub.TRANSACTION_addService, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            getDefaultImpl().addService(name, service, allowIsolated, dumpPriority);
            return;
          }
          _reply.readException();
        }
        ...
      }

proxy 的代码比较简单:

  • 通过接口writeStrongBinder() 将需要添加的 service 写到 Parcel 中;
  • 通过mRemote.transact() 进行通信,确认是否达到addService() 目的;
  • 如果上面失败,则尝试通过 getDefaultImpl() 进行 addService();

通常是走第二步,第三步的default impl 需要通过接口 setDefaultImpl() 指定。

通过第 2.2 节获悉,mRemote 就是ServiceManager 的java 端BinderProxy 对象。

3.1 writeStrongBinder()

frameworks/base/core/java/android/os/Parcel.java

    public final void writeStrongBinder(IBinder val) {
        nativeWriteStrongBinder(mNativePtr, val);
    }

JNI 接口位于android_os_Parcel.cpp 中,而此接口也是在zygote 启动是加载。这里不过多分析,详细请查看 AndroidRuntime.cpp 中数组 gRegJNI 里的成员 register_android_os_Parcel。

最终调用到JNI android_os_Parcel_writeStrongBinder():

frameworks/base/core/jni/android_os_Parcel.cpp

static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        const status_t err = parcel->writeStrongBinder(ibinderForJavaObject(env, object));
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}

来看下核心的 ibinderForJavaObject():

frameworks/base/core/jni/android_util_Binder.cpp

sp<IBinder> ibinderForJavaObject(JNIEnv* env, jobject obj)
{
    if (obj == NULL) return NULL;

    // Instance of Binder?
    if (env->IsInstanceOf(obj, gBinderOffsets.mClass)) {
        JavaBBinderHolder* jbh = (JavaBBinderHolder*)
            env->GetLongField(obj, gBinderOffsets.mObject);
        return jbh->get(env, obj);
    }

    // Instance of BinderProxy?
    if (env->IsInstanceOf(obj, gBinderProxyOffsets.mClass)) {
        return getBPNativeData(env, obj)->mObject;
    }

    ALOGW("ibinderForJavaObject: %p is not a Binder object", obj);
    return NULL;
}

纯正的service 属于 Binder 类型,所以匹配时与gBinderOffsets.mClass 匹配,该变量是也是在注册的时候赋值,详细看 int_register_android_os_Binder()

gBinderOffsets.mObject 获取的是 Binder.java 中的 mObject 对象(详细看int_register_android_os_Binder函数),也就是在Binder 构造时,从native 获取的 BBinderHolder 指针:

frameworks/base/core/java/android/os/Binder.java

    public Binder(@Nullable String descriptor)  {
        mObject = getNativeBBinderHolder();

        ...
    }

----> 

frameworks/base/core/jni/android_util_Binder.cpp

static jlong android_os_Binder_getNativeBBinderHolder(JNIEnv* env, jobject clazz)
{
    JavaBBinderHolder* jbh = new JavaBBinderHolder();
    return (jlong) jbh;
}

ibinderForJavaObject() 除了上面获取到的JavaBBinderHolder,最终就是通过该 holder 调用 get()接口,获取 servcie 的BBinder:

frameworks/base/core/jni/android_util_Binder.cpp

    sp<JavaBBinder> get(JNIEnv* env, jobject obj)
    {
        AutoMutex _l(mLock);
        sp<JavaBBinder> b = mBinder.promote();
        if (b == NULL) {
            b = new JavaBBinder(env, obj);
            ...
        }

        return b;
    }

来看下JavaBBinder:

frameworks/base/core/jni/android_util_Binder.cpp

class JavaBBinder : public BBinder
{
    ...
}

所以,writeStrongBinder() 最终就是 native 中 BBinder 写入parcel 传入transact()。

3.2 BinderProxy.transact()

详细看第 5节

3.3 JavaBBinder.onTransact()

当Client 通过 transact() 进行通信时,最终 BBinder 会触发onTransact(),对于java service,最终触发的就是 JavaBBinder.onTransact()

frameworks/base/core/jni/android_util_Binder.cpp

    status_t onTransact(
        uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) override
    {
        ...
        jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,
            code, reinterpret_cast<jlong>(&data), reinterpret_cast<jlong>(reply), flags);

gBinderOffsets.mExecTransact 也是int_register_android_os_Binder() 赋值,调用的是Binder.java 中execTransact(),这里不过多阐述,最终调用stub.onTransact。

4. ServiceManager.getService()

同addService,getService 最终也是调用到IServiceManager中:

      @Override public android.os.IBinder checkService(java.lang.String name) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        android.os.IBinder _result;
        try {
          ...
          boolean _status = mRemote.transact(Stub.TRANSACTION_checkService, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().checkService(name);
          }
          _reply.readException();
          _result = _reply.readStrongBinder();
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }

        return _result;
      }

同理addService(),这里会通过 _reply.readStrongBinder() 获得native 端传过来的binder 信息。

4.1 readStrongBinder()

最终会调用到JNI android_os_Parcel.cpp

frameworks/base/core/jni/android_os_Parcel.cpp

static jobject android_os_Parcel_readStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        return javaObjectForIBinder(env, parcel->readStrongBinder());
    }
    return NULL;
}

2.1 节已经分析过了,最终通过javaObjectForIBinder() 获取到 service 在java 端的BinderProxy 对象。

5. BinderProxy.transact()

frameworks/base/core/java/android/os/BinderProxy.java

    public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");

        ....
        try {
            return transactNative(code, data, reply, flags);
        } finally {
            ...
        }
    }

其实这里核心就是调用transactNative:

frameworks/base/core/jni/android_util_Binder.cpp

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    ...

    status_t err = target->transact(code, *data, reply, flags);
    ...
}

接下来就是BpBinder->transact,看详细看本系列文章中的第三篇:service 注册

6. 总结 

最后再来根据框架图进行总结:

  • Java client 通过getService() 获取service 的代理,getService() 最后会通过readStrongBinder() 获取 BinderProxy 对象;
  • Java client 通过transact 与service 通信,通过native 接口 transactNative() 调用到BpBinder;
  • Java service 通过addService() 注册,addService() 会通过writeStrongBinder() 调用native 接口注册到JNI;
  • Java service 注册时,会在JNI 中创建 JavaBBinderHolder,这里封装了与 native 通信的JavaBBinder,而JavaBBinder 就是继承自BBinder;
  • Java client 的调用对于 java service 最终会调用 onTransact(),并回调到java 端stub.onTransact();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/704636.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端如何进行页面优化_如何优化前端页面

优化前端页面 1 .前期准备 1.1 首页命名为index.html / index.htm / index.php等。 1.2 需要制作404页面。 1.3 文件夹结构合理。 1.4 命名使用英文且有语义性的单词&#xff0c;并提供参考文档。 2 .结构 2.1 文件头部分 2.1.1 需要提供文档声明 2.1.2 设置utf-8的编…

虚拟机创建linux系统并使用SSH工具连接

安装VMware Workstation 创建新的虚拟机或从别人那里拷贝虚拟机文件&#xff0c;下面是我从别人那里拷贝的 虚拟机存在后&#xff0c;点击播放按钮启动虚拟机 启动成功后&#xff0c;长这个样子 虚拟机启动成功后&#xff0c;不想用linux指令操作&#xff0c;而是用windows的可…

报错:Information:java: javacTask: 源发行版 8 需要目标发行版 1.8

1.背景 编译项目或启动项目报错 2.解决方法 设置为对应的版本 3.完美

DOM编程实现动态时钟

文章目录 第一种&#xff1a;第二种&#xff1a;第三种&#xff1a; 以下是一个使用 JavaScript 和 DOM 编程实现的动态实时的时钟应用&#xff1a; 第一种&#xff1a; HTML&#xff1a; <!DOCTYPE html> <html><head><meta charset"UTF-8"…

想知道PDF转高清图片软件哪个好?

张琳是一名设计师&#xff0c;她经常需要将自己的设计作品整理成PDF文档&#xff0c;以便向客户展示和交付。然而&#xff0c;有时客户需要对设计进行更详细的审查&#xff0c;而PDF格式的文件并不方便进行缩放和查看细节。这一问题让张琳感到非常困扰&#xff0c;她希望能够找…

HttpWatch用于界面单页性能测试

HttpWatch是强大的网页数据分析工具&#xff0c;作为插件可集成在IE工具栏&#xff0c;包括网页摘要、Cookies管理、缓存管理、消息头发送/接受、字符查询、POST 数据和目录管理功能、以及报告的输出。今天笔者就介绍HttpWatch的安装及使用。一、HttpWatch的安装 1、在百度下载…

UNI-APP前端项目开发实战

一、创建项目 1.创建项目分为通过HBuilderX可视化界面和vue-cli命令行两种方式创建 2.官方推荐使用HBuilderX可视化界面方式创建,主要是编译器跟随开发工具升级,vue-cli是跟在项目中的,需要手动通过 npx @dcloudio/uvm 命令升级。 3.使用HBuilderX有一个弊端就是无…

【雕爷学编程】Arduino动手做(135)---W5100 网络扩展板模块

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

中小企业怎么轻松搭建产品发行说明文档?

发行说明是产品上线必备的文档&#xff0c;它包含了项目进度的跟踪和产品版本的基础信息以及新增功能的介绍。 以下是为什么您需要发行说明的几个原因&#xff1a; 1.通知用户新功能&#xff1a;发行说明是向用户传达团队正在进行的工作和产品更新的重要途径。定期更新发行说明…

Selenium系列(三) - 详细解读针对元素常见的简单操作

针对元素有哪些常见的简单操作&#xff1f; 点击输入内容、清除内容返回元素尺寸、坐标获取元素标签文本获取元素属性值检查元素&#xff1a;是否可见、是否可点击、是否已被选择表单提交 点击右边目录即可跳转哦&#xff01; -------------->>>>>>>>…

服务器压力测试

目录 一、磁盘性能测试安装fio磁盘性能测试工具测试内容为顺序读、随机读、顺序写、随机写、随机读写结果分析 二、CPU性能测试安装sysbench性能测试工具结果分析 三、内存性能测试结果分析 四、网络性能测试测试iperf3工具测试结果分析1.测试TCP吞吐量2 . 测试UDP丢包和延迟 此…

EasyExcel读文件详解和源码分析

读取文件导入的话&#xff0c;我们经常看到下面这些方法。 //同步的返回&#xff0c;不推荐使用 EasyExcel.read(file).sheet(sheetNo).head(Class.class).headRowNumber(headRowNum)..doReadSync(); //异步的&#xff0c;通过监听器处理读到的数据。 EasyExcel.read(file).sh…

VMware Tools安装“保熟“技巧

网上关于如何安装VMware Tools也有很多帖子,但是基本很难对症下药。下面笔者给出两种情况&#xff0c;读者可根据自己概况定位自己的问题&#xff0c;从而进行解决。 如果读者安装操作系统时是如笔者如下截图 那么读者可参考这个解决方案 安装VMware Tools选项显示灰色的正确解…

【算法与数据结构】151、LeetCode反转字符串中的单词

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;本题的主要思路是先处理多余的空格&#xff0c;这个处理方法可以参考博主的这篇文章【算法与数据结构】…

PNP三极管开关电路注意事项

这个是一个NPN三极管的开关电路。 有时候我们也会用PNP的三极管搭建三极管开关电路。 当MCU输出低电平时&#xff0c;三极管BE间的电压小于开启电压&#xff0c;这时三极管饱和导通。 当MCU输出高电平&#xff0c;很多同学认为三极管BE间的电压大于开启&#xff0c;这时三极管截…

在IDEA中使用groovy脚本生成POJO

步骤1&#xff1a;打开Database窗格&#xff0c;新建数据库连接 数据库连接默认只是当前工程使用&#xff0c;想要所有IDEA窗口共享 步骤2&#xff1a;编辑groovy脚本 步骤3&#xff1a;选择一张或多张表&#xff0c;生成代码 生成效果 附&#xff1a;groovy脚本 import com.i…

POI合并单元格设置单元格样式

文章目录 设置居中设置背景颜色设置边框设置字体合并单元格实际使用运行效果 设置居中 CellStyle centerStyle wb.createCellStyle();centerStyle.setAlignment(HorizontalAlignment.CENTER); // 居中centerStyle.setVerticalAlignment(VerticalAlignment.CENTER);//垂直居中设…

桐庐旅行|桐庐团建全攻略「两天一夜行程」

桐庐被《国家地理》评选为全球25个最佳旅行目的地之一 获评「中国最美县城」&#xff0c;热门综艺《向往的生活》拍摄地 浙江的山水精华尽在「桐庐」 今夏绝对不能错过的避暑胜地 交通信息 车程&#xff1a;杭州1.5h 、上海3h、宁波2.5H、南京3.5H 尖峰推荐目的地 深澳古村 始…

原来,这就是铁路隧道R型变压器的工作真相!

铁路作为我们日常交通的重要出行设备&#xff0c;其安全稳定性极为重要。高速铁路具有行车速度快、行车密度高、负荷分布密集、自动化程度高、要求安全、正点运行的特点。铁路隧道对电力系统的供电可靠性也有非常严格的要求。铁路隧道R型变压器在铁路隧道供电系统中的主要功能是…

【线程池】史上最全的ThreadPoolExecutor源码详解

目录 一、线程池框架 1.1 第一层结构 1.2 接口简介 1.3 核心实现类 1.4 辅助类 1.5 完成服务 二、ThreadPoolExecutor的成员属性和内部类 2.1 主要成员属性以及工具方法 2.2 五种内部类 2.2.1 拒绝策略内部类&#xff08;Policy&#xff09; 2.2.2 工作线程内部类&a…