-
Notifications
You must be signed in to change notification settings - Fork 138
/
AppLibClassLoaderServiceImpl.java
618 lines (562 loc) · 23.4 KB
/
AppLibClassLoaderServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
/*
* Copyright (c) 2023, 2024 Contributors to the Eclipse Foundation.
* Copyright (c) 2007, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.enterprise.v3.server;
import com.sun.enterprise.util.OS;
import com.sun.enterprise.util.io.FileUtils;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.System.Logger;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.glassfish.api.event.EventListener;
import org.glassfish.api.event.Events;
import org.glassfish.api.event.RestrictTo;
import org.glassfish.common.util.GlassfishUrlClassLoader;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.internal.api.ClassLoaderHierarchy;
import org.glassfish.internal.api.DelegatingClassLoader;
import org.glassfish.internal.api.DelegatingClassLoader.ClassFinder;
import org.jvnet.hk2.annotations.Service;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE;
import static java.lang.System.Logger.Level.WARNING;
import static java.util.Collections.emptyEnumeration;
import static java.util.Collections.enumeration;
import static org.glassfish.api.event.EventTypes.PREPARE_SHUTDOWN_NAME;
/**
* This class is responsible for constructing class loader that has visibility
* to deploy time libraries ({@code --libraries} and {@code EXTENSION_LIST} of
* {@code MANIFEST.MF} for an application.
* <p>
* It is different from common class loader in a sense that the libraries that are part of
* common class loader are shared by all applications, whereas this class loader adds
* a scope to a library.
*
* @author Sanjeeb.Sahoo@Sun.COM
*/
@Service
@Singleton
public class AppLibClassLoaderServiceImpl implements EventListener {
private static final Logger LOG = System.getLogger(AppLibClassLoaderServiceImpl.class.getName());
/**
* Class finders' registry.
* <p>
* If multiple applications or modules refer to the same libraries,
* share this libraries by reusing the same class loaders.
*/
private final Map<Library, ClassFinder> classFinderRegistry = new ConcurrentHashMap<>();
@Inject
private ServiceLocator serviceLocator;
@Inject
private CommonClassLoaderServiceImpl commonClassLoaderService;
@Inject
private Events events;
@PostConstruct
public void postConstruct() {
events.register(this);
}
@Override
public void event(@RestrictTo(PREPARE_SHUTDOWN_NAME) Event<?> event) {
// Close application libraries class finders
for (ClassFinder classFinder : classFinderRegistry.values()) {
try {
((GlassfishUrlClassLoader) classFinder).close();
} catch (IOException e) {
LOG.log(WARNING, () -> "Could not close class finder " + classFinder, e);
}
}
// Remove application libraries temporary snapshots
for (Library library : classFinderRegistry.keySet()) {
if (library.isSnapshot()) {
try {
Files.delete(Path.of(library.getURI()));
} catch (IOException e) {
LOG.log(WARNING, () -> "Could not delete application library snapshot " + library.getURI(), e);
}
}
}
}
/**
* Returns the application libraries class loader.
* <p>
* This class loader has visibility to deploy time libraries for an application.
* <p>
* This class loader is different from common class loader in a sense that the libraries
* that are part of common class loader are shared by all applications, whereas this
* class loader adds a scope to a libraries.
*
* @param application the application for which class loader is created
* @param libURIs the URIs from which to load classes and resources
* @return the class loader that has visibility to appropriate application specific libraries
* @throws MalformedURLException if some error occurred while constructing the URL
* @see org.glassfish.internal.api.ClassLoaderHierarchy#getAppLibClassLoader(String, List)
*/
public ClassLoader getAppLibClassLoader(String application, List<URI> libURIs) throws MalformedURLException {
ClassLoaderHierarchy classLoaderHierarchy = serviceLocator.getService(ClassLoaderHierarchy.class);
DelegatingClassLoader connectorClassLoader = classLoaderHierarchy.getConnectorClassLoader(application);
if (libURIs == null || libURIs.isEmpty()) {
// Optimization: when there are no libraries, why create an empty
// class loader in the hierarchy? Instead, return the parent.
return connectorClassLoader;
}
final ClassLoader commonClassLoader = commonClassLoaderService.getCommonClassLoader();
PrivilegedAction<DelegatingClassLoader> action = () -> new DelegatingClassLoader(commonClassLoader);
DelegatingClassLoader appLibClassLoader = AccessController.doPrivileged(action);
// Order of class finders is important here.
// Connector's class finders should be added before libraries' class finders
// as the delegation hierarchy is appCL->app-libsCL->connectorCL->commonCL->API-CL
// since we are merging connector and applib class finders to be at same level,
// connector class finders need to be before applib class finders in the horizontal
// search path
for (ClassFinder classFinder : connectorClassLoader.getDelegates()) {
appLibClassLoader.addDelegate(classFinder);
}
addDelegates(libURIs, appLibClassLoader);
return appLibClassLoader;
}
/**
* Returns the application libraries class loader.
* <p>
* This class loader has visibility to deploy time libraries for an application.
* <p>
* This class loader adds a scope to a libraries and will be used only by the
* connector class loader.
*
* @param libURIs the URIs from which to load classes and resources
* @return the class loader that has visibility to appropriate application specific libraries
* @throws MalformedURLException if some error occurred while constructing the URL
* @see org.glassfish.internal.api.ClassLoaderHierarchy#getAppLibClassFinder(List)
*/
public ClassFinder getAppLibClassFinder(Collection<URI> libURIs) throws MalformedURLException {
final ClassLoader commonClassLoader = commonClassLoaderService.getCommonClassLoader();
DelegatingClassFinder appLibClassFinder = AccessController.doPrivileged(
(PrivilegedAction<DelegatingClassFinder>) () -> new DelegatingClassFinder(commonClassLoader));
addDelegates(libURIs, appLibClassFinder);
return appLibClassFinder;
}
/**
* Adds application libraries class loaders to the delegating class loader.
*/
private void addDelegates(Collection<URI> libURIs, DelegatingClassLoader holder) throws MalformedURLException {
ClassLoader commonClassLoader = commonClassLoaderService.getCommonClassLoader();
for (URI libURI : libURIs) {
synchronized (this) {
Library library = new Library(libURI);
ClassFinder classFinder = classFinderRegistry.get(library);
if (classFinder == null) {
classFinder = new URLClassFinder(library.getURI().toURL(), commonClassLoader);
classFinderRegistry.put(library, classFinder);
} else {
library.close();
}
holder.addDelegate(classFinder);
}
}
}
/**
* This class loader is used to load classes and resources from the URL
* referring to a JAR file.
*/
private static class URLClassFinder extends GlassfishUrlClassLoader implements ClassFinder {
private final Set<String> notFoundResources = ConcurrentHashMap.newKeySet();
URLClassFinder(URL url, ClassLoader parent) {
super(new URL[] {url}, parent);
}
/**
* Finds the class with the specified binary name.
*
* @param name the binary name of the class
* @return the resulting {@code Class} object
* @throws ClassNotFoundException if class could not be found
*/
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
notFoundResources.add(name);
throw e;
}
}
/**
* Returns the loaded class with the given binary name.
*
* @param name the binary name of the class
* @return the {@code Class} object, or {@code null} if the class has not been loaded
*/
@Override
public Class<?> findExistingClass(String name) {
if (notFoundResources.contains(name)) {
return null;
}
return findLoadedClass(name);
}
/**
* Finds the resource with the given name.
*
* @param name the resource name
* @return a URL object for reading the resource, or {@code null} if the resource
* could not be found
*/
@Override
public URL findResource(String name) {
if (notFoundResources.contains(name)) {
return null;
}
URL resourceURL = super.findResource(name);
if (resourceURL == null){
notFoundResources.add(name);
}
return resourceURL;
}
/**
* Returns an enumeration of URL object representing the resource with the given name.
*
* @param name the resource name
* @return a singleton enumeration of URL object for the resource, or empty enumeration
* if the resources not found
*/
@Override
public Enumeration<URL> findResources(String name) {
URL resourceURL = findResource(name);
return resourceURL != null ? enumeration(List.of(resourceURL)) : emptyEnumeration();
}
}
/**
* This class loader has a list of class loaders called as delegates
* that it uses to find resources and classes.
*/
private static class DelegatingClassFinder extends DelegatingClassLoader implements ClassFinder {
DelegatingClassFinder(ClassLoader parent) {
super(parent);
}
/**
* Always returns {@code null} because delegating class loader will
* never be a defining class loader.
*/
@Override
public Class<?> findExistingClass(String name) {
return null;
}
/**
* Finds the resource with the given name.
*
* @param name the resource name
* @return a URL object for reading the resource, or {@code null} if the resource
* could not be found
*/
@Override
public URL findResource(String name) {
return super.findResource(name);
}
/**
* Returns an enumeration of URL objects representing all resources with the given name.
*
* @param name the resource name
* @return an enumeration of URL objects for the resources, or empty enumeration
* if the resources not found
* @throws IOException if an I/O error occurs
*/
@Override
public Enumeration<URL> findResources(String name) throws IOException {
return super.findResources(name);
}
}
/**
* Represents a deployment time library.
*/
private static class Library {
private static final Logger LOG = System.getLogger(Library.class.getName());
/**
* Represents a method to read the attributes of an open file.
*/
private static final Method readAttributesMethod;
/**
* Represents a field to access to the native descriptor for an open file.
*/
private static final Field nativeDescriptorField;
static {
Method method;
Field field;
try {
if (OS.isWindows()) {
Class<?> attributesClass = Class.forName("sun.nio.fs.WindowsFileAttributes");
method = attributesClass.getDeclaredMethod("readAttributes", Long.TYPE);
field = FileDescriptor.class.getDeclaredField("handle");
} else {
Class<?> attributesClass = Class.forName("sun.nio.fs.UnixFileAttributes");
method = attributesClass.getDeclaredMethod("get", Integer.TYPE);
field = FileDescriptor.class.getDeclaredField("fd");
}
method.setAccessible(true);
field.setAccessible(true);
} catch (ReflectiveOperationException e) {
method = null;
field = null;
}
readAttributesMethod = method;
nativeDescriptorField = field;
}
/**
* Original library URI.
*/
private final URI originalSource;
/**
* Library basic file attributes.
*/
private final BasicFileAttributes attributes;
/**
* Library file input stream. Used to create library snapshot.
*/
private FileInputStream fileInputStream;
/**
* Actual library URI.
*/
private URI source;
Library(URI libURI) {
this.originalSource = libURI;
try {
this.fileInputStream = new FileInputStream(new File(libURI));
} catch (IOException e) {
LOG.log(WARNING, () -> "Could not open input stream for application library " + libURI, e);
}
BasicFileAttributes attributes = null;
// Try to read file attributes of an open library file
if (fileInputStream != null) {
Object nativeDescriptor = getNativeDescriptor(fileInputStream);
if (nativeDescriptor != null) {
attributes = readAttributes(nativeDescriptor);
}
}
// Fallback to the standard NIO.2 method
if (attributes == null) {
attributes = readAttributes(libURI);
}
this.attributes = attributes;
}
/**
* Gets a {@code file:} URI that represents this library.
* <p>
* Creates a library snapshot in the default temporary-file directory.
*
* @return the deployment time library URI
*/
public URI getURI() {
if (source == null) {
File snapshot = createSnapshot();
if (snapshot != null) {
LOG.log(TRACE, "Created snapshot {0} for application library {1}", snapshot, originalSource);
// Use snapshot URI as a library source.
source = snapshot.toURI();
} else {
// Snapshot creation failed.
// Use original library URI.
source = originalSource;
}
}
return source;
}
public boolean isSnapshot() {
return source != originalSource;
}
/**
* Closes associated file input stream and releases any system resources
* associated with the stream.
*/
public void close() {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
LOG.log(WARNING, () -> "Could not close input stream for application library " + source, e);
}
}
}
/**
* Tests this library for equality with the given object.
* <p>
* If the given object is not a {@code Library}, then this method returns {@code false}.
*
* @param obj the object to which this object to be compared
* @return {@code true} if, and only if, the given object is a {@code Library} that is
* identical to this {@code Library}
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Library)) {
return false;
}
Library library = (Library) obj;
if (!originalSource.equals(library.originalSource)) {
return false;
}
if (attributes == null || library.attributes == null) {
return attributes == library.attributes;
}
if (attributes.size() != library.attributes.size()) {
return false;
}
if (!Objects.equals(attributes.lastModifiedTime(), library.attributes.lastModifiedTime())) {
return false;
}
return Objects.equals(attributes.fileKey(), library.attributes.fileKey());
}
/**
* Computes a hash code for this library.
*
* @return the hash code value for this library
*/
@Override
public int hashCode() {
int hash = originalSource.hashCode();
if (attributes != null) {
hash = 31 * hash + Long.hashCode(attributes.size());
hash = 31 + hash + Objects.hashCode(attributes.lastModifiedTime());
hash = 31 + hash + Objects.hashCode(attributes.fileKey());
}
return hash;
}
/**
* Gets OS-dependent native file descriptor for an open file related to
* this application library {@code fileInputStream}.
*
* @param fileInputStream the application library file input stream
* @return the native file descriptor
*/
private Object getNativeDescriptor(FileInputStream fileInputStream) {
Object nativeDescriptor = null;
if (nativeDescriptorField != null) {
try {
FileDescriptor fileDescriptor = fileInputStream.getFD();
if (fileDescriptor.valid()) {
nativeDescriptor = nativeDescriptorField.get(fileDescriptor);
LOG.log(TRACE, "Returning nativeDescriptor={0} for application library {1}",
nativeDescriptor, source);
}
} catch (IllegalAccessException | IOException e) {
LOG.log(WARNING, () -> "Could not obtain native descriptor for application library " + source, e);
}
}
return nativeDescriptor;
}
/**
* Gets the {@link BasicFileAttributes} for an open application library.
*
* @param nativeDescriptor the open library native descriptor
* @return the file attributes or {@code null} if an error occurs
*/
private BasicFileAttributes readAttributes(Object nativeDescriptor) {
LOG.log(DEBUG, "readAttributes(nativeDescriptor={0})", nativeDescriptor);
BasicFileAttributes attributes = null;
if (readAttributesMethod != null) {
try {
attributes = (BasicFileAttributes) readAttributesMethod.invoke(null, nativeDescriptor);
} catch (Exception e) {
LOG.log(WARNING, () -> "Could not read file attributes for nativeDescriptor="
+ nativeDescriptor, e);
}
}
return attributes;
}
/**
* Reads a file's basic attributes as a bulk operation.
*
* @param libURI an absolute, hierarchical URI with scheme equal to {@code file:},
* a non-empty path component, and undefined authority, query and fragment components
* @return the file attributes or {@code null} if an error occurs
*/
private BasicFileAttributes readAttributes(URI libURI) {
LOG.log(DEBUG, "readAttributes(libURI={0})", libURI);
try {
return Files.readAttributes(Path.of(libURI), BasicFileAttributes.class);
} catch (IOException e) {
LOG.log(WARNING, () -> "Could not read file attributes for libURI=" + libURI, e);
return null;
}
}
/**
* Creates a library snapshot in the default temporary-file directory.
* <p>
* Closes a file input stream associated with library file.
*
* @return an abstract pathname denoting a newly-created snapshot
*/
private File createSnapshot() {
LOG.log(DEBUG, "createSnapshot()");
File snapshot = null;
try {
snapshot = File.createTempFile("applib", ".jar");
if (!copy(fileInputStream, snapshot)) {
FileUtils.copy(new File(originalSource), snapshot);
}
// Normally snapshots should be removed at server shutdown.
// This should remove snapshot if SIGTERM signal received.
snapshot.deleteOnExit();
} catch (IOException e) {
LOG.log(WARNING, () -> "Could not create snapshot for application library " + source, e);
FileUtils.deleteFileMaybe(snapshot);
} finally {
fileInputStream = null;
}
return snapshot;
}
/**
* Copies all bytes from an input stream to a file.
* <p>
* Closes input stream after completion.
*
* @param inputStream the input stream to read from
* @param file the target output file. If the file already exists, it will be overwritten
* @return {@code true} if all bytes copied, {@code false} otherwise
*/
private boolean copy(InputStream inputStream, File file) {
if (inputStream == null) {
return false;
}
try (inputStream) {
FileUtils.copy(inputStream, file);
return true;
} catch (IOException e) {
return false;
}
}
}
}