展开说说:Android服务之bindService解析
前面两篇文章我们分别总结了Android四种Service的基本使用以及源码层面总结一下startService的执行过程,本篇继续从源码层面总结bindService的执行过程。
本文依然按着是什么?有什么?怎么用?啥原理?的步骤来分析。
bindService使用方法和调用流程都与startService时有很多相似之处,方便的话请先阅读上一篇《展开说说:Android服务之startService解析》。
- 是什么
调用bindService()来创建,调用方可以通过一个IBinder接口和service进行通信,需要通过ServiceConnection建立连接。多用于有交互的场景。
只能调用方通过unbindService()方法来断开连接。调用方可以和Service通讯,并且一个service可以同时和多个调用方存在绑定关系,解除绑定也需要所有调用全部解除绑定之后系统才会销毁service。
2、有什么
Service和Activity一样也有自己的生命周期,也需要在AndroidManifest.xml中注册。另外bindService的使用比startService要复杂一些:第一需要中创建一个Binder子类并定义方法来给使用者调用在onBind方法中返回它的实例;第二使用者需要创建ServiceConnection对象,并在onServiceConnected回调方法调用Binder子类中定义方法。
2.1 在AndroidManifest.xml中注册
和startService注册流程一样:
<service android:name="com.example.testdemo.service.ServiceJia" />
2.2 bindService时Service的生命周期
与startService时执行的生命周期有些不同。
onCreate
它只在Service刚被创建的时刻被调用,Service在运行中,这个方法将不会被调用。也就是只有经历过onDestroy生命周期以后再次。
onBind
当另一个组件调用 bindService()想要与Service绑定(例如执行 RPC)时执行,在此方法的实现中,必须通过返回 IBinder 提供一个接口,供客户端用来与服务通信。您必须始终实现此方法;但是,如果您不想允许绑定,则应返回 null。这个方法默认时返回null。
onUnbind
调用方调用 unbindService() 来解除Service绑定时执行。
onDestroy
所有绑定到Service的调用方都解绑以后,则系统会销毁该服务。
onRebind
当Service中的onUnbind方法返回true,并且Service调用unbindService之后并没有销毁,此时重新绑定时将会触发onRebind。Service执行过onBind又onUnbind返回true并且还没执行onDestroy,等再次bindService就会执行它。
日志打印
bindService
2024-12-01 11:19:14.434 30802-30802/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onCreate:
2023-12-01 11:19:14.436 30802-30802/com.example.testdemo3 E/com.example.testdemo3.activity.ServiceActivity: onServiceConnected:
2023-12-01 11:19:14.436 30802-30802/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: JiaBinder --doSomething: start conncetion //这里不是生命周期,是binder对象调用binder内方法的打印,证明完成交互
unbindService
2023-12-01 11:21:10.705 12765-12765/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onUnbind:
2023-12-01 11:21:10.705 12765-12765/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onDestroy:
- 怎么用
因为是有交互的嘛,因此肯定比那些启动以后就成了甩手掌柜的startService使用稍微负责一些,第一需要中创建一个Binder子类并定义方法来给使用者调用在onBind方法中返回它的实例;第二使用者需要创建ServiceConnection对象,并在onServiceConnected回调方法调用Binder子类中定义方法。
具体可以参考前面的《展开说说:Android四大组件之Service使用》已经总结了使用方法,这里不在赘述。
- 啥原理,SDK版本API 30
bindService调用流程都与startService时有很多相似之处,方便的话请先阅读上一篇《展开说说:Android服务之startService解析》。
bindService的启动方法是调用
Intent serviceIntent = new Intent(ServiceActivity.this, ServiceJia.class);
bindService(serviceIntent,serviceConnection, Context.BIND_AUTO_CREATE)
然后我们顺着bindService方法开始解析源码,Go :
4.1 从ContexWrapper的bindService开始,同startService:
@Override
public boolean bindService(Intent service, ServiceConnection conn,int flags) {return mBase.bindService(service, conn, flags);
}
4.2 ContextImpl类bindService
mBase的类型是Context,但实际代码逻辑是在它的实现类ContextImpl类。
@Overridepublic boolean bindService(Intent service, ServiceConnection conn, int flags) {warnIfCallingFromSystemProcess();return bindServiceCommon(service, conn, flags, null, mMainThread.getHandler(), null,getUser());
}private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,String instanceName, Handler handler, Executor executor, UserHandle user) {// Keep this in sync with DevicePolicyManager.bindDeviceAdminServiceAsUser.IServiceConnection sd;if (conn == null) {throw new IllegalArgumentException("connection is null");}if (handler != null && executor != null) {throw new IllegalArgumentException("Handler and Executor both supplied");}if (mPackageInfo != null) {if (executor != null) {sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), executor, flags);} else {sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);}} else {throw new RuntimeException("Not supported in system context");}validateServiceIntent(service);try {IBinder token = getActivityToken();if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null&& mPackageInfo.getApplicationInfo().targetSdkVersion< android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {flags |= BIND_WAIVE_PRIORITY;}service.prepareToLeaveProcess(this);int res = ActivityManager.getService().bindIsolatedService(mMainThread.getApplicationThread(), getActivityToken(), service,service.resolveTypeIfNeeded(getContentResolver()),sd, flags, instanceName, getOpPackageName(), user.getIdentifier());if (res < 0) {throw new SecurityException("Not allowed to bind to service " + service);}return res != 0;} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}
bindService调用bindServiceCommon方法。将ServiceConnection 转为Binder的实现类IServiceConnection方便跨进程的远程服务的回调自己定义的方法。
4.3 来到LoadedApk
final @NonNull LoadedApk mPackageInfo;
因此来到LoadedApk 查看getServiceDispatcher方法:
@UnsupportedAppUsage
public final IServiceConnection getServiceDispatcher(ServiceConnection c,Context context, Handler handler, int flags) {return getServiceDispatcherCommon(c, context, handler, null, flags);
}private IServiceConnection getServiceDispatcherCommon(ServiceConnection c,Context context, Handler handler, Executor executor, int flags) {synchronized (mServices) {LoadedApk.ServiceDispatcher sd = null;ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);if (map != null) {if (DEBUG) Slog.d(TAG, "Returning existing dispatcher " + sd + " for conn " + c);sd = map.get(c);}if (sd == null) {if (executor != null) {sd = new ServiceDispatcher(c, context, executor, flags);} else {sd = new ServiceDispatcher(c, context, handler, flags);}if (DEBUG) Slog.d(TAG, "Creating new dispatcher " + sd + " for conn " + c);if (map == null) {map = new ArrayMap<>();mServices.put(context, map);}map.put(c, sd);} else {sd.validate(context, handler, executor);}return sd.getIServiceConnection();}
}
private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices
= new ArrayMap<>();
mServices记录了应用当前活动的ServiceConnection和ServiceDispatcher的映射关系,不知是否记得ActivityThread中也有一个final ArrayMap<IBinder, Service> mServices = new ArrayMap<>(); 记录了IBinder,和Service的映射关系。
继续说LoadedApk中的service哈,上面代码会判断是否存在相同的ServiceConnection,如果不存在就创建新ServiceDispatcher实例并将其存储在mService中,key时ServiceConnection,value为ServiceDispatcher,ServiceDispatcher内部存储了ServiceConnection和InnerConnection对象。在调用bindService以后Service和调用方成功建立连接时系统会通过InnerConnection调用ServiceConnection中的onServiceConnected方法,此时我们就可以利用传过来的IBinder调用Service中的方法完成交互了。这个过程支持跨进程IPC通信,比如两个进程使用AIDL通信。
4.4 ContextImpl类bindService的bindIsolatedService
返回头看上面4.2中的bindService方法,继续向下看会调用ActivityManagerService的bindIsolatedService方法:
synchronized(this) {return mServices.bindServiceLocked(caller, token, service,resolvedType, connection, flags, instanceName, callingPackage, userId);}
4.5 来到ActiveService类的bindServiceLocked
继续调用本类的bringUpServiceLocked:
bringUpServiceLocked(serviceRecord,
serviceIntent.getFlags(),
callerFg, false, false);
在调用本类realStartServiceLocked:
realStartServiceLocked(r, app, execInFg);
一般来说源码中当一个方法多次穿梭调用之后突然带上了real,那一定是离真相不远了。
app.thread.scheduleCreateService(r, r.serviceInfo,
mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
app.getReportedProcState());
这一行就很熟悉了,和上一篇startService一样,调用的ApplicationThread来创建Service实例并调用它的onCreate生命周期。
上一篇分析这个方法之下是调用onStartCommand生命周期
,没错这里也不例外下面requestServiceBindingsLocked(r, execInFg)也会去调用ApplicationThread的scheduleBindService:
r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind,
r.app.getReportedProcState());
4.6来到ApplicationThread
利用它封装的handler发送BIND_SERVICE消息:
public final void scheduleBindService(IBinder token, Intent intent,boolean rebind, int processState) {updateProcessState(processState, false);BindServiceData s = new BindServiceData();s.token = token;s.intent = intent;s.rebind = rebind;if (DEBUG_SERVICE)Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid="+ Binder.getCallingUid() + " pid=" + Binder.getCallingPid());sendMessage(H.BIND_SERVICE, s);}
接收消息:
case BIND_SERVICE:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");handleBindService((BindServiceData)msg.obj);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;
关键来咯:
private void handleBindService(BindServiceData data) {Service s = mServices.get(data.token);if (DEBUG_SERVICE)Slog.v(TAG, "handleBindService s=" + s + " rebind=" + data.rebind);if (s != null) {try {data.intent.setExtrasClassLoader(s.getClassLoader());data.intent.prepareToEnterProcess();try {if (!data.rebind) {IBinder binder = s.onBind(data.intent);ActivityManager.getService().publishService(data.token, data.intent, binder);} else {s.onRebind(data.intent);ActivityManager.getService().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);}} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}} catch (Exception e) {if (!mInstrumentation.onException(s, e)) {throw new RuntimeException("Unable to bind to service " + s+ " with " + data.intent + ": " + e.toString(), e);}}}
}
上面代码显示根据token获取Service对象,然后判断首次绑定就调用onBind生命周期,已经绑定过就调用onReBind生命周期,返回的IBinder对象就可以用来调用Service中的方法了。但是为了让调用方拿到这个IBinder就同过onServiceConnected方法回调回去,这个工作就有ActivityManagerService的publishService方法来完成。
4.6 来到ActivityManagerService
public void publishService(IBinder token, Intent intent, IBinder service) {// Refuse possible leaked file descriptorsif (intent != null && intent.hasFileDescriptors() == true) {throw new IllegalArgumentException("File descriptors passed in Intent");}synchronized(this) {if (!(token instanceof ServiceRecord)) {throw new IllegalArgumentException("Invalid service token");}mServices.publishServiceLocked((ServiceRecord)token, intent, service);}}
然后它有调用了ActiveService的publishServiceLocked方法来处理:
void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {final long origId = Binder.clearCallingIdentity();try {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "PUBLISHING " + r+ " " + intent + ": " + service);if (r != null) {Intent.FilterComparison filter= new Intent.FilterComparison(intent);IntentBindRecord b = r.bindings.get(filter);if (b != null && !b.received) {b.binder = service;b.requested = true;b.received = true;ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = r.getConnections();for (int conni = connections.size() - 1; conni >= 0; conni--) {ArrayList<ConnectionRecord> clist = connections.valueAt(conni);for (int i=0; i<clist.size(); i++) {ConnectionRecord c = clist.get(i);if (!filter.equals(c.binding.intent.intent)) {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Not publishing to: " + c);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bound intent: " + c.binding.intent.intent);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Published intent: " + intent);continue;}if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Publishing to: " + c);try {c.conn.connected(r.name, service, false);} catch (Exception e) {Slog.w(TAG, "Failure sending service " + r.shortInstanceName+ " to connection " + c.conn.asBinder()+ " (in " + c.binding.client.processName + ")", e);}}}}serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false);}} finally {Binder.restoreCallingIdentity(origId);}
}
它在for循环里调用一行代码:
c.conn.connected(r.name, service, false);
顺着代码看:
Conn的类型是是在ConnectionRecord类定义的IServiceConnection:
final IServiceConnection conn; // The client connection.
Service就是建立连接的Ibinder实例。
4.7再次来到LoadedApk类
看一下IServiceConnection类connected方法:
private static class InnerConnection extends IServiceConnection.Stub {@UnsupportedAppUsagefinal WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;InnerConnection(LoadedApk.ServiceDispatcher sd) {mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);}public void connected(ComponentName name, IBinder service, boolean dead)throws RemoteException {LoadedApk.ServiceDispatcher sd = mDispatcher.get();if (sd != null) {sd.connected(name, service, dead);}}
}
而它又调用了ActivityThread类,mActivityThread就是其中Handler子类 H ,这一步就是为了利用Handler在主线程回调给调用方的onServiceConnected:
public void connected(ComponentName name, IBinder service, boolean dead) {if (mActivityExecutor != null) {mActivityExecutor.execute(new RunConnection(name, service, 0, dead));} else if (mActivityThread != null) {mActivityThread.post(new RunConnection(name, service, 0, dead));} else {doConnected(name, service, dead);}
}
RunConnection 的实现:
private final class RunConnection implements Runnable {RunConnection(ComponentName name, IBinder service, int command, boolean dead) {mName = name;mService = service;mCommand = command;mDead = dead;}public void run() {if (mCommand == 0) {doConnected(mName, mService, mDead);} else if (mCommand == 1) {doDeath(mName, mService);}}final ComponentName mName;final IBinder mService;final int mCommand;final boolean mDead;
}
回调onServiceConnected,彻底呼应上了:
public void doConnected(ComponentName name, IBinder service, boolean dead) {ServiceDispatcher.ConnectionInfo old;ServiceDispatcher.ConnectionInfo info;synchronized (this) {if (mForgotten) {// We unbound before receiving the connection; ignore// any connection received.return;}old = mActiveConnections.get(name);if (old != null && old.binder == service) {// Huh, already have this one. Oh well!return;}if (service != null) {// A new service is being connected... set it all up.info = new ConnectionInfo();info.binder = service;info.deathMonitor = new DeathMonitor(name, service);try {service.linkToDeath(info.deathMonitor, 0);mActiveConnections.put(name, info);} catch (RemoteException e) {// This service was dead before we got it... just// don't do anything with it.mActiveConnections.remove(name);return;}} else {// The named service is being disconnected... clean up.mActiveConnections.remove(name);}if (old != null) {old.binder.unlinkToDeath(old.deathMonitor, 0);}}// If there was an old service, it is now disconnected.if (old != null) {mConnection.onServiceDisconnected(name);}if (dead) {mConnection.onBindingDied(name);}// If there is a new viable service, it is now connected.if (service != null) {mConnection.onServiceConnected(name, service);} else {// The binding machinery worked, but the remote returned null from onBind().mConnection.onNullBinding(name);}
}
至此bindService的绑定流程分析完毕!
才疏学浅,如有错误,欢迎指正,多谢。
相关文章:
展开说说:Android服务之bindService解析
前面两篇文章我们分别总结了Android四种Service的基本使用以及源码层面总结一下startService的执行过程,本篇继续从源码层面总结bindService的执行过程。 本文依然按着是什么?有什么?怎么用?啥原理?的步骤来分析。 b…...
node-sass 老版本4.14.0 安装失败解决办法
旧项目 npm install 发现 node-sass 安装 失败 切换淘宝镜像之后 不能完全解决问题。因为需要编译,本地没有Python环境不能实现 安装node-sass时,在install阶段会从Github上下载一个叫binding.node的文件,而「GitHub Releases」里的文件…...
最近很火的字幕截图生成器
网址 https://disksing.com/fake-screenshot/ 最近很火的字幕截图生成器,对于自媒体来说真的太实用了 另外透露一下,你仔细研究就会发现,这是个纯前端的项目...
使用RabbitMQ实现可靠的消息传递机制
使用RabbitMQ实现可靠的消息传递机制 大家好,我是微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿! 1. RabbitMQ简介 RabbitMQ是一个开源的消息代理软件,实现了高级消息队列协议(AMQP&…...

Function Call ReACT,Agent应用落地的加速器_qwen的function calling和react有什么不同
探索智能体Agent的未来之路:Function Call与ReACT框架的较量,谁能引领未来? 引言 各大平台出现智能体应用创建,智能体逐渐落地,背后的使用哪种框架? 随着各大平台,例如百度千帆APPbuilder、阿…...
Java的JSONPath(fastjson)使用总结
背景 最近使用json实现复杂业务配置, 因为功能需要解析读取json的中节点数据。如果使用循环或者stream处理,可以实现,但是都过于麻烦。在想能否使用更简单json读取方式,正好发现fastjson支持该功能,本文做一个记录 案例说明 示…...
【大模型】大语言模型:光鲜背后的阴影——事实准确性和推理能力的挑战
大语言模型:光鲜背后的阴影——事实准确性和推理能力的挑战 引言一、概念界定二、事实准确性的局限2.1 训练数据的偏差2.2 知识的时效性问题2.3 复杂概念的理解与表述 三、推理能力的局限3.1 表层理解与深层逻辑的脱节3.2 缺乏常识推理3.3 无法进行长期记忆和连续推…...
Java面向对象练习(1.手机类)(2024.7.4)
手机类 package Phone;public class Phone {private String brand;private int price;private String color;public Phone(){}public Phone(String brand, int price, String color){this.brand brand;this.price price;this.color color;}public void setBrand(String bra…...

智慧生活新篇章,Vatee万腾平台领航前行
在21世纪的科技浪潮中,智慧生活已不再是一个遥远的梦想,而是正逐步成为我们日常生活的现实。从智能家居的温馨便捷,到智慧城市的高效运转,科技的每一次进步都在为我们的生活增添新的色彩。而在这场智慧生活的变革中,Va…...
Spring Cloud Gateway报sun.misc.Unsafe.park(Native Method)
项目引入spring cloud gateway的jar报,启动的时候报: [2024-07-05 10:10:16.162][main][ERROR][org.springframework.boot.web.embedded.tomcat.TomcatStarter][61]:Error starting Tomcat context. Exception: org.springframework.beans.factory.Bean…...
select single , select endselect
select single , select endselect single 根据条件找到一条数据,就出来了。 select endselect是在里面循环,每次找一条,依次放到into table中,或者放到into work area中,下面append table 。 实际开发中不建议这么操…...

后端学习(一)
添加数据库包: 数据库连接时 发生错误: 解决方式: SqlConnection conn new SqlConnection("serverlocalhost;databaseMyBBSDb;uidsa;pwd123456;Encryptfalse;") ;conn.Open();SqlCommand cmd new SqlCommand("SELECT * FROM…...

【活动行】参与上海两场线下活动,教育生态行业赛总决赛活动和WAIC人工智能大会活动 - 上海活动总结
目录 背景决赛最后一公里领域范围 决赛作品AI智教相机辅导老师Copilot辅导老师Copilot雅思写作竞技场 优秀作品总结 背景 决赛 百度发起的千帆杯教育生态行业赛于2024年7月4日进行线下决赛,博主虽然没能进入决赛,但也非常荣幸能够以嘉宾身份到现场给进…...
conda 安装设置
安装anaconda 推荐官网下载和安装,最新版本是anaconda3+python3.11,个人选择。有可能找不到 Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror Tips:小白一定要全部勾选,特别第二项“add anaconda3 to my path environment variable…...

用PlantUML和语雀画UML类图
概述 首先阐述一下几个简单概念: UML:是统一建模语言(Unified Modeling Language)的缩写,它是一种用于软件工程的标准化建模语言,旨在提供一种通用的方式来可视化软件系统的结构、行为和交互。UML由Grady…...

uniapp微信小程序电子签名
先上效果图,不满意可以直接关闭这页签 新建成单独的组件,然后具体功能引入,具体功能点击签名按钮,把当前功能页面用样式隐藏掉,v-show和v-if也行,然后再把这个组件显示出来。 【签名-撤销】原理是之前绘画时…...
MetaPoint_速读
Meta-Point Learning and Refining for Category-Agnostic Pose Estimation https://arxiv.org/abs/2404.14808https://github.com/chenbys/metapointabstract 这篇文章介绍了一种名为Meta-Point Learning and Refining的框架,用于实现类别不可知的姿势估计。该框…...

数据库逆向工程工具reverse_sql
reverse_sql 是一个用于解析和转换 MySQL 二进制日志(binlog)的工具。它可以将二进制日志文件中记录的数据库更改操作(如插入、更新、删除)转换为反向的 SQL 语句,以便对系统或人为产生的误操作进行数据回滚和恢复。 *…...
四大内网穿透利器对比
本文精选四款市场上的佼佼者——巴比达、花生壳、Frp及NatApp,详细剖析它们的特点与优势,助力企业和个人用户精准选择,其中特别强调了巴比达在企业级安全访问方面的突出贡献。 1. 巴比达 特点 深度安全防护:巴比达提供全方位安…...
【LeetCode】每日一题:跳跃游戏 II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - 1] 的最小…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...

如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...

微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...

【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...

Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习) 一、Aspose.PDF 简介二、说明(⚠️仅供学习与研究使用)三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...

基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...
安卓基础(Java 和 Gradle 版本)
1. 设置项目的 JDK 版本 方法1:通过 Project Structure File → Project Structure... (或按 CtrlAltShiftS) 左侧选择 SDK Location 在 Gradle Settings 部分,设置 Gradle JDK 方法2:通过 Settings File → Settings... (或 CtrlAltS)…...