И така, след 2 дни ровене в проблема и научаване за класа URLConnection
, най-накрая успях да измисля моя собствена (вероятно груба) реализация, която бих искал да споделя тук, в случай че някой с толкова малко познания като мен се случи да се спъвам в тази тема. Отново основната идея беше да се поставят само определени типове файлове в кеша, а не всичко. Избрах да съхранявам jpg
, png
, gif
и js
файлове, тъй като ги смятах за най-натоварващи, въпреки че всеки друг файлов формат също трябва да е възможен. На първо място: browser.getEngine().setUserDataDirectory(...)
определено НЕ върши работата. Все още нямам представа за какво е добър, но със сигурност не съхранява файлове с изображения %)
Вместо това, това, което направих, беше основно създаване на 5 класа:
CachedResource
: Състои се от byte[]
масив, съдържащ необработените данни на ресурса и някаква мета информация (заглавни полета, lastModified)
ResourceCache
: Съдържа всички текущо кеширани ресурсни обекти.
MyHttpUrlConnection
(разширява sun.net.www.protocol.http.HttpURLConnection
): Клас обвивка, който отговаря за извличането на файла, към който сочи дадения му URL адрес. Той прави цялата мрежова магия.
CachedUrlConnection
(разширява java.net.URLConnection
): (почти) празна реализация, която вече има всички необходими данни и само чака системата да я извика.
MyUrlConnectionHandler
(разширява sun.net.www.protocol.http.Handler
): Този клас се регистрира при стартиране на приложението и решава кога да използва кой URLConnection
(вижте по-долу).
Класовете ResourceCache
, CachedResource
и CachedUrlConnection
са доста малки и лесни за писане. Проектирах кеша на ресурсите, за да съпоставя ресурса на url със съответния му CachedResource
обект, по този начин: ConcurrentHashMap<URL, CachedResource>
плюс функция за получаване и addResource(...)
. Добавих някои други неща към него като съхраняване на файлове локално, но това води извън темата.
След това внедрих класа CachedUrlConnection
както следва:
public class CachedUrlConnection extends URLConnection {
private CachedResource resource;
private ByteArrayInputStream inputStream;
/* Constructors */
public CachedUrlConnection(URL url, CachedResource resource) throws IOException {
super(url);
this.resource = resource;
this.inputStream = new ByteArrayInputStream(resource.getByteData());
}
@Override
public void connect() throws IOException {
// No need to do anything.
}
/* Object Methods */
/* Getters and Setters */
@Override
public String getHeaderField(int index) { ... }
@Override
public String getHeaderField(String key) { ... }
@Override
public Map<String, List<String>> getHeaderFields() { ... }
@Override
public InputStream getInputStream() throws IOException {
return inputStream; // <---- Here, the system can grab the data.
}
}
Когато разглеждате изходния код на URLConnection (например тук), бързо ще забележите, че повечето имплементации на метода са фиктивни, които или връщат null
, или хвърлят UnknownServiceException
.
Това е важно: не знам точно кое от тях трябва да внедрите!
За да разбера, използвах класа MyHttpUrlConnection
и добавих към почти всяка функция
System.out.println("function xyz called!");
super.xyz();
но ме мързеше и не ги проверих всичките. Засега изглежда всичко работи добре %)
Следващият клас беше MyHttpUrlConnection
. Не съм 100% сигурен дали наистина трябваше да презапиша класа HttpURLConnection
, но все пак го направих, защото той има конструктор protected
, който имплицитно ще бъде извикан с нов sun.net.www.protocol.http.Handler
. Този манипулатор очевидно няма да следва нашата http политика, така че просто исках да съм сигурен (вж. изходен код ред 801). Така класът изглежда доста празен:
public class MyHttpUrlConnection extends HttpURLConnection {
protected MyHttpUrlConnection(URL url, Handler handler) {
this(url, null, handler);
}
public MyHttpUrlConnection(URL url, Proxy proxy) {
this(url, proxy, new MyUrlConnectionHandler()); // <--- No way sneaking around^^
}
protected MyHttpUrlConnection(URL url, Proxy proxy, Handler handler) {
super(url, proxy, handler);
}
public MyHttpUrlConnection(URL url, String host, int port) {
this(url, new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(host, port))); // Taken over from the HttpURLConnection sourcecode.
}
}
Сега идва най-важната част: MyUrlConnectionHandler
. Отново проверете тази нишка за това къде да го поставите. Всичко, от което се нуждае самият клас, е да презапише функцията openConnection(URL, Proxy)
. Преди да публикувам моя код, ще ви дам преглед на това, което прави.
- Ако даденият URL адрес е jpg, png, ... файл:
- Използвайте обект
MyHttpUrlConnection
, за да получите последната дата на промяна на ресурса на сървъра. това трябва да извиква само заглавката, а НЕ целия ресурс. Иначе нямаше да спечелим нищо. Кредитите отиват в тази тема. Не съм напълно сигурен обаче дали затварям URLConnection
правилно тук. По-добре проверете отново, ако се съмнявате;)
- Ако няма ресурс в кеша ИЛИ ресурсът в кеша е остарял:
- Затворете мини връзката и отворете „правилна“, за да изтеглите всичко.
- Създайте нов
CachedResource
обект и го добавете към кеша.
- Затворете и тази нова връзка.
- Върнете нов
CachedUrlConnection
обект, който съдържа данните. Това може да изглежда малко глупаво, тъй като вече имаме всичко, но функцията трябва да върне URLConnection
.
- Ако има някакво изключение или просто не сме работили с jpg, png, ... файл, върнете "по подразбиране"
MyHttpUrlConnection
обект, за да обработите URL нормално.
И съответният код изглежда по следния начин. Имайте предвид, че използвах Apache org.apache.commons.io.IOUtils
:
@Override
protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
try {
// Is this some resource that we'd like to cache?
if (ResourceCache.isCachableURL(url)) {
// Retrieve whatever is in the cache first.
ResourceCache cache = ResourceCache.getInstance();
CachedResource resource = cache.getCachedResource(url);
// Open a connection to the server to at least check for the last-modified field.
MyHttpUrlConnection conn = new MyHttpUrlConnection(url, this); // Don't use URL#openConnection to avoid looping!
conn.setRequestMethod("HEAD");
conn.connect();
long lastModified = conn.getLastModified();
// Did we get the last-modified value at all?
if (lastModified == 0) {
throw new Exception("No last-modified value could be read! \n\t" + url);
}
// Resource not cached or out of date?
if (resource == null || resource.getLastModified() < lastModified) {
conn = new MyHttpUrlConnection(url, this);
conn.connect();
InputStream input = conn.getInputStream();
byte[] data = IOUtils.toByteArray(input);
Map<String, List<String>> headerFields = conn.getHeaderFields();
IOUtils.closeQuietly(input);
resource = new CachedResource(url.getFile(), data, headerFields, lastModified); // I use url.getFile() to store the file on my hard drive.
cache.addCachedResource(url, resource);
}
return new CachedUrlConnection(url, resource);
}
} catch (Exception e) {
e.printStackTrace();
}
// Return the default HttpURLConnection in our wrapper class.
return new MyHttpUrlConnection(url, proxy, this);
}
Едно последно нещо: За по-сигурно, НИКОГА не използвайте метода URL#openConnection
във функцията MyUrlConnectionHandler#openConnection
. Записвайки го по този начин, става доста очевидно защо, но днес ми отне доста време, за да разбера откъде идва безкрайният цикъл %) Използвайте конструктора и извикайте connect()
вместо това.
Надявам се, че това някога ще помогне на някого, иначе беше добро упражнение за мен ^^
person
MightyMalcolm
schedule
23.09.2015