java 使用 JGit 操作 git - 工具
最编程
2024-03-09 21:24:38
...
@Slf4j
public class GitTools implements InitializingBean {
protected Log logger = LogFactory.getLog(getClass());
private String uri = "https://gitlab.xxxxx.git";
private File basedir;
public String getUri() {
return uri;
}
/**
* Username for authentication with remote repository.
*/
private String username;
/**
* Password for authentication with remote repository.
*/
private String password;
/**
* Error message for URI for git repo.
*/
public static final String MESSAGE = "You need to configure a uri for the git repository.";
private static final String FILE_URI_PREFIX = "file:";
private static final String LOCAL_BRANCH_REF_PREFIX = "refs/remotes/origin/";
/**
* Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable). Default 5 seconds.
*/
private int timeout;
/**
* Time (in seconds) between refresh of the git repository.
*/
private int refreshRate = 0;
/**
* Time of the last refresh of the git repository.
*/
private long lastRefresh;
/**
* Flag to indicate that the repository should be cloned on startup (not on demand). Generally leads to slower
* startup but faster first query.
*/
private boolean cloneOnStart;
private JGitEnvironmentRepository.JGitFactory gitFactory;
private String defaultLabel;
/**
* Factory used to create the credentials provider to use to connect to the Git repository.
*/
private GitCredentialsProviderFactory gitCredentialsProviderFactory = new GitCredentialsProviderFactory();
/**
* Transport configuration callback for JGit commands.
*/
private TransportConfigCallback transportConfigCallback;
/**
* Flag to indicate that the repository should force pull. If true discard any local changes and take from remote
* repository.
*/
private boolean forcePull;
private boolean initialized;
/**
* Flag to indicate that the branch should be deleted locally if it's origin tracked branch was removed.
*/
private boolean deleteUntrackedBranches;
/**
* Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served
* over an HTTPS connection.
*/
private boolean skipSslValidation;
public GitTools(String basedir, String username, String password, boolean skipSslValidation) {
this.username = username;
this.password = password;
this.cloneOnStart = true;
this.defaultLabel = "master";
this.forcePull = true;
this.timeout = 2000;
this.deleteUntrackedBranches = true;
this.refreshRate = 5; // 5s刷新一次
this.skipSslValidation = skipSslValidation; //是否跳过ssl
this.gitFactory = new JGitEnvironmentRepository.JGitFactory(true);
if (!StringUtils.hasText(basedir)) {
this.basedir = createBaseDir();
} else {
this.basedir = new File(basedir);
}
logger.info("basedir => " + basedir);
}
public void setTransportConfigCallback(TransportConfigCallback transportConfigCallback) {
this.transportConfigCallback = transportConfigCallback;
}
public JGitEnvironmentRepository.JGitFactory getGitFactory() {
return this.gitFactory;
}
public void setGitFactory(JGitEnvironmentRepository.JGitFactory gitFactory) {
this.gitFactory = gitFactory;
}
public void setGitCredentialsProviderFactory(GitCredentialsProviderFactory gitCredentialsProviderFactory) {
this.gitCredentialsProviderFactory = gitCredentialsProviderFactory;
}
GitCredentialsProviderFactory getGitCredentialsProviderFactory() {
return gitCredentialsProviderFactory;
}
public String getDefaultLabel() {
return this.defaultLabel;
}
public void setDefaultLabel(String defaultLabel) {
this.defaultLabel = defaultLabel;
}
public boolean isForcePull() {
return this.forcePull;
}
public void setForcePull(boolean forcePull) {
this.forcePull = forcePull;
}
public boolean isDeleteUntrackedBranches() {
return this.deleteUntrackedBranches;
}
public void setDeleteUntrackedBranches(boolean deleteUntrackedBranches) {
this.deleteUntrackedBranches = deleteUntrackedBranches;
}
public boolean isSkipSslValidation() {
return this.skipSslValidation;
}
public void setSkipSslValidation(boolean skipSslValidation) {
this.skipSslValidation = skipSslValidation;
}
@Override
public synchronized void afterPropertiesSet() throws Exception {
Assert.state(getUri() != null, MESSAGE);
initialize();
if (this.cloneOnStart) {
initClonedRepository();
}
}
public static final String SEPARATOR = "/";
public String getBranchRealName(String ref) {
return ref.substring(ref.lastIndexOf(SEPARATOR) + 1);
}
public void refreshAllBranch() throws Exception {
afterPropertiesSet();
Git git = null;
try {
git = createGitClient();
List<Ref> refs = git.branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call();
for (Ref ref : refs) {
getFilesFromRemote(git, getBranchRealName(ref.getName()));
}
} catch (RefNotFoundException e) {
throw new NoSuchLabelException("No such label: " + e);
} catch (NoRemoteRepositoryException e) {
throw new NoSuchRepositoryException("No such repository: " + getUri(), e);
} catch (GitAPIException e) {
throw new NoSuchRepositoryException("Cannot clone or checkout repository: " + getUri(), e);
} catch (Exception e) {
throw new IllegalStateException("Cannot load environment", e);
} finally {
try {
if (git != null) {
git.close();
}
} catch (Exception e) {
this.logger.warn("Could not close git repository", e);
}
}
}
public void getFilesFromRemote(Git git, String label) throws GitAPIException, IOException {
// checkout after fetch so we can get any new branches, tags, ect.
// if nothing to update so just checkout and merge.
// Merge because remote branch could have been updated before
FetchResult fetchStatus = fetch(git, label);
if (this.deleteUntrackedBranches && fetchStatus != null) {
deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git);
}
checkout(git, label);
tryMerge(git, label);
System.out.println("============= branch now =>" + label);
readFile(new File(git.getRepository().getWorkTree().getAbsolutePath() + "/env"));
}
public void readFile(File file) throws IOException {
if (file.isDirectory()) {
for (final File listFile : file.listFiles()) {
if (listFile.isDirectory()) {
readFile(listFile);
} else {
showFileContent(listFile);
}
}
} else {
showFileContent(file);
}
}
public void showFileContent(File file) throws IOException {
if (file.isFile()) {
System.out.println(System.lineSeparator() + "开始读取文件: " + file.getName());
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
String str;
while ((str = randomAccessFile.readLine()) != null) {
System.out.println(str);
}
randomAccessFile.close();
}
}
/**
* Get the working directory ready.
*
* @param label label to refresh
* @return head id
*/
public String refresh(String label) {
Git git = null;
try {
git = createGitClient();
if (shouldPull(git)) {
FetchResult fetchStatus = fetch(git, label);
if (this.deleteUntrackedBranches && fetchStatus != null) {
deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git);
}
}
// checkout after fetch so we can get any new branches, tags, ect.
// if nothing to update so just checkout and merge.
// Merge because remote branch could have been updated before
checkout(git, label);
tryMerge(git, label);
// always return what is currently HEAD as the version
return git.getRepository().findRef("HEAD").getObjectId().getName();
} catch (RefNotFoundException e) {
throw new NoSuchLabelException("No such label: " + label, e);
} catch (NoRemoteRepositoryException e) {
throw new NoSuchRepositoryException("No such repository: " + getUri(), e);
} catch (GitAPIException e) {
throw new NoSuchRepositoryException("Cannot clone or checkout repository: " + getUri(), e);
} catch (Exception e) {
throw new IllegalStateException("Cannot load environment", e);
} finally {
try {
if (git != null) {
git.close();
}
} catch (Exception e) {
this.logger.warn("Could not close git repository", e);
}
}
}
private void tryMerge(Git git, String label) {
try {
if (isBranch(git, label)) {
// merge results from fetch
merge(git, label);
if (!isClean(git, label)) {
this.logger.warn("The local repository is dirty or ahead of origin. Resetting" + " it to origin/"
+ label + ".");
// resetHard(git, label, LOCAL_BRANCH_REF_PREFIX + label);
}
}
} catch (GitAPIException e) {
throw new NoSuchRepositoryException("Cannot clone or checkout repository: " + getUri(), e);
}
}
/**
* Clones the remote repository and then opens a connection to it. Checks out to the defaultLabel if specified.
*
* @throws GitAPIException when cloning fails
* @throws IOException when repo opening fails
*/
private void initClonedRepository() throws GitAPIException, IOException {
deleteBaseDirIfExists();
Git git = cloneToBasedir();
if (git != null) {
git.close();
}
git = openGitRepository();
// Check if git points to valid repository and default label is not empty or
// null.
if (null != git && git.getRepository() != null && !StringUtils.isEmpty(getDefaultLabel())) {
// Checkout the default branch set for repo in git. This may not always be
// master. It depends on the
// admin and organization settings.
String defaultBranchInGit = git.getRepository().getBranch();
// If default branch is not empty and NOT equal to defaultLabel, then
// checkout the branch/tag/commit-id.
if (!StringUtils.isEmpty(defaultBranchInGit)
&& !getDefaultLabel().equalsIgnoreCase(defaultBranchInGit)) {
checkout(git, getDefaultLabel());
}
}
if (git != null) {
git.close();
}
}
/**
* Deletes local branches if corresponding remote branch was removed.
*
* @param trackingRefUpdates list of tracking ref updates
* @param git git instance
* @return list of deleted branches
*/
private Collection<String> deleteUntrackedLocalBranches(Collection<TrackingRefUpdate> trackingRefUpdates, Git git) {
if (CollectionUtils.isEmpty(trackingRefUpdates)) {
return Collections.emptyList();
}
Collection<String> branchesToDelete = new ArrayList<>();
for (TrackingRefUpdate trackingRefUpdate : trackingRefUpdates) {
ReceiveCommand receiveCommand = trackingRefUpdate.asReceiveCommand();
if (receiveCommand.getType() == DELETE) {
String localRefName = trackingRefUpdate.getLocalName();
if (StringUtils.startsWithIgnoreCase(localRefName, LOCAL_BRANCH_REF_PREFIX)) {
String localBranchName = localRefName.substring(
LOCAL_BRANCH_REF_PREFIX.length(),
localRefName.length()
);
branchesToDelete.add(localBranchName);
}
}
}
if (CollectionUtils.isEmpty(branchesToDelete)) {
return Collections.emptyList();
}
try {
// make sure that deleted branch not a current one
checkout(git, this.defaultLabel);
return deleteBranches(git, branchesToDelete);
} catch (Exception ex) {
String message = String.format("Failed to delete %s branches.", branchesToDelete);
warn(message, ex);
return Collections.emptyList();
}
}
private List<String> deleteBranches(Git git, Collection<String> branchesToDelete) throws GitAPIException {
DeleteBranchCommand deleteBranchCommand = git.branchDelete()
.setBranchNames(branchesToDelete.toArray(new String[0]))
// local branch can contain data which is not merged to HEAD - force
// delete it anyway, since local copy should be R/O
.setForce(true);
List<String> resultList = deleteBranchCommand.call();
this.logger.info(
String.format("Deleted %s branches from %s branches to delete.", resultList, branchesToDelete));
return resultList;
}
private Ref checkout(Git git, String label) throws GitAPIException {
CheckoutCommand checkout = git.checkout();
if (shouldTrack(git, label)) {
trackBranch(git, checkout, label);
} else {
// works for tags and local branches
checkout.setName(label);
}
return checkout.call();
}
protected boolean shouldPull(Git git) throws GitAPIException {
boolean shouldPull;
if (this.refreshRate > 0 && System.currentTimeMillis() - this.lastRefresh < (this.refreshRate * 1000)) {
return false;
}
Status gitStatus;
try {
gitStatus = git.status().call();
} catch (JGitInternalException e) {
onPullInvalidIndex(git, e);
gitStatus = git.status().call();
}
boolean isWorkingTreeClean = gitStatus.isClean();
String originUrl = git.getRepository().getConfig().getString("remote", "origin", "url");
if (this.forcePull && !isWorkingTreeClean) {
shouldPull = true;
logDirty(gitStatus);
} else {
shouldPull = isWorkingTreeClean && originUrl != null;
}
if (!isWorkingTreeClean && !this.forcePull) {
this.logger.info("Cannot pull from remote " + originUrl + ", the working tree is not clean.");
}
return shouldPull;
}
protected void onPullInvalidIndex(Git git, JGitInternalException e) {
if (!e.getMessage().contains("Short read of block.")) {
throw e;
}
if (!this.forcePull) {
throw e;
}
try {
new File(getWorkingDirectory(), ".git/index").delete();
git.reset().setMode(ResetCommand.ResetType.HARD).setRef("HEAD").call();
} catch (GitAPIException ex) {
e.addSuppressed(ex);
throw e;
}
}
@SuppressWarnings("unchecked")
private void logDirty(Status status) {
Set<String> dirties = dirties(status.getAdded(), status.getChanged(), status.getRemoved(), status.getMissing(),
status.getModified(), status.getConflicting(), status.getUntracked()
);
this.logger.warn(String.format("Dirty files found: %s", dirties));
}
@SuppressWarnings("unchecked")
private Set<String> dirties(Set<String>... changes) {
Set<String> dirties = new HashSet<>();
for (Set<String> files : changes) {
dirties.addAll(files);
}
return dirties;
}
private boolean shouldTrack(Git git, String label) throws GitAPIException {
return isBranch(git, label) && !isLocalBranch(git, label);
}
protected FetchResult fetch(Git git, String label) {
FetchCommand fetch = git.fetch();
fetch.setRemote("origin");
fetch.setTagOpt(TagOpt.FETCH_TAGS);
fetch.setRemoveDeletedRefs(this.deleteUntrackedBranches);
if (this.refreshRate > 0) {
this.setLastRefresh(System.currentTimeMillis());
}
configureCommand(fetch);
try {
FetchResult result = fetch.call();
if (result.getTrackingRefUpdates() != null && result.getTrackingRefUpdates().size() > 0) {
this.logger.info("Fetched for remote " + label + " and found " + result.getTrackingRefUpdates().size()
+ " updates");
}
return result;
} catch (Exception ex) {
String message = "Could not fetch remote for " + label + " remote: "
+ git.getRepository().getConfig().getString("remote", "origin", "url");
warn(message, ex);
return null;
}
}
private MergeResult merge(Git git, String label) {
try {
MergeCommand merge = git.merge();
merge.include(git.getRepository().findRef("origin/" + label));
MergeResult result = merge.call();
if (!result.getMergeStatus().isSuccessful()) {
this.logger.warn("Merged from remote " + label + " with result " + result.getMergeStatus());
}
return result;
} catch (Exception ex) {
String message = "Could not merge remote for " + label + " remote: "
+ git.getRepository().getConfig().getString("remote", "origin", "url");
warn(message, ex);
return null;
}
}
// private Ref resetHard(Git git, String label, String ref) {
// ResetCommand reset = git.reset();
// reset.setRef(ref);
// reset.setMode(ResetType.HARD);
// try {
// Ref resetRef = reset.call();
// if (resetRef != null) {
// this.logger.info("Reset label " + label + " to version " + resetRef.getObjectId());
// }
// return resetRef;
// } catch (Exception ex) {
// String message = "Could not reset to remote for " + label + " (current ref=" + ref + "), remote: "
// + git.getRepository().getConfig().getString("remote", "origin", "url");
// warn(message, ex);
// return null;
// }
// }
private Git createGitClient() throws IOException, GitAPIException {
File lock = new File(getWorkingDirectory(), ".git/index.lock");
if (lock.exists()) {
// The only way this can happen is if another JVM (e.g. one that
// crashed earlier) created the lock. We can attempt to recover by
// wiping the slate clean.
this.logger.info("Deleting stale JGit lock file at " + lock);
lock.delete();
}
if (new File(getWorkingDirectory(), ".git").exists()) {
return openGitRepository();
} else {
return copyRepository();
}
}
// Synchronize here so that multiple requests don't all try and delete the
// base dir
// together (this is a once only operation, so it only holds things up on
// the first
// request).
private synchronized Git copyRepository() throws IOException, GitAPIException {
deleteBaseDirIfExists();
getBasedir().mkdirs();
Assert.state(getBasedir().exists(), "Could not create basedir: " + getBasedir());
if (getUri().startsWith(FILE_URI_PREFIX)) {
return copyFromLocalRepository();
} else {
return cloneToBasedir();
}
}
private Git openGitRepository() throws IOException {
Git git = this.gitFactory.getGitByOpen(getWorkingDirectory());
return git;
}
private Git copyFromLocalRepository() throws IOException {
Git git;
File remote = new UrlResource(StringUtils.cleanPath(getUri())).getFile();
Assert.state(remote.isDirectory(), "No directory at " + getUri());
File gitDir = new File(remote, ".git");
Assert.state(gitDir.exists(), "No .git at " + getUri());
Assert.state(gitDir.isDirectory(), "No .git directory at " + getUri());
git = this.gitFactory.getGitByOpen(remote);
return git;
}
protected File getWorkingDirectory() {
return this.basedir;
}
private Git cloneToBasedir() throws GitAPIException {
CloneCommand clone = this.gitFactory.getCloneCommandByCloneRepository().setURI(getUri())
.setDirectory(getBasedir());
configureCommand(clone);
try {
return clone.call();
} catch (GitAPIException e) {
this.logger.warn("Error occured cloning to base directory.", e);
deleteBaseDirIfExists();
throw e;
}
}
public File getBasedir() {
return basedir;
}
private void deleteBaseDirIfExists() {
if (getBasedir().exists()) {
for (File file : getBasedir().listFiles()) {
try {
FileUtils.delete(file, FileUtils.RECURSIVE);
} catch (IOException e) {
throw new IllegalStateException("Failed to initialize base directory", e);
}
}
}
}
protected File createBaseDir() {
try {
final Path basedir = Files.createTempDirectory("config-repo-");
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
FileSystemUtils.deleteRecursively(basedir);
} catch (IOException e) {
logger.warn("Failed to delete temporary directory on exit: " + e);
}
}
});
return basedir.toFile();
} catch (IOException e) {
throw new IllegalStateException("Cannot create temp dir", e);
}
}
private void initialize() {
if (!this.initialized) {
this.initialized = true;
}
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
private void configureCommand(TransportCommand<?, ?> command) {
command.setTimeout(this.timeout);
if (this.transportConfigCallback != null) {
command.setTransportConfigCallback(this.transportConfigCallback);
}
CredentialsProvider credentialsProvider = getCredentialsProvider();
if (credentialsProvider != null) {
command.setCredentialsProvider(credentialsProvider);
}
}
private CredentialsProvider getCredentialsProvider() {
return this.gitCredentialsProviderFactory.createFor(this.getUri(), getUsername(), getPassword(),
"", isSkipSslValidation()
);
}
private boolean isClean(Git git, String label) {
StatusCommand status = git.status();
try {
BranchTrackingStatus trackingStatus = BranchTrackingStatus.of(git.getRepository(), label);
boolean isBranchAhead = trackingStatus != null && trackingStatus.getAheadCount() > 0;
return status.call().isClean() && !isBranchAhead;
} catch (Exception e) {
String message = "Could not execute status command on local repository. Cause: ("
+ e.getClass().getSimpleName() + ") " + e.getMessage();
warn(message, e);
return false;
}
}
private void trackBranch(Git git, CheckoutCommand checkout, String label) {
checkout.setCreateBranch(true).setName(label).setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK)
.setStartPoint("origin/" + label);
}
private
上一篇: JGit学习
下一篇: git 操作类
推荐阅读
-
使用 Linux 操作系统的基本开发工具 - vim、gcc/g++、MakeFile、gdb、yum
-
35 岁实现财务*,腾讯程序员手握2300万提前退休?-1000万房产、1000万腾讯股票、加上300万的现金,一共2300万的财产。有网友算了一笔账,假设1000万的房产用于自住,剩下1300万资产按照平均税后20-50万不等进行计算,大约花上26-60年左右的时间才能赚到这笔钱。也就是说,普通人可能奋斗一辈子,才能赚到这笔钱。在很多人还在为中年危机而惶惶不可终日的时候,有的人的35岁,就已经安全着陆,试问哪个打工人不羡慕?但问题是有这样财富积累必然有像样的实力做靠山。没有人可以不劳而获。 看到这里,肯定有人说,那么对于普通人来说,卷可能真就成了唯一的出路。但是卷也有轻松的卷,“偷懒”的卷法,对于程序员而言,刨除掉一时无法改掉的开会传统占用的大部分时间,如何把有限的时间和精力放在真正重要的架构设计、需求设计上,而不是重复的造*,编码、改bug、手动测试。因此在科技改变生活的今天,学会使用AI工具成为程序员们的必备技能。 以全栈式全自动的软件开发工具飞算SoFlu软件机器人为例,作为全球首款面向微服务架构设计和最佳实践的软件机器人,SoFlu软件机器人改变了原来手工编码的作业模式,通过可视化拖拽方式以及参数配置就能实现等同于编写复杂代码的业务逻辑,在设计业务逻辑时就完成了微服务应用开发,做到“业务即图,图即代码”,实现“软件开发,十倍提效”。 而作为飞算SoFlu软件机器人的一个重要组成部分,FuncGPT(慧函数)支持所有类型函数创建。通过自然语言描述Java函数需求,实时生成高质量、高可读性的Java函数代码。生成代码可直接复制到IDEA,或一键导入Java全自动开发工具函数库。 FuncGPT(慧函数)具有五大核心能力: ● 自然语言:通过自然语言即可生成函数,降低软件开发门槛。 ● 秒级函数开发:全自动秒级开发函数,秒级完成,效率千倍提升。 ● 一站式函数开发应用:函数生成即可直接应用,效率、安全有保障。 ● 质量可靠:生成的函数代码符合业界规范、具备良好的可读性,符合最优解。 ● 灵活生成:可以根据开发人员的具体需求生成并支持调整和修改。 以“将文件流转换为File对象”这个需求为例,以下是FuncGPT(慧函数)秒级生成的代码: / 类名:FileConverter // 函数名:convertStreamToFile // 函数功能:将文件流转换为File对象 // POM依赖包:无 import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; public class FileConverter { /** * 将文件流转换为File对象 * @param inputStream 文件流 * @param filePath 文件路径 * @param fileName 文件名 * @return 转换后的File对象 * @throws IOException 如果发生I/O异常 */ public static File convertStreamToFile(InputStream inputStream, String filePath, String fileName) throws IOException { File file = new File(filePath + File.separator + fileName); // 创建File对象 try (FileOutputStream outputStream = new FileOutputStream(file)) { // 创建文件输出流 byte buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { // 从文件流读取数据并写入文件 outputStream.write(buffer, 0, bytesRead); } } return file; // 返回转换后的File对象 } } // 函数示例 // 将文件流转换为File对象示例 // 入参:inputStream,文件流 // 入参:filePath,文件路径 // 入参:fileName,文件名 // 出参:file,转换后的File对象 // 调用示例: // InputStream inputStream = new FileInputStream("example.txt"); // String filePath = "C:\\Users\\User\\Documents"; // String fileName = "example.txt"; // File file = FileConverter.convertStreamToFile(inputStream, filePath, fileName); // System.out.println(file.getAbsolutePath); // 输出结果:例如,将文件流转换为File对象后,文件的绝对路径为:C:\Users\User\Documents\example.txt // 则输出结果为:C:\Users\User\Documents\example.txt 通过分析,不难发现以上代码:
-
使用 winzip 生成的扩展名为 -ZIP 的压缩文件通常使用".zip "扩展名,其 MIME 格式为 application/zip。(推荐学习:phpstorm) 目前,ZIP 格式属于几种主流压缩格式之一,其竞争对手包括 RAR 格式和开源的 7z 格式。 从性能对比来看,RAR 和 7z 格式比 ZIP 格式压缩率更高,而 7-Zip 由于提供免费压缩工具而逐渐在更多领域得到应用。 微软从 Windows ME 操作系统开始就内置了对 zip 格式的支持,即使用户没有在电脑上安装解压软件,也可以打开和创建 zip 格式的压缩文件,OS X 和流行的 Linux 操作系统也提供了类似的 zip 格式支持。 因此,如果要在网络上传播和分发文件,zip 格式往往是最常见的选择。
-
java keytool 证书工具使用摘要
-
使用 Java 密钥工具
-
Java 证书工具 keytool 使用摘要
-
了解 Java 的 keytool 工具以及如何使用 .crt 文件解决证书相关问题
-
Java 创建证书工具 keytool 使用摘要
-
Java 按键工具,可使用少量语音
-
Java 创建证书工具 keytool 使用详情