Skip to content

Instantly share code, notes, and snippets.

@ashigeru
Created June 4, 2010 17:07
Show Gist options
  • Save ashigeru/425672 to your computer and use it in GitHub Desktop.
Save ashigeru/425672 to your computer and use it in GitHub Desktop.
プロダクション環境でHot Reloadingしたい。
/*
* 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);
}
}
/*
* 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