1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.catalina.loader;
18
19 import java.beans.PropertyChangeEvent;
20 import java.beans.PropertyChangeListener;
21 import java.beans.PropertyChangeSupport;
22 import java.io.File;
23 import java.io.FilePermission;
24 import java.io.IOException;
25 import java.lang.reflect.Constructor;
26 import java.net.URL;
27 import java.net.URLClassLoader;
28
29 import javax.management.ObjectName;
30 import javax.servlet.ServletContext;
31
32 import org.apache.catalina.Context;
33 import org.apache.catalina.Globals;
34 import org.apache.catalina.LifecycleException;
35 import org.apache.catalina.LifecycleState;
36 import org.apache.catalina.Loader;
37 import org.apache.catalina.util.LifecycleMBeanBase;
38 import org.apache.catalina.util.ToStringUtil;
39 import org.apache.juli.logging.Log;
40 import org.apache.juli.logging.LogFactory;
41 import org.apache.tomcat.util.ExceptionUtils;
42 import org.apache.tomcat.util.buf.UDecoder;
43 import org.apache.tomcat.util.compat.JreCompat;
44 import org.apache.tomcat.util.modeler.Registry;
45 import org.apache.tomcat.util.res.StringManager;
46
47 /**
48 * Classloader implementation which is specialized for handling web
49 * applications in the most efficient way, while being Catalina aware (all
50 * accesses to resources are made through
51 * {@link org.apache.catalina.WebResourceRoot}).
52 * This class loader supports detection of modified
53 * Java classes, which can be used to implement auto-reload support.
54 * <p>
55 * This class loader is configured via the Resources children of its Context
56 * prior to calling <code>start()</code>. When a new class is required,
57 * these Resources will be consulted first to locate the class. If it
58 * is not present, the system class loader will be used instead.
59 *
60 * @author Craig R. McClanahan
61 * @author Remy Maucherat
62 */
63 public class WebappLoader extends LifecycleMBeanBase
64 implements Loader, PropertyChangeListener {
65
66 private static final Log log = LogFactory.getLog(WebappLoader.class);
67
68 // ----------------------------------------------------------- Constructors
69
70 /**
71 * Construct a new WebappLoader. The parent class loader will be defined by
72 * {@link Context#getParentClassLoader()}.
73 */
74 public WebappLoader() {
75 this(null);
76 }
77
78
79 /**
80 * Construct a new WebappLoader with the specified class loader
81 * to be defined as the parent of the ClassLoader we ultimately create.
82 *
83 * @param parent The parent class loader
84 *
85 * @deprecated Use {@link Context#setParentClassLoader(ClassLoader)} to
86 * specify the required class loader. This method will be
87 * removed in Tomcat 10 onwards.
88 */
89 @Deprecated
90 public WebappLoader(ClassLoader parent) {
91 super();
92 this.parentClassLoader = parent;
93 }
94
95
96 // ----------------------------------------------------- Instance Variables
97
98 /**
99 * The class loader being managed by this Loader component.
100 */
101 private WebappClassLoaderBase classLoader = null;
102
103
104 /**
105 * The Context with which this Loader has been associated.
106 */
107 private Context context = null;
108
109
110 /**
111 * The "follow standard delegation model" flag that will be used to
112 * configure our ClassLoader.
113 */
114 private boolean delegate = false;
115
116
117 /**
118 * The Java class name of the ClassLoader implementation to be used.
119 * This class should extend WebappClassLoaderBase, otherwise, a different
120 * loader implementation must be used.
121 */
122 private String loaderClass = ParallelWebappClassLoader.class.getName();
123
124
125 /**
126 * The parent class loader of the class loader we will create.
127 */
128 private ClassLoader parentClassLoader = null;
129
130
131 /**
132 * The reloadable flag for this Loader.
133 */
134 private boolean reloadable = false;
135
136
137 /**
138 * The string manager for this package.
139 */
140 protected static final StringManager sm =
141 StringManager.getManager(Constants.Package);
142
143
144 /**
145 * The property change support for this component.
146 */
147 protected final PropertyChangeSupport support = new PropertyChangeSupport(this);
148
149
150 /**
151 * Classpath set in the loader.
152 */
153 private String classpath = null;
154
155
156 // ------------------------------------------------------------- Properties
157
158 /**
159 * Return the Java class loader to be used by this Container.
160 */
161 @Override
162 public ClassLoader getClassLoader() {
163 return classLoader;
164 }
165
166
167 @Override
168 public Context getContext() {
169 return context;
170 }
171
172
173 @Override
174 public void setContext(Context context) {
175
176 if (this.context == context) {
177 return;
178 }
179
180 if (getState().isAvailable()) {
181 throw new IllegalStateException(
182 sm.getString("webappLoader.setContext.ise"));
183 }
184
185 // Deregister from the old Context (if any)
186 if (this.context != null) {
187 this.context.removePropertyChangeListener(this);
188 }
189
190 // Process this property change
191 Context oldContext = this.context;
192 this.context = context;
193 support.firePropertyChange("context", oldContext, this.context);
194
195 // Register with the new Container (if any)
196 if (this.context != null) {
197 setReloadable(this.context.getReloadable());
198 this.context.addPropertyChangeListener(this);
199 }
200 }
201
202
203 /**
204 * Return the "follow standard delegation model" flag used to configure
205 * our ClassLoader.
206 */
207 @Override
208 public boolean getDelegate() {
209 return this.delegate;
210 }
211
212
213 /**
214 * Set the "follow standard delegation model" flag used to configure
215 * our ClassLoader.
216 *
217 * @param delegate The new flag
218 */
219 @Override
220 public void setDelegate(boolean delegate) {
221 boolean oldDelegate = this.delegate;
222 this.delegate = delegate;
223 support.firePropertyChange("delegate", Boolean.valueOf(oldDelegate),
224 Boolean.valueOf(this.delegate));
225 }
226
227
228 /**
229 * @return the ClassLoader class name.
230 */
231 public String getLoaderClass() {
232 return this.loaderClass;
233 }
234
235
236 /**
237 * Set the ClassLoader class name.
238 *
239 * @param loaderClass The new ClassLoader class name
240 */
241 public void setLoaderClass(String loaderClass) {
242 this.loaderClass = loaderClass;
243 }
244
245
246 /**
247 * Return the reloadable flag for this Loader.
248 */
249 @Override
250 public boolean getReloadable() {
251 return this.reloadable;
252 }
253
254
255 /**
256 * Set the reloadable flag for this Loader.
257 *
258 * @param reloadable The new reloadable flag
259 */
260 @Override
261 public void setReloadable(boolean reloadable) {
262 // Process this property change
263 boolean oldReloadable = this.reloadable;
264 this.reloadable = reloadable;
265 support.firePropertyChange("reloadable",
266 Boolean.valueOf(oldReloadable),
267 Boolean.valueOf(this.reloadable));
268 }
269
270
271 // --------------------------------------------------------- Public Methods
272
273 /**
274 * Add a property change listener to this component.
275 *
276 * @param listener The listener to add
277 */
278 @Override
279 public void addPropertyChangeListener(PropertyChangeListener listener) {
280
281 support.addPropertyChangeListener(listener);
282
283 }
284
285
286 /**
287 * Execute a periodic task, such as reloading, etc. This method will be
288 * invoked inside the classloading context of this container. Unexpected
289 * throwables will be caught and logged.
290 */
291 @Override
292 public void backgroundProcess() {
293 if (reloadable && modified()) {
294 try {
295 Thread.currentThread().setContextClassLoader
296 (WebappLoader.class.getClassLoader());
297 if (context != null) {
298 context.reload();
299 }
300 } finally {
301 if (context != null && context.getLoader() != null) {
302 Thread.currentThread().setContextClassLoader
303 (context.getLoader().getClassLoader());
304 }
305 }
306 }
307 }
308
309
310 public String[] getLoaderRepositories() {
311 if (classLoader == null) {
312 return new String[0];
313 }
314 URL[] urls = classLoader.getURLs();
315 String[] result = new String[urls.length];
316 for (int i = 0; i < urls.length; i++) {
317 result[i] = urls[i].toExternalForm();
318 }
319 return result;
320 }
321
322 public String getLoaderRepositoriesString() {
323 String repositories[]=getLoaderRepositories();
324 StringBuilder sb=new StringBuilder();
325 for( int i=0; i<repositories.length ; i++ ) {
326 sb.append( repositories[i]).append(":");
327 }
328 return sb.toString();
329 }
330
331
332 /**
333 * Classpath, as set in org.apache.catalina.jsp_classpath context
334 * property
335 *
336 * @return The classpath
337 */
338 public String getClasspath() {
339 return classpath;
340 }
341
342
343 /**
344 * Has the internal repository associated with this Loader been modified,
345 * such that the loaded classes should be reloaded?
346 */
347 @Override
348 public boolean modified() {
349 return classLoader != null ? classLoader.modified() : false ;
350 }
351
352
353 /**
354 * Remove a property change listener from this component.
355 *
356 * @param listener The listener to remove
357 */
358 @Override
359 public void removePropertyChangeListener(PropertyChangeListener listener) {
360 support.removePropertyChangeListener(listener);
361 }
362
363
364 /**
365 * Return a String representation of this component.
366 */
367 @Override
368 public String toString() {
369 return ToStringUtil.toString(this, context);
370 }
371
372
373 /**
374 * Start associated {@link ClassLoader} and implement the requirements
375 * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
376 *
377 * @exception LifecycleException if this component detects a fatal error
378 * that prevents this component from being used
379 */
380 @Override
381 protected void startInternal() throws LifecycleException {
382
383 if (log.isDebugEnabled())
384 log.debug(sm.getString("webappLoader.starting"));
385
386 if (context.getResources() == null) {
387 log.info(sm.getString("webappLoader.noResources", context));
388 setState(LifecycleState.STARTING);
389 return;
390 }
391
392 // Construct a class loader based on our current repositories list
393 try {
394
395 classLoader = createClassLoader();
396 classLoader.setResources(context.getResources());
397 classLoader.setDelegate(this.delegate);
398
399 // Configure our repositories
400 setClassPath();
401
402 setPermissions();
403
404 classLoader.start();
405
406 String contextName = context.getName();
407 if (!contextName.startsWith("/")) {
408 contextName = "/" + contextName;
409 }
410 ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
411 classLoader.getClass().getSimpleName() + ",host=" +
412 context.getParent().getName() + ",context=" + contextName);
413 Registry.getRegistry(null, null)
414 .registerComponent(classLoader, cloname, null);
415
416 } catch (Throwable t) {
417 t = ExceptionUtils.unwrapInvocationTargetException(t);
418 ExceptionUtils.handleThrowable(t);
419 throw new LifecycleException(sm.getString("webappLoader.startError"), t);
420 }
421
422 setState(LifecycleState.STARTING);
423 }
424
425
426 /**
427 * Stop associated {@link ClassLoader} and implement the requirements
428 * of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
429 *
430 * @exception LifecycleException if this component detects a fatal error
431 * that prevents this component from being used
432 */
433 @Override
434 protected void stopInternal() throws LifecycleException {
435
436 if (log.isDebugEnabled())
437 log.debug(sm.getString("webappLoader.stopping"));
438
439 setState(LifecycleState.STOPPING);
440
441 // Remove context attributes as appropriate
442 ServletContext servletContext = context.getServletContext();
443 servletContext.removeAttribute(Globals.CLASS_PATH_ATTR);
444
445 // Throw away our current class loader if any
446 if (classLoader != null) {
447 try {
448 classLoader.stop();
449 } finally {
450 classLoader.destroy();
451 }
452
453 // classLoader must be non-null to have been registered
454 try {
455 String contextName = context.getName();
456 if (!contextName.startsWith("/")) {
457 contextName = "/" + contextName;
458 }
459 ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
460 classLoader.getClass().getSimpleName() + ",host=" +
461 context.getParent().getName() + ",context=" + contextName);
462 Registry.getRegistry(null, null).unregisterComponent(cloname);
463 } catch (Exception e) {
464 log.warn(sm.getString("webappLoader.stopError"), e);
465 }
466 }
467
468
469 classLoader = null;
470 }
471
472
473 // ----------------------------------------- PropertyChangeListener Methods
474
475
476 /**
477 * Process property change events from our associated Context.
478 *
479 * @param event The property change event that has occurred
480 */
481 @Override
482 public void propertyChange(PropertyChangeEvent event) {
483
484 // Validate the source of this event
485 if (!(event.getSource() instanceof Context))
486 return;
487
488 // Process a relevant property change
489 if (event.getPropertyName().equals("reloadable")) {
490 try {
491 setReloadable
492 ( ((Boolean) event.getNewValue()).booleanValue() );
493 } catch (NumberFormatException e) {
494 log.error(sm.getString("webappLoader.reloadable",
495 event.getNewValue().toString()));
496 }
497 }
498 }
499
500
501 // ------------------------------------------------------- Private Methods
502
503 /**
504 * Create associated classLoader.
505 */
506 private WebappClassLoaderBase createClassLoader()
507 throws Exception {
508
509 Class<?> clazz = Class.forName(loaderClass);
510 WebappClassLoaderBase classLoader = null;
511
512 if (parentClassLoader == null) {
513 parentClassLoader = context.getParentClassLoader();
514 } else {
515 context.setParentClassLoader(parentClassLoader);
516 }
517 Class<?>[] argTypes = { ClassLoader.class };
518 Object[] args = { parentClassLoader };
519 Constructor<?> constr = clazz.getConstructor(argTypes);
520 classLoader = (WebappClassLoaderBase) constr.newInstance(args);
521
522 return classLoader;
523 }
524
525
526 /**
527 * Configure associated class loader permissions.
528 */
529 private void setPermissions() {
530
531 if (!Globals.IS_SECURITY_ENABLED)
532 return;
533 if (context == null)
534 return;
535
536 // Tell the class loader the root of the context
537 ServletContext servletContext = context.getServletContext();
538
539 // Assigning permissions for the work directory
540 File workDir =
541 (File) servletContext.getAttribute(ServletContext.TEMPDIR);
542 if (workDir != null) {
543 try {
544 String workDirPath = workDir.getCanonicalPath();
545 classLoader.addPermission
546 (new FilePermission(workDirPath, "read,write"));
547 classLoader.addPermission
548 (new FilePermission(workDirPath + File.separator + "-",
549 "read,write,delete"));
550 } catch (IOException e) {
551 // Ignore
552 }
553 }
554
555 for (URL url : context.getResources().getBaseUrls()) {
556 classLoader.addPermission(url);
557 }
558 }
559
560
561 /**
562 * Set the appropriate context attribute for our class path. This
563 * is required only because Jasper depends on it.
564 */
565 private void setClassPath() {
566
567 // Validate our current state information
568 if (context == null)
569 return;
570 ServletContext servletContext = context.getServletContext();
571 if (servletContext == null)
572 return;
573
574 StringBuilder classpath = new StringBuilder();
575
576 // Assemble the class path information from our class loader chain
577 ClassLoader loader = getClassLoader();
578
579 if (delegate && loader != null) {
580 // Skip the webapp loader for now as delegation is enabled
581 loader = loader.getParent();
582 }
583
584 while (loader != null) {
585 if (!buildClassPath(classpath, loader)) {
586 break;
587 }
588 loader = loader.getParent();
589 }
590
591 if (delegate) {
592 // Delegation was enabled, go back and add the webapp paths
593 loader = getClassLoader();
594 if (loader != null) {
595 buildClassPath(classpath, loader);
596 }
597 }
598
599 this.classpath = classpath.toString();
600
601 // Store the assembled class path as a servlet context attribute
602 servletContext.setAttribute(Globals.CLASS_PATH_ATTR, this.classpath);
603 }
604
605
606 private boolean buildClassPath(StringBuilder classpath, ClassLoader loader) {
607 if (loader instanceof URLClassLoader) {
608 URL repositories[] = ((URLClassLoader) loader).getURLs();
609 for (int i = 0; i < repositories.length; i++) {
610 String repository = repositories[i].toString();
611 if (repository.startsWith("file://"))
612 repository = UDecoder.URLDecode(repository.substring(7));
613 else if (repository.startsWith("file:"))
614 repository = UDecoder.URLDecode(repository.substring(5));
615 else
616 continue;
617 if (repository == null)
618 continue;
619 if (classpath.length() > 0)
620 classpath.append(File.pathSeparator);
621 classpath.append(repository);
622 }
623 } else if (loader == ClassLoader.getSystemClassLoader()){
624 // Java 9 onwards. The internal class loaders no longer extend
625 // URLCLassLoader
626 String cp = System.getProperty("java.class.path");
627 if (cp != null && cp.length() > 0) {
628 if (classpath.length() > 0) {
629 classpath.append(File.pathSeparator);
630 }
631 classpath.append(cp);
632 }
633 return false;
634 } else {
635 // Ignore Graal "unknown" classloader
636 if (!JreCompat.isGraalAvailable()) {
637 log.info(sm.getString("webappLoader.unknownClassLoader", loader, loader.getClass()));
638 }
639 return false;
640 }
641 return true;
642 }
643
644 @Override
645 protected String getDomainInternal() {
646 return context.getDomain();
647 }
648
649
650 @Override
651 protected String getObjectNameKeyProperties() {
652
653 StringBuilder name = new StringBuilder("type=Loader");
654
655 name.append(",host=");
656 name.append(context.getParent().getName());
657
658 name.append(",context=");
659
660 String contextName = context.getName();
661 if (!contextName.startsWith("/")) {
662 name.append("/");
663 }
664 name.append(contextName);
665
666 return name.toString();
667 }
668 }
669