Created
June 4, 2010 17:07
-
-
Save ashigeru/425672 to your computer and use it in GitHub Desktop.
プロダクション環境でHot Reloadingしたい。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2010 @ashigeru. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, | |
* either express or implied. See the License for the specific language | |
* governing permissions and limitations under the License. | |
*/ | |
package com.ashigeru.appengine.tools.classload; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.HashSet; | |
import java.util.Set; | |
import com.google.appengine.api.datastore.DatastoreService; | |
/** | |
* Google App Engine のデータストアを利用してクラスをロードする。 | |
* <p> | |
* この実装では、{@link #getResource(String)}や{@link #getResources(String)}は | |
* 正しく動作しない。 | |
* リソースの取得には、{@link #getResourceAsStream(String)}のみを利用できる。 | |
* </p> | |
* @author ashigeru | |
*/ | |
public class DatastoreClassLoader extends ClassLoader { | |
private static final String CLASS_EXTENSION = ".class"; //$NON-NLS-1$ | |
private ClassLoader parent; | |
private DatastoreClassStore classStore; | |
private Set<String> pathPrefix; | |
private boolean forceLoad; | |
/** | |
* インスタンスを生成する。 | |
* @param parent 親クラスローダ | |
* @param service ロードに利用するデータストアサービス | |
* @param rootPackages ロード対象とするルートのパッケージ名 | |
* @param kindName クラスを保存してあるカインド名 | |
* @param forceLoad 対象クラスを親クラスローダより優先してロードする | |
* @throws IllegalArgumentException 引数に{@code null}が含まれる場合 | |
*/ | |
public DatastoreClassLoader( | |
ClassLoader parent, | |
DatastoreService service, | |
Set<String> rootPackages, | |
String kindName, | |
boolean forceLoad) { | |
super(parent); | |
if (parent == null) { | |
throw new IllegalArgumentException("parent is null"); //$NON-NLS-1$ | |
} | |
if (service == null) { | |
throw new IllegalArgumentException("service is null"); //$NON-NLS-1$ | |
} | |
if (rootPackages == null) { | |
throw new IllegalArgumentException("rootPackages is null"); //$NON-NLS-1$ | |
} | |
if (kindName == null) { | |
throw new IllegalArgumentException("kindName is null"); //$NON-NLS-1$ | |
} | |
this.parent = parent; | |
this.classStore = new DatastoreClassStore(service, kindName); | |
this.pathPrefix = new HashSet<String>(); | |
for (String rootPackage : rootPackages) { | |
String prefix = rootPackage.replace('.', '/') + '/'; | |
pathPrefix.add(prefix); | |
} | |
this.forceLoad = forceLoad; | |
} | |
/** | |
* 指定のパス上のデータをこのクラスローダで取り扱う場合のみ{@code true}を返す。 | |
* @param path 対象のパス | |
* @return このクラスローダで取り扱う場合のみ{@code true} | |
* @throws IllegalArgumentException 引数に{@code null}が含まれる場合 | |
*/ | |
protected boolean accepts(String path) { | |
if (path == null) { | |
throw new IllegalArgumentException("path is null"); //$NON-NLS-1$ | |
} | |
for (String prefix : pathPrefix) { | |
if (path.startsWith(prefix)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
@Override | |
protected synchronized Class<?> loadClass(String name, boolean resolve) | |
throws ClassNotFoundException { | |
Class<?> loaded = findLoadedClass(name); | |
if (loaded != null) { | |
return loaded; | |
} | |
if (forceLoad == false) { | |
return super.loadClass(name, resolve); | |
} | |
String path = toClassFileName(name); | |
if (accepts(path) == false) { | |
return super.loadClass(name, resolve); | |
} | |
// find from datastore | |
try { | |
Class<?> found = findClass(name); | |
if (resolve) { | |
resolveClass(found); | |
} | |
return found; | |
} | |
catch (ClassNotFoundException e) { | |
// continue... | |
} | |
// find from parent class path | |
InputStream in = parent.getResourceAsStream(path); | |
if (in == null) { | |
return super.loadClass(name, resolve); | |
} | |
try { | |
ByteArrayOutputStream results = new ByteArrayOutputStream(); | |
byte[] buf = new byte[1024]; | |
while (true) { | |
int read = in.read(buf); | |
if (read == -1) { | |
break; | |
} | |
results.write(buf, 0, read); | |
} | |
byte[] bytes = results.toByteArray(); | |
Class<?> intercept = defineClass(name, bytes, 0, bytes.length); | |
if (resolve) { | |
resolveClass(intercept); | |
} | |
return intercept; | |
} | |
catch (IOException e) { | |
// continue.. | |
} | |
finally { | |
try { | |
in.close(); | |
} | |
catch (IOException e) { | |
// ignored | |
} | |
} | |
return super.loadClass(name, resolve); | |
} | |
@Override | |
protected Class<?> findClass(String name) throws ClassNotFoundException { | |
try { | |
byte[] binary = findClassBinaryFromDatastore(name); | |
if (binary == null) { | |
throw new ClassNotFoundException(name); | |
} | |
return defineClass( | |
name, | |
binary, | |
0, | |
binary.length, | |
null); | |
} | |
catch (Exception e) { | |
throw new ClassNotFoundException(name, e); | |
} | |
} | |
@Override | |
public InputStream getResourceAsStream(String name) { | |
if (forceLoad) { | |
InputStream stream = findResourceAsStream(name); | |
if (stream != null) { | |
return stream; | |
} | |
return super.getResourceAsStream(name); | |
} | |
else { | |
InputStream stream = super.getResourceAsStream(name); | |
if (stream != null) { | |
return stream; | |
} | |
return findResourceAsStream(name); | |
} | |
} | |
private InputStream findResourceAsStream(String name) { | |
byte[] contents = findResourceFromDatastore(name); | |
if (contents == null) { | |
return null; | |
} | |
return new ByteArrayInputStream(contents); | |
} | |
byte[] findClassBinaryFromDatastore(String name) { | |
return findResourceFromDatastore(toClassFileName(name)); | |
} | |
private String toClassFileName(String name) { | |
return name + CLASS_EXTENSION; | |
} | |
byte[] findResourceFromDatastore(String path) { | |
if (accepts(path) == false) { | |
return null; | |
} | |
return classStore.get(path); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2010 @ashigeru. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, | |
* either express or implied. See the License for the specific language | |
* governing permissions and limitations under the License. | |
*/ | |
package com.ashigeru.appengine.tools.classload; | |
import com.google.appengine.api.datastore.Blob; | |
import com.google.appengine.api.datastore.DatastoreService; | |
import com.google.appengine.api.datastore.Entity; | |
import com.google.appengine.api.datastore.EntityNotFoundException; | |
import com.google.appengine.api.datastore.Key; | |
import com.google.appengine.api.datastore.KeyFactory; | |
import com.google.appengine.api.datastore.Transaction; | |
/** | |
* {@link DatastoreClassLoader}がロード可能なクラスを保存する。 | |
* @author ashigeru | |
*/ | |
public class DatastoreClassStore { | |
private static final String PROPERTY_CONTENTS = "contents"; //$NON-NLS-1$ | |
private DatastoreService service; | |
private String kindName; | |
/** | |
* インスタンスを生成する。 | |
* @param service 保存に利用するデータストアサービス | |
* @param kindName クラスを保存するカインド名 | |
* @throws IllegalArgumentException 引数に{@code null}が含まれる場合 | |
*/ | |
public DatastoreClassStore( | |
DatastoreService service, | |
String kindName) { | |
if (service == null) { | |
throw new IllegalArgumentException("service is null"); //$NON-NLS-1$ | |
} | |
if (kindName == null) { | |
throw new IllegalArgumentException("kindName is null"); //$NON-NLS-1$ | |
} | |
this.service = service; | |
this.kindName = kindName; | |
} | |
/** | |
* 指定のパスのファイルを保存するためのキーを作成して返す。 | |
* @param path 対象のパス | |
* @return 対応するキー | |
* @throws IllegalArgumentException 引数に{@code null}が含まれる場合 | |
*/ | |
public Key createKey(String path) { | |
if (path == null) { | |
throw new IllegalArgumentException("path is null"); //$NON-NLS-1$ | |
} | |
return KeyFactory.createKey(kindName, path); | |
} | |
/** | |
* 指定のパスに対応するファイルの内容をデータストアから読み出して返す。 | |
* @param path 対象のパス | |
* @return 対応するファイルの内容、存在しない場合は{@code null} | |
* @throws IllegalArgumentException 引数に{@code null}が含まれる場合 | |
*/ | |
public byte[] get(String path) { | |
if (path == null) { | |
throw new IllegalArgumentException("path is null"); //$NON-NLS-1$ | |
} | |
Key key = createKey(path); | |
Entity entity; | |
try { | |
entity = service.get(null, key); | |
} | |
catch (EntityNotFoundException e) { | |
return null; | |
} | |
if (entity.hasProperty(PROPERTY_CONTENTS) == false) { | |
return null; | |
} | |
Blob contents = (Blob) entity.getProperty(PROPERTY_CONTENTS); | |
return contents.getBytes(); | |
} | |
/** | |
* 指定のパスに対応するファイルの内容を、データストアに書き出す。 | |
* <p> | |
* 指定のパスに対応するファイルが既にデータストア上に存在する場合、新しい内容で上書きする。 | |
* </p> | |
* @param path 対象のパス | |
* @param contents 対象のファイルの内容 | |
* @throws IllegalArgumentException 引数に{@code null}が含まれる場合 | |
*/ | |
public void put(String path, byte[] contents) { | |
if (path == null) { | |
throw new IllegalArgumentException("path is null"); //$NON-NLS-1$ | |
} | |
if (contents == null) { | |
throw new IllegalArgumentException("contents is null"); //$NON-NLS-1$ | |
} | |
Key key = createKey(path); | |
Entity entity = new Entity(key); | |
entity.setUnindexedProperty(PROPERTY_CONTENTS, new Blob(contents)); | |
service.put(entity); | |
} | |
/** | |
* 指定のパスに対応するデータストア上のデータを削除する。 | |
* <p> | |
* データストア上にそのようなデータが存在しない場合、この呼び出しはなにも行わない。 | |
* </p> | |
* @param path 対象のパス | |
* @throws IllegalArgumentException 引数に{@code null}が含まれる場合 | |
*/ | |
public void delete(String path) { | |
if (path == null) { | |
throw new IllegalArgumentException("path is null"); //$NON-NLS-1$ | |
} | |
Key key = createKey(path); | |
service.delete((Transaction) null, key); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment