Это дополнительный вопрос для подключения приложения Android к FTP-серверу на Java. Я пытаюсь создать крошечный менеджер FTP-хоста с FTPHostProfile
класс и FTPHostProfiles
класс.
Экспериментальная реализация
Название проекта: FTPHostManagement
FTPHostProfile
реализация класса:package com.example.ftphostmanagement; public class FTPHostProfile { private String ProfileName = ""; private String FTPHostName = ""; private int FTPPort = 21; private String FTPUsername = ""; private String FTPPassword = ""; public FTPHostProfile(String hostnameInput, int portInput, String usernameInput, String passwordInput) { this.ProfileName = FTPHostName; this.FTPHostName = hostnameInput; this.FTPPort = portInput; this.FTPUsername = usernameInput; this.FTPPassword = passwordInput; } public FTPHostProfile(String profileNameInput, String hostnameInput, int portInput, String usernameInput, String passwordInput) { this.ProfileName = profileNameInput; this.FTPHostName = hostnameInput; this.FTPPort = portInput; this.FTPUsername = usernameInput; this.FTPPassword = passwordInput; } public String GetProfilename() { return this.ProfileName; } public String GetHostname() { return this.FTPHostName; } public int GetPort() { return this.FTPPort; } public String GetUsername() { return this.FTPUsername; } public String GetPassword() { return this.FTPPassword; } }
FTPHostProfiles
реализация класса:package com.example.ftphostmanagement; import android.util.Log; import org.apache.commons.net.ftp.FTP; import java.util.ArrayList; import java.util.stream.Collectors; public class FTPHostProfiles { private ArrayList<FTPHostProfile> Collection = new ArrayList<>(); /// Empty constructor public FTPHostProfiles() { } /// Single FTPHostProfile handler public FTPHostProfiles(FTPHostProfile input) { this.Collection.add(input); } /// Multiple FTPHostProfiles handler public FTPHostProfiles(ArrayList<FTPHostProfile> input) { this.Collection = input; } public FTPHostProfiles(FTPHostProfiles input) { this.Collection.addAll(input.Collection); } public FTPHostProfiles AddProfile(FTPHostProfile input) { this.Collection.add(input); return this; } public FTPHostProfiles AddProfiles(ArrayList<FTPHostProfile> input) { this.Collection.addAll(input); return this; } public FTPHostProfiles AddProfiles(FTPHostProfiles input) { this.Collection.addAll(input.Collection); return this; } public FTPHostProfile GetProfile(String profileNameInput) { var filteredResult = this.Collection.stream().filter(element -> (element.GetProfilename().equals(profileNameInput))).collect(Collectors.toList()); if (filteredResult.stream().count() >= 1) { return filteredResult.get(0); } else { Log.d("GetProfile", "no such item in stored profiles"); throw new IllegalStateException("no such item in stored profiles"); } } }
FTPconnection
реализация класса:package com.example.ftphostmanagement; import android.util.Log; import java.io.IOException; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import org.apache.commons.net.SocketClient; import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPReply; public class FTPconnection { FTPHostProfiles mFTPHostProfiles = new FTPHostProfiles(); public FTPconnection(FTPHostProfiles input) { this.mFTPHostProfiles = input; } public FTPClient connectftp(String profilename) { // Reference: https://stackoverflow.com/a/8761268/6667035 FTPClient ftp = new FTPClient(); try { var FTPHostProfile = mFTPHostProfiles.GetProfile(profilename); // Reference: https://stackoverflow.com/a/55950845/6667035 // The argument of `FTPClient.connect` method is hostname, not URL. ftp.connect(FTPHostProfile.GetHostname(), FTPHostProfile.GetPort()); boolean status = ftp.login(FTPHostProfile.GetUsername(), FTPHostProfile.GetPassword()); if (status) { ftp.enterLocalPassiveMode(); ftp.setFileType(FTP.BINARY_FILE_TYPE); ftp.sendCommand("OPTS UTF8 ON"); } System.out.println("status : " + ftp.getStatus()); } catch (UnknownHostException ex) { ex.printStackTrace(); } catch (SocketException en) { en.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return ftp; } }
Настройка разрешений пользователя
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.ftphostmanagement"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.FTPHostManagement"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> </manifest>
build.gradle
plugins { id 'com.android.application' } android { compileSdk 30 defaultConfig { applicationId "com.example.ftphostmanagement" minSdk 26 targetSdk 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') // https://mvnrepository.com/artifact/commons-net/commons-net implementation group: 'commons-net', name: 'commons-net', version: '20030805.205232' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }
Полный код тестирования
MainActivity.java
выполнение:package com.example.ftphostmanagement; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.widget.Toast; import org.apache.commons.net.ftp.FTPClient; public class MainActivity extends AppCompatActivity { private FTPClient ftpClient; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new FtpTask().execute(); } void ShowToast(String Text, int Duration) { Context context = getApplicationContext(); CharSequence text = Text; int duration = Duration; Toast toast = Toast.makeText(context, text, duration); toast.show(); } // AsyncTask must be subclassed to be used. The subclass will override at least one method // `doInBackground(Params...)`, and most often will override a second one `onPostExecute(Result)` // Reference: https://developer.android.com/reference/android/os/AsyncTask?authuser=4 // Reference: https://stackoverflow.com/a/12447497/6667035 private class FtpTask extends AsyncTask<Void, Void, FTPClient> { // `doInBackground` invoked on the background thread immediately after `onPreExecute()` // finishes executing. This step is used to perform background computation // that can take a long time. The parameters of the asynchronous task are // passed to this step. The result of the computation must be returned by // this step and will be passed back to the last step. This step can also use // `publishProgress(Progress...)` to publish one or more units of progress. // These values are published on the UI thread, in the `onProgressUpdate(Progress...)` // step. protected FTPClient doInBackground(Void... args) { FTPHostProfiles ftpHostProfiles = new FTPHostProfiles(); ftpHostProfiles.AddProfile(new FTPHostProfile( "Profile1", "Hostname1", 21, "Username1", "Password1")); ftpHostProfiles.AddProfile(new FTPHostProfile( "Profile2", "Hostname2", 21, "Username2", "Password2")); FTPconnection ftpConnect = new FTPconnection(ftpHostProfiles); FTPClient ftp = ftpConnect.connectftp("Profile1"); return ftp; } // `onPostExecute` invoked on the UI thread after the background computation finishes. // The result of the background computation is passed to this step as // a parameter. protected void onPostExecute(FTPClient result) { Log.v("FTPTask","FTP connection complete"); ftpClient = result; //Where ftpClient is a instance variable in the main activity Log.v("Boolean.toString(ftpClient.isConnected())", Boolean.toString(ftpClient.isConnected())); ShowToast(Boolean.toString(ftpClient.isConnected()), Toast.LENGTH_SHORT); } } }
Все предложения приветствуются.
Сводная информация:
На какой вопрос это продолжение?
Приложение Android подключается к FTP-серверу на Java
Какие изменения были внесены в код с момента последнего вопроса?
В
FTPHostProfile
класс иFTPHostProfiles
реализация класса является ключевой частью этого поста.Почему запрашивается новый обзор?
Если есть какие-то улучшения, пожалуйста, дайте мне знать.
1 ответ
Важное предостережение: я ничего не знаю о специальных соглашениях о кодировании для Android, которые отличаются от обычных java.
Общий
Желательно использовать строчные буквы для акронимов в именах классов. FtpHostProfile легче читать, чем FTPHostProfile.
Классы, которые не были тщательно разработаны для расширения, должны быть окончательными.
Идиоматично, когда имена переменных начинаются со строчной буквы. profileName
, так далее.
Все переменные, которые не изменяются после создания объекта, должны быть отмечены. final
. Это снижает когнитивную нагрузку на читателя, поскольку он может быть уверен, что не будет переназначен после построения.
Идиоматично, когда имена методов начинаются со строчной буквы.
Идиоматично, когда фигурные скобки находятся на той же строке, что и объявление метода, а не на отдельной строке.
Предпочитайте использовать наиболее подходящий тип в левой части назначений. List
вместо ArrayList
, так далее.
Нет необходимости присваивать переменным значение, если они будут немедленно перезаписаны в конструкторе.
FTPHostProfile
Нет смысла назначать переменным значение «» «, если вы просто собираетесь перезаписать их в обоих конструкторах.
Почему в первом конструкторе для имени профиля устанавливается значение ""
установив для него значение FTPHostName
? Если вы хотите, чтобы он был пустым, сделайте его пустым. Это особенно сбивает с толку, потому что FTPHostName переназначается в следующей строке.
Строки длиной более сотни символов читаются с трудом. Подумайте о том, чтобы разбить объявление второго конструктора на несколько строк или, по крайней мере, начать объявления переменных в отдельной строке.
username
а также hostname
иногда записываются одним словом. Profilename
это не английское слово, поэтому его следует разбить на ProfileName
. Имена переменных также несовместимы с именами методов.
FTPHostProfiles
Почему конструктор номер 3 требует, чтобы несколько профилей были отправлены в `ArrayList`? Что не так с LinkedList или (ах!) С каким-то набором? Этот конструктор должен иметь коллекцию. То же самое для addProfiles.
Непосредственно присвоение input
этого конструктора в Collection
плохая идея. Теперь вы потеряли контроль над своими внутренностями — кто бы ни прошел input
все еще имеет указатель на него и может изменять его содержимое без ведома или согласия этого класса. Просто позвони addAll
в вашей коллекции — вы получаете значения, хранящиеся в input
, и никто не сможет испортить вашу коллекцию.
Collection
не лучшее имя переменной. Он не описательный и противоречит имени класса. Рассмотреть возможность profiles
или же ftpHostProfiles
как имена переменных.
Вам действительно нужны как конструкторы, так и методы для добавления профилей? Это кажется излишним.
Имея addProfile
методы return this
удобно для создания цепочки, но подумайте, нужно ли вам вместо этого вернуть что-то еще, например, было ли добавление успешным или нет.
getProfile
проясняет, что profiles
должен быть Map
, а не List
.
FTP-соединение
Буква `c` должна начинаться с заглавной буквы.
mFtpHostProfiles
вероятно должно быть private final
.
У этого класса есть несколько неиспользуемых импортов.
Вероятно, вы можете назвать переменную profile
Я думаю, что контекста достаточно, чтобы FTPHost
является избыточным.
Возможно loggedIn
было бы более наглядным, чем status
?
Если вы внесли все эти изменения, ваш код мог бы выглядеть примерно так:
FtpConnection:
public final class FtpConnection {
private final FtpHostProfiles mFtpHostProfiles = new FtpHostProfiles();
public FtpConnection(FtpHostProfiles input) {
this.mFtpHostProfiles.addProfiles(input);
}
public FTPClient connectftp(String profileName) {
// Reference: https://stackoverflow.com/a/8761268/6667035
FTPClient ftp = new FTPClient();
try {
FtpHostProfile profile = mFtpHostProfiles.getProfile(profileName);
// Reference: https://stackoverflow.com/a/55950845/6667035
// The argument of `FtpClient.connect` method is hostname, not URL.
ftp.connect(profile.getHostname(), profile.getPort());
boolean loggedIn = ftp.login(profile.getUsername(), profile.getPassword());
if (loggedIn) {
ftp.enterLocalPassiveMode();
ftp.setFileType(FTP.BINARY_FILE_TYPE);
ftp.sendCommand("OPTS UTF8 ON");
}
System.out.println("status : " + ftp.getStatus());
} catch (UnknownHostException ex) {
ex.printStackTrace();
} catch (SocketException en) {
en.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return ftp;
}
}
FtpHostProfile:
public final class FtpHostProfile {
private final String profileName;
private final String ftpHostName;
private final int ftpPort;
private final String ftpUserName;
private final String ftpPassword;
public FtpHostProfile(String hostNameInput, int portInput, String userNameInput, String passwordInput) {
this.profileName = "";
this.ftpHostName = hostNameInput;
this.ftpPort = portInput;
this.ftpUserName = userNameInput;
this.ftpPassword = passwordInput;
}
public FtpHostProfile(
String profileNameInput,
String hostnameInput,
int portInput,
String usernameInput,
String passwordInput) {
this.profileName = profileNameInput;
this.ftpHostName = hostnameInput;
this.ftpPort = portInput;
this.ftpUserName = usernameInput;
this.ftpPassword = passwordInput;
}
public String getProfileName() {
return this.profileName;
}
public String getHostname() {
return this.ftpHostName;
}
public int getPort() {
return this.ftpPort;
}
public String getUsername() {
return this.ftpUserName;
}
public String getPassword() {
return this.ftpPassword;
}
}
FtpHostProfiles:
public final class FtpHostProfiles {
private final Map<String, FtpHostProfile> profiles = new HashMap<>();
public FtpHostProfiles() {
// Empty constructor
}
public FtpHostProfiles addProfile(FtpHostProfile input) {
this.profiles.put(input.getProfileName(), input);
return this;
}
public FtpHostProfiles addProfiles(Collection<FtpHostProfile> input) {
input.stream().forEach(profile -> profiles.put(profile.getProfileName(), profile));
return this;
}
public FtpHostProfiles addProfiles(FtpHostProfiles input) {
this.profiles.putAll(input.profiles);
return this;
}
public FtpHostProfile getProfile(String profileNameInput) {
FtpHostProfile profile = profiles.get(profileNameInput);
if (profile == null) {
Log.d("GetProfile", "No such item in stored profiles");
throw new IllegalStateException("No such item in stored profiles");
}
return profile;
}
}