目录
基础配置
最近用到了APP内部更新,网上查的资料零零碎碎,完整的解决方案实际也是用不了,通过整合和自己写的代码也是实现了,励志做新年最全的实现方式,但是需要一点代码经验哦.
先给一下gradle的依赖用的是retrofit,直接用okhttp3我不太会原谅我是个菜鸟.
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//后来引入
implementation 'androidx.navigation:navigation-fragment:2.4.1'
implementation 'androidx.navigation:navigation-ui:2.4.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.8.8'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// 如果需要使用 Gson 解析 JSON
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 如果需要使用 Scalars 转换器(支持 String、int、boolean 等类型)
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'commons-logging:commons-logging:1.2'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.greenrobot:eventbus:3.2.0' // 添加 EventBus 依赖
}
这些都是很常用的依赖重点是得添加retrofit,retrofit并不是一个网络请求的框架,它是一个基于okHttp3的封装,之前自己封装过okhttp3做过基础的请求方法,但是过于难用了,避免后来者看到代码自取其辱,然后也是狠狠加入retrofit了,现在就是真香,兄弟们!!!
我们先做最简单的前置工作,也就是manifest的工作,这部分不需要代码能力只要会复制粘贴就好了.下面我上manifest
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:required="false" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" android:required="false" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" android:required="false" />
<uses-permission android:name="android.permission.READ_MEDIA_FILES" android:required="false" />
上面这个是加基础的权限,但是部分需要代码的动态申请也是需要用户自己操作的,待会会上代码的.
<application
中间是你的其他奇奇怪怪的设置总之你得把这个provider被application进行包裹
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.ximin.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
这里的@xml/file_paths需要你创建一个xml文件
那个network_security_config.xml的文件是安卓允许明文访问ip请求接口的配置,你配置请求的时候是得自己设置的,我的重点不是这个,所以要配置记得自己找别的博客弄哦.
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 允许共享外部存储的下载目录 -->
<external-path
name="external_files"
path="." />
<!-- 如果需要,也可以配置更多路径 -->
</paths>
上面这个就是file_paths.xml的代码,直接复制粘贴过去就可以爽用了.
到目前位置,没有需要敲代码的地方,只要会复制粘贴就可以了,下面我们来需要一些理解了.
实现思路
1.首先得需要获取你的软件的版本号,然后将当前安装的版本号和服务器上你上传的apk进行版本对比,得出你是否需要对你的软件进行更新.
2.实现文件写入,然后申请安装.
代码部分
1.我们先来获取我们的版本号,这里有个打包小技巧,我也是从别的博客哪里学来的,实在是太好用了,后续的版本对比我直接就抛弃了output.json这个文件的主要原因,当然详细的怎么搞只需要你会文件的写入写出也能自己解决,我就写我自己的解决方案了.
plugins {
id 'com.android.application'
}
android {
namespace 这里写你们的包名格式是单引号例如 'com.yonglu'
compileSdk 33
defaultConfig {
applicationId "com.ximin"
minSdk 26
targetSdk 33
versionCode 1
versionName "${releaseTime()}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "my_${releaseTime()}.apk"
}
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//后来引入
implementation 'androidx.navigation:navigation-fragment:2.4.1'
implementation 'androidx.navigation:navigation-ui:2.4.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.8.8'
// 在项目的 build.gradle 文件中
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// 如果需要使用 Gson 解析 JSON
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 如果需要使用 Scalars 转换器(支持 String、int、boolean 等类型)
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'commons-logging:commons-logging:1.2'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.greenrobot:eventbus:3.2.0' // 添加 EventBus 依赖
}
def releaseTime() {
return new Date().format("yyyyMMddHHmmss", TimeZone.getTimeZone("UTC"))
}
细节是这里的outputFileName这里根据了时间戳生成安装包名称,实际版本会是你的时间戳哦!!!
2.我们先创建一个utils的包,用来存放我们的工具类
我知道我废话有点多了,但是想让很多人能看懂,这里我们需要AppUtils的实现.
APPUtils
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
/**
* 为了获取软件名称做的工具类
*/
public class AppUtils {
public static String getAppVersion(Context context) {
try {
// 获取当前应用的 PackageInfo
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
// 获取版本号(数字形式)
int versionCode = packageInfo.versionCode;
// 获取版本名(字符串形式)
String versionName = packageInfo.versionName;
// 输出版本号和版本名
return versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
这里复制过去就可以使用.
下面是存放接口的service
FileDownloadService
import com.ximin.pojo.ApiResponse;
import com.ximin.pojo.UpdateResponse;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.Streaming;
import retrofit2.http.Url;
public interface FileDownloadService {
@GET
@Streaming
Call<ResponseBody> downloadFile(@Url String fileUrl);
@GET("/checkUpdate")
Call<ApiResponse<UpdateResponse>> checkUpdate(@Query("currentVersion")String currentVersion);
}
上面的downloadFile直接传递一个url就能下载了,下面的checkUpdate也是很好理解就是查询更新
创建一个retrofit的实例方便获取和直接使用
RetrofitClient
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* 下载安装的client
*/
public class DownloadRetrofitClient {
private static Retrofit retrofit;
public static Retrofit getClient(String baseUrl) {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
public static FileDownloadService getDownloadService(String baseUrl) {
return getClient(baseUrl).create(FileDownloadService.class);
}
}
写代码时直接获取下面这个FileDownloadService就行.
下面这个也是放进utils,是网络请求的封装
UpdateManager
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import com.ximin.call.DownloadProgressCallback;
import com.ximin.call.GeneralObjectCallBack;
import com.ximin.client.DownloadRetrofitClient;
import com.ximin.inface.ProgressListener;
import com.ximin.pojo.ApiResponse;
import com.ximin.pojo.UpdateResponse;
import com.ximin.service.FileDownloadService;
import java.io.File;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class UpdateManager {
private final static String TAG = "UpdateManager";
// 直接传入完整的 downloadUrl,而不再需要固定的 baseUrl
public void downloadApkWithProgress(Context context, String apkUrl, String apkFileName, DownloadProgressCallback callback) {
Log.d(TAG, "downloadApkWithProgress: 我被调用了");
FileDownloadService downloadService = DownloadRetrofitClient.getDownloadService(""); // 不需要 baseUrl因为我是直接让服务器返回了apk在服务器中的下载链接,要是你有别的需求是可以改的哦,我就不提供别的实现了,反正就是这么个事
downloadService.downloadFile(apkUrl).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
try {
long totalBytes = response.body().contentLength(); // 获取文件的总长度
// 使用异步任务或线程来处理文件保存和进度更新
new Thread(() -> {
File file = null;
try {
file = FileUtils.saveFileFromResponseWithProgress(context, response.body(), apkFileName, totalBytes, new ProgressListener() {
@Override
public void update(long bytesRead, long contentLength, boolean done) {
// 进度更新时切换到主线程
final int progress = (int) ((bytesRead * 100) / contentLength);
// 使用 handler 或 runOnUiThread 来更新 UI
new Handler(Looper.getMainLooper()).post(() -> callback.onProgress(progress));
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
// 下载完成后回调
File finalFile = file;
new Handler(Looper.getMainLooper()).post(() -> callback.onComplete(finalFile));
}).start();
} catch (Exception e) {
e.printStackTrace();
new Handler(Looper.getMainLooper()).post(() -> callback.onError("保存文件失败"));
}
} else {
new Handler(Looper.getMainLooper()).post(() -> callback.onError("下载失败"));
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
t.printStackTrace();
new Handler(Looper.getMainLooper()).post(() -> callback.onError("下载失败: " + t.getMessage()));
}
});
}
//向服务器发起请求获取软件更新的url地址
public void checkUpdate(String currentApkName, GeneralObjectCallBack<UpdateResponse> callBack) {
FileDownloadService downloadService = DownloadRetrofitClient.getDownloadService("这里需要写你的服务器请求的地址哦比如http://loacalhost:8080 localhost换成你服务器ip这里的端口当然是你接口放的端口了啦");
downloadService.checkUpdate(currentApkName).enqueue(new Callback<ApiResponse<UpdateResponse>>() {
@Override
public void onResponse(Call<ApiResponse<UpdateResponse>> call, Response<ApiResponse<UpdateResponse>> response) {
ApiResponse<UpdateResponse> apiResponse = response.body();
if (apiResponse != null && apiResponse.getCode() != 0) {
UpdateResponse updateResponse = apiResponse.getData();
callBack.onSuccess(updateResponse);
} else {
Log.d(TAG, "onResponse: 返回为空");
}
}
@Override
public void onFailure(Call<ApiResponse<UpdateResponse>> call, Throwable t) {
Log.e(TAG, "onFailure: 请求失败", t);
callBack.onFailure("请求失败" + t.getMessage());
}
});
}
}
下面是回调的接口
DownloadProgressCallback
import java.io.File;
public interface DownloadProgressCallback {
void onProgress(int progress); // 下载进度回调
void onComplete(File file); // 下载完成回调
void onError(String errorMessage); // 错误回调
}
然后是FileUtils
FileUtils
import android.content.Context;
import com.ximin.inface.ProgressListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import okhttp3.ResponseBody;
public class FileUtils {
public static File saveFileFromResponseWithProgress(Context context, ResponseBody body, String fileName, long totalBytes, ProgressListener listener) throws Exception {
File file = new File(context.getExternalFilesDir(null), fileName);
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = body.byteStream();
outputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
long totalRead = 0;
int bytesRead;
// 读取文件并写入
while ((bytesRead = inputStream.read(buffer)) != -1) {
totalRead += bytesRead;
outputStream.write(buffer, 0, bytesRead);
// 回调更新下载进度
listener.update(totalRead, totalBytes, totalRead == totalBytes);
}
outputStream.flush();
} finally {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
}
return file;
}
}
剩下的代码就是你打开app的主界面了哦,也就是mainActivity了啦要用到的声明变量
ApiResponse
public class ApiResponse<T> {
private int code;
private String msg;
private T data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public ApiResponse() {
}
public ApiResponse(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
GeneralObjectCallBack
/**
* 直接传回对象时用的通用接收类
*/
public interface GeneralObjectCallBack<T> {
void onSuccess(T data);
void onFailure(String errorMessage);
}
/**
* 更新用的url接收类
*/
public class UpdateResponse {
private boolean updateRequired;
private String latestVersion;
private String downloadUrl;
// Getters and Setters
public boolean isUpdateRequired() {
return updateRequired;
}
public void setUpdateRequired(boolean updateRequired) {
this.updateRequired = updateRequired;
}
public String getLatestVersion() {
return latestVersion;
}
public void setLatestVersion(String latestVersion) {
this.latestVersion = latestVersion;
}
public String getDownloadUrl() {
return downloadUrl;
}
public void setDownloadUrl(String downloadUrl) {
this.downloadUrl = downloadUrl;
}
}
MainActivity
private static final int INSTALL_PACKAGES_REQUEST_CODE = 1001;
private UpdateManager updateManager;
private Context context;
private ProgressBar progressBar; // 使用 ProgressBar 替代 ProgressDialog
private AlertDialog progressDialog; // 使用 AlertDialog 包裹进度条
private String apkVersion;//apk版本
然后是要在oncreat中实现的方法,给你们完整的看看怎么写的,然后你们可以自己复制粘贴到你自己代码的位置
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final int INSTALL_PACKAGES_REQUEST_CODE = 1001;
private UpdateManager updateManager;
private Context context;
private ProgressBar progressBar; // 使用 ProgressBar 替代 ProgressDialog
private AlertDialog progressDialog; // 使用 AlertDialog 包裹进度条
private String apkVersion;//apk版本
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
requestPermissions();
apkVersion = AppUtils.getAppVersion(this);
Log.d("APKFileName", "当前应用的 APK 文件名:" + apkVersion);
UpdateApp(apkVersion);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
}
// 显示进度条
public void showDownloadProgress(int progress) {
runOnUiThread(() -> {
if (progressBar != null) {
progressBar.setProgress(progress); // 更新进度
}
});
}
// 显示进度对话框
private void showProgressDialog() {
// 创建一个 AlertDialog 来显示进度条
progressDialog = new AlertDialog.Builder(this)
.setTitle("下载中...")
.setMessage("正在下载更新,请稍等...")
.setCancelable(false)
.create();
// 使用 ProgressBar 来显示进度
progressBar = new ProgressBar(this);
progressBar.setIndeterminate(false); // 设置为不确定模式
progressBar.setMax(100); // 设置最大值为 100
progressDialog.setView(progressBar);
progressDialog.show();
}
// 下载完成后的回调
public void downloadCompleted(File file) {
runOnUiThread(() -> {
if (progressDialog != null && progressDialog.isShowing()) {
progressDialog.dismiss(); // 下载完成后关闭进度条
}
Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show();
installApk(file); // 启动 APK 安装
});
}
// 启动 APK 安装
private void installApk(File file) {
Uri apkUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
}
// 检查并更新应用
private void UpdateApp(String apkFileName) {
updateManager.checkUpdate(apkFileName, new GeneralObjectCallBack<UpdateResponse>() {
@Override
public void onSuccess(UpdateResponse data) {
String downloadUrl = data.getDownloadUrl();
String version = data.getLatestVersion();
boolean updateRequired = data.isUpdateRequired();
Log.d(TAG, "onSuccess: 下载的链接是" + downloadUrl);
Log.d(TAG, "onSuccess: 版本是" + version);
if (updateRequired) {
showUpdateDialog(downloadUrl, version); // 显示更新提示
}
}
@Override
public void onFailure(String errorMessage) {
Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show();
}
});
}
// 显示更新对话框
private void showUpdateDialog(String downloadUrl, String version) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("发现新版本")
.setMessage("当前版本是"+apkVersion+"是否更新到最新版本 " + version + " ?")
.setPositiveButton("更新", (dialog, which) -> {
showProgressDialog(); // 显示下载进度条
updateManager.downloadApkWithProgress(context, downloadUrl, version, new DownloadProgressCallback() {
@Override
public void onProgress(int progress) {
showDownloadProgress(progress); // 更新进度
}
@Override
public void onComplete(File file) {
downloadCompleted(file); // 下载完成,安装 APK
}
@Override
public void onError(String errorMessage) {
}
});
})
.setNegativeButton("取消", null)
.show();
}
// 初始化视图
private void initView() {
updateManager = new UpdateManager();
context = MainActivity.this;
}
// 动态申请权限
private void requestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
String[] permissions = {
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.REQUEST_INSTALL_PACKAGES
};
ActivityCompat.requestPermissions(this, permissions, INSTALL_PACKAGES_REQUEST_CODE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] permissions = {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.REQUEST_INSTALL_PACKAGES
};
ActivityCompat.requestPermissions(this, permissions, INSTALL_PACKAGES_REQUEST_CODE);
}
}
// 权限结果回调
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == INSTALL_PACKAGES_REQUEST_CODE) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
// Toast.makeText(this, "需要权限才能继续更新", Toast.LENGTH_SHORT).show();
return;
}
}
}
}
}
把下面这些代码复制进你的项目,只需要在oncreat中直接调用就能实现自动更新了,然后就是改改url.
安卓部分的代码就这么多了,接下来是安卓java后端的代码哦!
JAVA后端SpringBoot
一样的UpdateResponse
UpdateResponse
public class UpdateResponse {
private boolean updateRequired;
private String latestVersion;
private String downloadUrl;
// Getters and Setters
public boolean isUpdateRequired() {
return updateRequired;
}
public void setUpdateRequired(boolean updateRequired) {
this.updateRequired = updateRequired;
}
public String getLatestVersion() {
return latestVersion;
}
public void setLatestVersion(String latestVersion) {
this.latestVersion = latestVersion;
}
public String getDownloadUrl() {
return downloadUrl;
}
public void setDownloadUrl(String downloadUrl) {
this.downloadUrl = downloadUrl;
}
}
FileUtils
import java.io.File;
import java.util.Arrays;
import java.util.Comparator;
public class FileUtils {
// 获取目录下最新的 APK 文件名
public static String getLatestApkFileName(String directoryPath) {
File dir = new File(directoryPath);
if (!dir.exists() || !dir.isDirectory()) {
throw new IllegalArgumentException("目录不存在或不是有效目录:" + directoryPath);
}
// 列出所有以 .apk 结尾的文件
File[] apkFiles = dir.listFiles((dir1, name) -> name.endsWith(".apk"));
if (apkFiles == null || apkFiles.length == 0) {
throw new IllegalStateException("目录中没有找到 APK 文件:" + directoryPath);
}
// 按文件名排序,确保最新的文件在最后(假设文件名带时间戳递增)
Arrays.sort(apkFiles, Comparator.comparing(File::getName));
return apkFiles[apkFiles.length - 1].getName(); // 返回最新文件名
}
}
Result
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
private String msg; //响应信息 描述字符串
private Object data; //返回的数据
//增删改 成功响应
public static Result success(){
return new Result(1,"success",null);
}
//查询 成功响应
public static Result success(Object data){
return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
public static Result exist(String msg){return new Result(2,msg,null);}
}
Controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UpdateController {
private static final String APK_DIRECTORY = "/www/uploads/app"; // 当前最新版本(带时间戳)这里是你服务器中apk的存放路径哦
@GetMapping("/checkUpdate")
public Result checkUpdate(@RequestParam("currentVersion") String currentVersion) {
UpdateResponse response = new UpdateResponse();
System.out.println("传递的版本号是" + currentVersion);
try {
// 动态获取最新版本号
String latestApkFileName = FileUtils.getLatestApkFileName(APK_DIRECTORY);
// 提取时间戳
String currentTimestamp = extractTimestampFromFilename(currentVersion);
String latestTimestamp = extractTimestampFromFilename(latestApkFileName);
System.out.println("裁切后的current: " + currentTimestamp);
System.out.println("裁切后的latest: " + latestTimestamp);
// 比较版本
if (currentTimestamp.compareTo(latestTimestamp) < 0) {
response.setUpdateRequired(true);
response.setLatestVersion(latestApkFileName);
response.setDownloadUrl("这里是放的是你用nginx映射的地址哦比如后面这个http://x.xx.xxx.xxx:8000/download/" + latestApkFileName); // 动态设置下载地址
} else {
response.setUpdateRequired(false);
}
} catch (Exception e) {
e.printStackTrace();
response.setUpdateRequired(false);
}
return Result.success(response);
}
// 提取文件名中的时间戳(假设格式为 my_yyyyMMddHHmmss.apk 或 智慧营养水肥机_yyyyMMddHHmmss.apk)
private String extractTimestampFromFilename(String filename) {
// 使用正则表达式提取时间戳部分
String regex = "(\\d{14})"; // 14位时间戳,匹配 yyyyMMddHHmmss 格式
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(regex);
java.util.regex.Matcher matcher = pattern.matcher(filename);
if (matcher.find()) {
return matcher.group(1); // 返回第一个匹配的时间戳
}
throw new IllegalArgumentException("文件名中未找到有效的时间戳: " + filename);
}
}
启动类我这里就不放了.
这里贴一个服务器返回的json数据
{
"code": 1,
"msg": "success",
"data": {
"updateRequired": false,
"latestVersion": null,
"downloadUrl": null
}
}
接下来是Nginx和一些服务器上的配置
Nginx
配置分析:
server {
listen 8000;
server_name xxx.xx.xxx.xx;这里是填你的服务器ip也可以是域名
index index.html index.htm index.php;
root /www/server/phpmyadmin;
#error_page 404 /404.html;
include enable-php.conf;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
expires 30d;
}
location ~ .*\.(js|css)?$ {
expires 12h;
}
location ~ /\.. {
deny all;
}
location /download/ {
alias /www/uploads/app/;这里就是你的apk存放路径
autoindex on;
}
access_log /www/wwwlogs/access.log;
}
1. server_name xx.xx.xx.xx;`
这里指定了服务器的 IP 地址 ` xx.xx.xx.xx`,表示只有当客户端请求的 `Host` 头部为这个 IP 地址时,Nginx 才会处理该请求。如果你通过浏览器直接访问 `http://xx.xx.xx.xx:8000` 就可以使用这个配置。
注意: 如果你希望在浏览器中通过域名访问(如 `http://example.com:8000`),则需要将 `server_name` 设置为对应的域名,而不是 IP 地址。
2. `root /www/server/phpmyadmin;
这条配置是指定了网站根目录的位置(即 `/www/server/phpmyadmin`)。
在 `/download/` 路径下使用了 `alias`,这是合理的做法,所以这条配置不会影响到 `/download/` 的访问。
如果你不需要访问 PHPMyAdmin,你可以将 `root` 改为更合适的路径,或者去掉它(只保留 `alias`)。
3. alias /www/uploads/app/;
这里使用 `alias` 来映射 `/download/` 路径到 `/www/uploads/app/` 目录,这是正确的做法。通过 `http://xx.xx.xx.xx:8000/download/` 访问时,Nginx 会映射到 `/www/uploads/app/` 目录。
注意:alias 后面的路径必须以 `/` 结尾,确保路径的正确映射。
4. autoindex on;
这条指令启用了目录列表,如果用户访问的是 `http://xx.xxx.xxx.xx:8000/download/`,并且目录下没有默认的首页文件(如 `index.html`),Nginx 会列出 `/www/uploads/app/` 目录下的文件。
5. `access_log /www/wwwlogs/access.log;`
这是记录访问日志的路径,确保日志文件存在并且有写入权限,方便你查看服务器的访问记录。
如果你希望通过 IP 地址访问你的服务器,可以直接使用 `server_name xx.xx.xx.xx;`。
端口号 需要通过 `listen` 指令来指定,已经是 `8000` 端口,不需要在 `server_name` 中添加端口。
确保你配置的路径(如 `/www/uploads/app/` 和 `/www/server/phpmyadmin`)是正确且存在的。
`autoindex on` 会启用目录列表,用户可以查看文件。
Nginx最终配置
server {
listen 8000;
server_name x.xx.xx.xx; # 这里是 IP 地址
index index.html index.htm index.php;
root /www/server/phpmyadmin; # 这个配置暂时无影响,但可以调整为更合适的目录
# error_page 404 /404.html; # 如果需要自定义 404 错误页面可以启用
include enable-php.conf; # 包含 PHP 配置(如果有 PHP 文件需要处理)
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
expires 30d;
}
location ~ .*\.(js|css)?$ {
expires 12h;
}
location ~ /\.. {
deny all; # 禁止访问隐藏文件(例如 .git、.env 等)
}
# 下载目录配置
location /download/ {
alias /www/uploads/app/;
autoindex on; # 启用目录列表,方便浏览文件
}
access_log /www/wwwlogs/access.log; # 访问日志路径
}
现在,你应该能够通过 `http://xx.xx.xx.xx:8000/download/` 访问并下载文件。但是实际拼接的可下载url是http://xx.xx.xx.xx:8000/download/yourApkname.apk,所以你要根据自己实际apk在的位置配置你的nginx,我的话是放在了服务器/www/uploads/app/这个目录下了,最后启动你的nginx就可以了,这时候你就可以通过http://xx.xx.xx.xx:8000/download/yourApkname.apk这个格式的url,来测试你是否成功配置了nginx,记得设置防火墙放行和安全组开放端口允许所有ip访问哦!!!!
实现方法已经完全给出了,我觉得确实有点难弄呢,如果有问题可以评论区问哦,如果很急也可以私信.