Dodge jebakan bersembunyi di kelas URLConnection

Jebakan adalah kode Java yang mengkompilasi dengan baik tetapi mengarah pada hasil yang salah, dan terkadang bencana. Menghindari jebakan dapat menghemat berjam-jam frustrasi. Pada artikel ini, saya akan menyajikan jebakan yang mungkin Anda temui saat memposting ke URL, dan kesalahan lain yang mengganggu pemula Java.

Kesalahan 5: Kompleksitas tersembunyi dalam memposting ke URL

Karena Simple Object Access Protocol (SOAP) dan panggilan prosedur jarak jauh XML (RPC) lainnya terus bertambah populer, memposting ke URL akan menjadi operasi yang lebih umum dan lebih penting - ini adalah metode untuk mengirim permintaan SOAP atau RPC ke server masing-masing.

Saat menerapkan server SOAP mandiri, saya menemukan beberapa perangkap yang terkait dengan posting ke URL, dimulai dengan desain nonintuitif dari kelas terkait URL dan diakhiri dengan perangkap kegunaan tertentu di URLConnectionkelas.

Kelas sederhana HttpClientakan menjadi cara paling langsung untuk melakukan operasi posting HTTP pada URL, tetapi setelah memindai java.net package, Anda akan kosong. Beberapa klien HTTP open source tersedia, tetapi saya belum mengujinya. (Jika Anda telah menguji klien tersebut, kirimi saya email mengenai utilitas dan stabilitas mereka.) Menariknya, ada HttpClientdalam sun.net.www.httppaket yang dikirimkan dengan JDK (dan digunakan oleh HttpURLConnection), tetapi itu bukan bagian dari API publik. Sebaliknya, java.netkelas URL dirancang untuk menjadi sangat umum dan memanfaatkan pemuatan kelas dinamis dari kedua protokol dan penangan konten. Sebelum kita beralih ke masalah khusus dengan posting, mari kita periksa struktur keseluruhan kelas yang akan kita gunakan (baik secara langsung maupun tidak langsung).

Diagram UML dari kelas-kelas terkait URL dalam java.netpaket menggambarkan keterkaitan kelas-kelas tersebut. (Diagram dibuat dengan ArgoUML- lihat Sumber untuk link.) Singkatnya, diagram hanya menunjukkan metode kunci dan tidak ada anggota data.

Perangkap 5 pusat di kelas utama: URLConnection. Namun, Anda tidak dapat membuat instance kelas itu secara langsung - ini abstrak. Sebagai gantinya, Anda akan menerima referensi ke subkelas tertentu URLConnectionmelalui URLkelas tersebut.

Memang, angka di atas memang rumit. Urutan umum peristiwa berfungsi seperti ini: URL statis biasanya menentukan lokasi beberapa konten dan protokol yang diperlukan untuk mengaksesnya. Pertama kali URLkelas digunakan, sebuah URLStreamHandlerFactorysingleton dibuat. Pabrik tersebut menghasilkan URLStreamHandleryang memahami protokol akses yang ditentukan dalam URL. The URLStreamHandlerinstantiates yang sesuai URLConnectionkelas, yang membuka koneksi ke URL dan instantiates yang tepat ContentHandleruntuk menangani konten di URL.

Jadi apa masalahnya? Karena desain kelas yang terlalu umum, mereka kekurangan model konseptual yang jelas. Dalam bukunya, The Design of Everyday Things (Doubleday, 1990), Donald Norman menyatakan bahwa salah satu prinsip utama desain yang baik adalah model konseptual yang baik yang memungkinkan kita untuk "memprediksi efek tindakan kita". Beberapa masalah dengan URLmodel konseptual kelas meliputi:

  • The URLkelas konseptual kelebihan beban. URL hanyalah abstraksi untuk alamat atau titik akhir. Faktanya, desain yang lebih baik akan menampilkan subkelas URL yang membedakan sumber daya statis dari layanan dinamis. Hilang secara konseptual adalah URLClientkelas yang menggunakan URL sebagai titik akhir untuk membaca atau menulis.
  • The URLkelas condong ke mengambil data dari URL. Ada tiga metode untuk mengambil konten dari URL, tetapi hanya satu yang menulis data ke URL. Perbedaan tersebut akan lebih baik disajikan dengan subkelas URL untuk sumber daya statis yang hanya memiliki operasi baca; subclass URL untuk layanan dinamis akan memiliki metode baca dan tulis. Desain itu akan memberikan model konseptual yang bersih untuk digunakan.
  • Memanggil penangan protokol penangan "aliran" membingungkan karena tujuan utamanya adalah untuk menghasilkan (atau membangun) koneksi. Model yang lebih baik akan meniru Java API for XML Processing (JAXP), di mana a DocumentBuilderFactorymenghasilkan DocumentBuilder, yang menghasilkan Document. Menerapkan model itu ke kelas URL akan menghasilkan a URLConnectorFactoryyang menghasilkan URLConnectoryang menghasilkan URLConnection.

Sekarang Anda siap untuk menangani URLConnectionkelas dan mencoba mengirim ke URL. Tujuannya adalah untuk membuat program Java sederhana yang memposting beberapa teks ke program antarmuka gateway umum (CGI). Untuk menguji program, saya membuat program CGI sederhana di C yang menggemakan (dalam bungkus HTML) apa pun yang lolos ke dalamnya. (Lihat Sumberdaya untuk mengunduh kode sumber untuk program apapun di artikel ini, termasuk program CGI.)

The URLConnectionkelas memiliki getOutputStream()dan getInputStream()metode, seperti Socketkelas. Berdasarkan kesamaan itu, Anda berharap bahwa mengirim data ke URL semudah menulis data ke file Socket. Berbekal informasi tersebut dan pemahaman tentang protokol HTTP, kami menulis program pada Listing 5.1 BadURLPost.java,.

Daftar 5.1 BadURLPost.java

paket com.javaworld.jpitfalls.article3; import java.net. *; impor java.io. *; public class BadURLPost {public static void main (String args []) {// dapatkan koneksi HTTP ke POST ke if (args.length <1) {System.out.println ("PENGGUNAAN: java GOV.dia.mditds.util .BadURLPost url "); System.exit (1); } coba {// dapatkan url sebagai string String surl = args [0]; URL url = URL baru (surl); URLConnection con = url.openConnection (); System.out.println ("Menerima:" + con.getClass (). GetName ()); con.setDoInput (true); con.setDoOutput (true); con.setUseCaches (false); String msg = "Hai SERVER HTTP! Cepat halo!"; con.setRequestProperty ("CONTENT_LENGTH", "5"); // Tidak dicentang con.setRequestProperty ("Stupid", "Nonsense"); System.out.println ("Mendapatkan aliran input ...");InputStream adalah = con.getInputStream (); System.out.println ("Mendapatkan aliran keluaran ..."); OutputStream os = con.getOutputStream (); / * con.setRequestProperty ("CONTENT_LENGTH", "" + msg.length ()); Kesalahan akses ilegal - tidak dapat mengatur ulang metode. * / OutputStreamWriter osw = OutputStreamWriter baru (os); osw.write (msg); osw.flush (); osw.close (); System.out.println ("Setelah pembilasan aliran keluaran."); // ada tanggapan? InputStreamReader isr = InputStreamReader baru (adalah); BufferedReader br = new BufferedReader (isr); Garis string = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} menangkap (Throwable t) {t.printStackTrace (); }}}setRequestProperty ("CONTENT_LENGTH", "" + msg.length ()); Kesalahan akses ilegal - tidak dapat mengatur ulang metode. * / OutputStreamWriter osw = OutputStreamWriter baru (os); osw.write (msg); osw.flush (); osw.close (); System.out.println ("Setelah pembilasan aliran keluaran."); // ada tanggapan? InputStreamReader isr = InputStreamReader baru (adalah); BufferedReader br = new BufferedReader (isr); Garis string = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} menangkap (Throwable t) {t.printStackTrace (); }}}setRequestProperty ("CONTENT_LENGTH", "" + msg.length ()); Kesalahan akses ilegal - tidak dapat mengatur ulang metode. * / OutputStreamWriter osw = OutputStreamWriter baru (os); osw.write (msg); osw.flush (); osw.close (); System.out.println ("Setelah pembilasan aliran keluaran."); // ada tanggapan? InputStreamReader isr = InputStreamReader baru (adalah); BufferedReader br = new BufferedReader (isr); Garis string = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} menangkap (Throwable t) {t.printStackTrace (); }}}// ada tanggapan? InputStreamReader isr = InputStreamReader baru (adalah); BufferedReader br = new BufferedReader (isr); Garis string = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} menangkap (Throwable t) {t.printStackTrace (); }}}// ada tanggapan? InputStreamReader isr = InputStreamReader baru (adalah); BufferedReader br = new BufferedReader (isr); Garis string = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} menangkap (Throwable t) {t.printStackTrace (); }}}

Serangkaian Listing 5.1 menghasilkan:

E: \ class \ com \ javaworld \ jpitfalls \ article3> java -Djava.compiler = NONE com.javaworld.jpitfalls.article3.BadURLPost //localhost/cgi-bin/echocgi.exe Menerima: sun.net.www.protocol .http.HttpURLConnection Mendapatkan aliran masukan ... Mendapatkan aliran keluaran ... java.net.ProtocolException: Tidak dapat menyetel ulang metode: sudah terhubung di java.net.HttpURLConnection.setRequestMethod (HttpURLConnection.java:10 2) di matahari .net.www.protocol.http.HttpURLConnection.getOutputStream (HttpURLCo nnection.java:349) di com.javaworld.jpitfalls.article2.BadURLPost.main (BadURLPost.java:38) 

Ketika kami mencoba untuk mendapatkan HttpURLConnectionaliran keluaran kelas, program memberi tahu kami bahwa kami tidak dapat mengatur ulang metode karena kami sudah terhubung. Javadoc untuk HttpURLConnectionkelas tidak berisi referensi untuk menyetel metode. Program ini mengacu pada metode HTTP, yaitu POSTsaat kita mengirim data ke URL dan GETsaat kita mengambil data dari URL.

The getOutputStream()Metode menyebabkan program untuk melempar ProtocolExceptiondengan pesan error "Can not ulang metode." Kode sumber JDK mengungkapkan bahwa pesan kesalahan terjadi karena getInputStream()metode memiliki efek samping pengiriman permintaan (yang metode permintaan defaultnya GET) ke server Web. Ini mirip dengan efek samping pada ObjectInputStreamdan ObjectOutputStreamkonstruktor, yang dirinci dalam buku saya, Java Pitfalls: Time Saving Solutions and Workarounds to Enhancement Programs (John Wiley & Sons, 2000).

Perangkap adalah asumsi bahwa metode getInputStream()dan getOutputStream()berperilaku seperti yang mereka lakukan untuk Socketsambungan. Karena mekanisme yang mendasari untuk berkomunikasi ke server Web sebenarnya adalah Socket, ini bukanlah asumsi yang tidak masuk akal. Implementasi yang lebih baik HttpURLConnectionakan menunda efek samping sampai pembacaan atau penulisan awal ke aliran input atau output masing-masing. Anda dapat melakukannya dengan membuat HttpInputStreamdan HttpOutputStream, yang akan membuat Socketmodel tetap utuh. Anda dapat berargumen bahwa HTTP adalah protokol tanpa status permintaan / respons, dan Socketmodelnya tidak cocok. Namun demikian, API harus sesuai dengan model konseptual; jika model saat ini identik dengan aSocketkoneksi, itu harus berperilaku seperti itu. Jika tidak, Anda telah meregangkan batasan abstraksi terlalu jauh.

Selain pesan error tersebut, ada dua masalah pada kode diatas:

  • The setRequestProperty()parameter metode tidak diperiksa, yang kami menunjukkan dengan menetapkan sifat yang disebut bodoh dengan nilai omong kosong. Karena properti tersebut benar-benar masuk ke permintaan HTTP dan tidak divalidasi oleh metode (sebagaimana mestinya), Anda harus lebih berhati-hati untuk memastikan bahwa nama dan nilai parameter sudah benar.
  • Meskipun kodenya diberi komentar, juga ilegal untuk mencoba menyetel properti permintaan setelah mendapatkan aliran masukan atau keluaran. Dokumentasi untuk URLConnectionmenunjukkan urutan untuk menyiapkan koneksi, meskipun tidak menyatakan bahwa itu adalah urutan wajib.

Jika kita tidak memiliki kemewahan untuk memeriksa kode sumber - yang seharusnya tidak menjadi persyaratan untuk menggunakan API - kita akan direduksi menjadi trial and error, cara terburuk untuk memprogram. Baik dokumentasi maupun API HttpURLConnectionkelas tersebut tidak memberi kami pemahaman apa pun tentang bagaimana protokol diimplementasikan, jadi kami dengan lemah berusaha membalik urutan panggilan ke getInputStream()dan getOutputStream(). Kode 5.2,, BadURLPost1.javaadalah versi singkat dari program itu.

Daftar 5.2 BadURLPost1.java

paket com.javaworld.jpitfalls.article3; import java.net. *; impor java.io. *; public class BadURLPost1 {public static void main (String args []) {// ... try {// ... System.out.println ("Mendapatkan aliran keluaran ..."); OutputStream os = con.getOutputStream (); System.out.println ("Mendapatkan aliran input ..."); InputStream adalah = con.getInputStream (); // ...} catch (Throwable t) {t.printStackTrace (); }}}

Jalankan Listing 5.2 menghasilkan:

E: \ class \ com \ javaworld \ jpitfalls \ article3> java -Djava.compiler = NONE com.javaworld.jpitfalls.article3.BadURLPost1 //localhost/cgi-bin/echocgi.exe Menerima: sun.net.www.protocol .http.HttpURLConnection Mendapatkan aliran keluaran ... Mendapatkan aliran masukan ... Setelah pembilasan aliran keluaran. baris: baris: baris program Echo CGI: baris: baris:

Gema

line: line: Tidak ada konten! KESALAHAN! baris: baris:

Meskipun program mengkompilasi dan berjalan, program CGI melaporkan bahwa tidak ada data yang dikirim! Mengapa? Efek samping dari getInputStream()menggigit kita lagi, menyebabkan POSTpermintaan dikirim sebelum apa pun ditempatkan di buffer keluaran posting, sehingga mengirim POSTpermintaan kosong .

Setelah gagal dua kali, kami memahami bahwa itu getInputStream()adalah metode kunci yang sebenarnya menulis permintaan ke server. Oleh karena itu kita harus melakukan operasi secara serial (buka keluaran, tulis, buka masukan, baca) seperti yang kita lakukan pada Listing 5.3 GoodURLPost,.

Daftar 5.3 GoodURLPost.java