I use xjar[https://github.com/core-lib/xjar]] to encrypt the jars. Since the way I use spring boot is lib separation, all the jars are in the lib directory
Spring boot default startup class is org. Springframework.. The boot loader. Launch. PropertiesLauncher, xjar extends to him, in the code is as follows:
public class XBootClassLoader extends LaunchedClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
private final XBootURLHandler xBootURLHandler;
private final Object urlClassPath;
private final Method getResource;
private final Method getCodeSourceURL;
private final Method getCodeSigners;
private static volatile boolean registered = false;
public XBootClassLoader(URL[] urls, ClassLoader parent, XDecryptor xDecryptor, XKey xKey) throws Exception {
super(true, urls, parent);
System.out.println("XBootClassLoader initialized with URLs:");
Arrays.stream(urls).forEach(url -> System.out.println(" - " + url));
this.xBootURLHandler = new XBootURLHandler(xDecryptor, xKey, this);
this.urlClassPath = XReflection.field(URLClassLoader.class, "ucp").get(this).value();
this.getResource = XReflection.method(urlClassPath.getClass(), "getResource", String.class).method();
this.getCodeSourceURL = XReflection.method(getResource.getReturnType(), "getCodeSourceURL").method();
this.getCodeSigners = XReflection.method(getResource.getReturnType(), "getCodeSigners").method();
}
@Override
public URL findResource(String name) {
URL url = super.findResource(name);
if (url == null) {
return null;
}
try {
return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile(), xBootURLHandler);
} catch (MalformedURLException e) {
return url;
}
}
@Override
public Enumeration<URL> findResources(String name) throws IOException {
Enumeration<URL> enumeration = super.findResources(name);
if (enumeration == null) {
return null;
}
return new XBootEnumeration(enumeration);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
System.out.println("Loading class: " + name);
return super.findClass(name);
} catch (ClassFormatError e) {
String path = name.replace('.', '/').concat(".class");
URL url = findResource(path);
if (url == null) {
throw new ClassNotFoundException(name, e);
}
try (InputStream in = url.openStream()) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
XKit.transfer(in, bos);
byte[] bytes = bos.toByteArray();
Object resource = getResource.invoke(urlClassPath, path);
URL codeSourceURL = (URL) getCodeSourceURL.invoke(resource);
CodeSigner[] codeSigners = (CodeSigner[]) getCodeSigners.invoke(resource);
CodeSource codeSource = new CodeSource(codeSourceURL, codeSigners);
return defineClass(name, bytes, 0, bytes.length, codeSource);
} catch (Throwable t) {
throw new ClassNotFoundException(name, t);
}
}
}
private class XBootEnumeration implements Enumeration<URL> {
private final Enumeration<URL> enumeration;
XBootEnumeration(Enumeration<URL> enumeration) {
this.enumeration = enumeration;
}
@Override
public boolean hasMoreElements() {
return enumeration.hasMoreElements();
}
@Override
public URL nextElement() {
URL url = enumeration.nextElement();
if (url == null) {
return null;
}
try {
return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile(), xBootURLHandler);
} catch (MalformedURLException e) {
return url;
}
}
}
}
One of the more important XBootURLHandler, XBootURLConnection respectively are defined as follows:
public class XBootURLHandler extends Handler implements XConstants {
private final XDecryptor xDecryptor;
private final XKey xKey;
private final Set<String> indexes;
public XBootURLHandler(XDecryptor xDecryptor, XKey xKey, ClassLoader classLoader) throws Exception {
this.xDecryptor = xDecryptor;
this.xKey = xKey;
this.indexes = new LinkedHashSet<>();
Enumeration<URL> resources = classLoader.getResources(XJAR_INF_DIR + XJAR_INF_IDX);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
String url = resource.toString();
String classpath = url.substring(0, url.lastIndexOf("!/") + 2);
InputStream in = resource.openStream();
InputStreamReader isr = new InputStreamReader(in);
LineNumberReader lnr = new LineNumberReader(isr);
String name;
while ((name = lnr.readLine()) != null) indexes.add(classpath + name);
}
}
@Override
protected URLConnection openConnection(URL url) throws IOException {
URLConnection urlConnection = super.openConnection(url);
return indexes.contains(url.toString())
&& urlConnection instanceof JarURLConnection
? new XBootURLConnection((JarURLConnection) urlConnection, xDecryptor, xKey)
: urlConnection;
}
}
public class XBootURLConnection extends JarURLConnection {
private final JarURLConnection jarURLConnection;
private final XDecryptor xDecryptor;
private final XKey xKey;
public XBootURLConnection(JarURLConnection jarURLConnection, XDecryptor xDecryptor, XKey xKey) throws MalformedURLException {
super(jarURLConnection.getURL());
this.jarURLConnection = jarURLConnection;
this.xDecryptor = xDecryptor;
this.xKey = xKey;
}
@Override
public void connect() throws IOException {
jarURLConnection.connect();
}
@Override
public int getConnectTimeout() {
return jarURLConnection.getConnectTimeout();
}
@Override
public void setConnectTimeout(int timeout) {
jarURLConnection.setConnectTimeout(timeout);
}
@Override
public int getReadTimeout() {
return jarURLConnection.getReadTimeout();
}
@Override
public void setReadTimeout(int timeout) {
jarURLConnection.setReadTimeout(timeout);
}
@Override
public URL getURL() {
return jarURLConnection.getURL();
}
@Override
public int getContentLength() {
return jarURLConnection.getContentLength();
}
@Override
public long getContentLengthLong() {
return jarURLConnection.getContentLengthLong();
}
@Override
public String getContentType() {
return jarURLConnection.getContentType();
}
@Override
public String getContentEncoding() {
return jarURLConnection.getContentEncoding();
}
@Override
public long getExpiration() {
return jarURLConnection.getExpiration();
}
@Override
public long getDate() {
return jarURLConnection.getDate();
}
@Override
public long getLastModified() {
return jarURLConnection.getLastModified();
}
@Override
public String getHeaderField(String name) {
return jarURLConnection.getHeaderField(name);
}
@Override
public Map<String, List<String>> getHeaderFields() {
return jarURLConnection.getHeaderFields();
}
@Override
public int getHeaderFieldInt(String name, int Default) {
return jarURLConnection.getHeaderFieldInt(name, Default);
}
@Override
public long getHeaderFieldLong(String name, long Default) {
return jarURLConnection.getHeaderFieldLong(name, Default);
}
@Override
public long getHeaderFieldDate(String name, long Default) {
return jarURLConnection.getHeaderFieldDate(name, Default);
}
@Override
public String getHeaderFieldKey(int n) {
return jarURLConnection.getHeaderFieldKey(n);
}
@Override
public String getHeaderField(int n) {
return jarURLConnection.getHeaderField(n);
}
@Override
public Object getContent() throws IOException {
return jarURLConnection.getContent();
}
@Override
public Object getContent(Class[] classes) throws IOException {
return jarURLConnection.getContent(classes);
}
@Override
public Permission getPermission() throws IOException {
return jarURLConnection.getPermission();
}
@Override
public InputStream getInputStream() throws IOException {
InputStream in = jarURLConnection.getInputStream();
//class解密
return xDecryptor.decrypt(xKey, in);
}
@Override
public OutputStream getOutputStream() throws IOException {
OutputStream out = jarURLConnection.getOutputStream();
System.out.println("getOutputStream......");
return null;
}
@Override
public String toString() {
return jarURLConnection.toString();
}
@Override
public boolean getDoInput() {
return jarURLConnection.getDoInput();
}
@Override
public void setDoInput(boolean doInput) {
jarURLConnection.setDoInput(doInput);
}
@Override
public boolean getDoOutput() {
return jarURLConnection.getDoOutput();
}
@Override
public void setDoOutput(boolean doOutput) {
jarURLConnection.setDoOutput(doOutput);
}
@Override
public boolean getAllowUserInteraction() {
return jarURLConnection.getAllowUserInteraction();
}
@Override
public void setAllowUserInteraction(boolean allowUserInteraction) {
jarURLConnection.setAllowUserInteraction(allowUserInteraction);
}
@Override
public boolean getUseCaches() {
return jarURLConnection.getUseCaches();
}
@Override
public void setUseCaches(boolean useCaches) {
jarURLConnection.setUseCaches(useCaches);
}
@Override
public long getIfModifiedSince() {
return jarURLConnection.getIfModifiedSince();
}
@Override
public void setIfModifiedSince(long ifModifiedSince) {
jarURLConnection.setIfModifiedSince(ifModifiedSince);
}
@Override
public boolean getDefaultUseCaches() {
return jarURLConnection.getDefaultUseCaches();
}
@Override
public void setDefaultUseCaches(boolean defaultUseCaches) {
jarURLConnection.setDefaultUseCaches(defaultUseCaches);
}
@Override
public void setRequestProperty(String key, String value) {
jarURLConnection.setRequestProperty(key, value);
}
@Override
public void addRequestProperty(String key, String value) {
jarURLConnection.addRequestProperty(key, value);
}
@Override
public String getRequestProperty(String key) {
return jarURLConnection.getRequestProperty(key);
}
@Override
public Map<String, List<String>> getRequestProperties() {
return jarURLConnection.getRequestProperties();
}
@Override
public URL getJarFileURL() {
return jarURLConnection.getJarFileURL();
}
@Override
public String getEntryName() {
return jarURLConnection.getEntryName();
}
@Override
public JarFile getJarFile() throws IOException {
return jarURLConnection.getJarFile();
}
@Override
public Manifest getManifest() throws IOException {
return jarURLConnection.getManifest();
}
@Override
public JarEntry getJarEntry() throws IOException {
return jarURLConnection.getJarEntry();
}
@Override
public Attributes getAttributes() throws IOException {
return jarURLConnection.getAttributes();
}
@Override
public Attributes getMainAttributes() throws IOException {
return jarURLConnection.getMainAttributes();
}
@Override
public Certificate[] getCertificates() throws IOException {
return jarURLConnection.getCertificates();
}
If the encryption class at start-up time will go in the start-up jar org.springframework.boot.loader.net.protocol.jar.JarUrlClassLoader#loadClass method, XBootClas will call at this time findClass in sLoader loads the class, and since the class is already encrypted, it raises a ClassFormatError exception. The following code handles the class:
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
System.out.println("Loading class: " + name);
return super.findClass(name);
} catch (ClassFormatError e) {
String path = name.replace('.', '/').concat(".class");
URL url = findResource(path);
if (url == null) {
throw new ClassNotFoundException(name, e);
}
//System.out.println("查找资源:" + name);
try (InputStream in = url.openStream()) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
XKit.transfer(in, bos);
byte[] bytes = bos.toByteArray();
Object resource = getResource.invoke(urlClassPath, path);
URL codeSourceURL = (URL) getCodeSourceURL.invoke(resource);
CodeSigner[] codeSigners = (CodeSigner[]) getCodeSigners.invoke(resource);
CodeSource codeSource = new CodeSource(codeSourceURL, codeSigners);
return defineClass(name, bytes, 0, bytes.length, codeSource);
} catch (Throwable t) {
throw new ClassNotFoundException(name, t);
}
}
}
@Override
public URL findResource(String name) {
URL url = super.findResource(name);
if (url == null) {
return null;
}
try {
return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile(), xBootURLHandler);
} catch (MalformedURLException e) {
return url;
}
}
To add a custom URL URLStreamHandler, namely xBootURLHandler, eventually in the org. Springframework. Core. The classreading. SimpleMetadataReader# getC The lassReader method calls resource.getInputStream()
private static ClassReader getClassReader(Resource resource) throws IOException {
try (InputStream is = resource.getInputStream()) {
try {
return new ClassReader(is);
}
catch (IllegalArgumentException ex) {
throw new ClassFormatException("ASM ClassReader failed to parse class file - " +
"probably due to a new Java class file version that is not supported yet. " +
"Consider compiling with a lower '-target' or upgrade your framework version. " +
"Affected class: " + resource, ex);
}
}
}
Then call org. Springframework. Core. IO. UrlResource# getInputStream
@Override
public InputStream getInputStream() throws IOException {
URLConnection con = this.url.openConnection();
customizeConnection(con);
try {
return con.getInputStream();
}
catch (IOException ex) {
// Close the HTTP connection (if applicable).
if (con instanceof HttpURLConnection httpConn) {
httpConn.disconnect();
}
throw ex;
}
}
In this method, if it is a class in the main program, a custom XBootURLHandler is created for it and then called
@Override
protected URLConnection openConnection(URL url) throws IOException {
URLConnection urlConnection = super.openConnection(url);
return indexes.contains(url.toString())
&& urlConnection instanceof JarURLConnection
? new XBootURLConnection((JarURLConnection) urlConnection, xDecryptor, xKey)
: urlConnection;
}
Call the decryption method in the XBootURLConnection to decrypt the class. The above method is valid for the class in the main program. If the jar in the lib directory is used, Won't go cn. Iocoder. Yudao. Loader. Guide. The module. The boot. XBootClassLoader# findClass method, can lead to getClassReader
private static ClassReader getClassReader(Resource resource) throws IOException {
try (InputStream is = resource.getInputStream()) {
try {
return new ClassReader(is);
}
catch (IllegalArgumentException ex) {
throw new ClassFormatException("ASM ClassReader failed to parse class file - " +
"probably due to a new Java class file version that is not supported yet. " +
"Consider compiling with a lower '-target' or upgrade your framework version. " +
"Affected class: " + resource, ex);
}
}
}
Method throws an exception directly, refer to: https://github.com/spring-projects/spring-framework/issues/27691 Set spring. Classformat. Ignore is true, and there is not much, because a jar of lib directory under the class + mapper. Do XML encryption, the load mapper. The XML directly when error lead to project operation to stop, quit, so I want to here Do you know how to create a custom URLStreamHandler for class loading in a jar
Comment From: philwebb
This issue should be closed as a duplicate of https://github.com/spring-projects/spring-boot/issues/44029
Comment From: fendo8888
What's going on? Turn this off, this is not an xjar problem, I'm asking how about in spring How do I load a custom URLStreamHandler for jars in the lib directory
Comment From: philwebb
I'm asking how about in spring How do I load a custom URLStreamHandler for jars in the lib directory
I'm sorry, but I don't understand the question or how it relates to a Spring Boot project that isn't using xjar. Perhaps you can provide a sample project that isn't using xjar that reproduces the problem?