1 /* 2 * Copyright (C) 2012 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.common.util.concurrent; 18 19 import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION; 20 import static com.google.common.base.StandardSystemProperty.OS_NAME; 21 import static com.google.common.truth.Truth.assertThat; 22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 23 import static java.util.Arrays.asList; 24 import static java.util.concurrent.TimeUnit.SECONDS; 25 import static org.junit.Assert.assertThrows; 26 27 import com.google.common.collect.ImmutableMap; 28 import com.google.common.collect.ImmutableSet; 29 import com.google.common.collect.Lists; 30 import com.google.common.collect.Sets; 31 import com.google.common.testing.NullPointerTester; 32 import com.google.common.testing.TestLogHandler; 33 import com.google.common.util.concurrent.Service.State; 34 import com.google.common.util.concurrent.ServiceManager.Listener; 35 import java.time.Duration; 36 import java.util.Arrays; 37 import java.util.Collection; 38 import java.util.List; 39 import java.util.Set; 40 import java.util.concurrent.CountDownLatch; 41 import java.util.concurrent.Executor; 42 import java.util.concurrent.TimeUnit; 43 import java.util.concurrent.TimeoutException; 44 import java.util.logging.Formatter; 45 import java.util.logging.Level; 46 import java.util.logging.LogRecord; 47 import java.util.logging.Logger; 48 import junit.framework.TestCase; 49 50 /** 51 * Tests for {@link ServiceManager}. 52 * 53 * @author Luke Sandberg 54 * @author Chris Nokleberg 55 */ 56 public class ServiceManagerTest extends TestCase { 57 58 private static class NoOpService extends AbstractService { 59 @Override doStart()60 protected void doStart() { 61 notifyStarted(); 62 } 63 64 @Override doStop()65 protected void doStop() { 66 notifyStopped(); 67 } 68 } 69 70 /* 71 * A NoOp service that will delay the startup and shutdown notification for a configurable amount 72 * of time. 73 */ 74 private static class NoOpDelayedService extends NoOpService { 75 private long delay; 76 NoOpDelayedService(long delay)77 public NoOpDelayedService(long delay) { 78 this.delay = delay; 79 } 80 81 @Override doStart()82 protected void doStart() { 83 new Thread() { 84 @Override 85 public void run() { 86 Uninterruptibles.sleepUninterruptibly(delay, TimeUnit.MILLISECONDS); 87 notifyStarted(); 88 } 89 }.start(); 90 } 91 92 @Override doStop()93 protected void doStop() { 94 new Thread() { 95 @Override 96 public void run() { 97 Uninterruptibles.sleepUninterruptibly(delay, TimeUnit.MILLISECONDS); 98 notifyStopped(); 99 } 100 }.start(); 101 } 102 } 103 104 private static class FailStartService extends NoOpService { 105 @Override doStart()106 protected void doStart() { 107 notifyFailed(new IllegalStateException("start failure")); 108 } 109 } 110 111 private static class FailRunService extends NoOpService { 112 @Override doStart()113 protected void doStart() { 114 super.doStart(); 115 notifyFailed(new IllegalStateException("run failure")); 116 } 117 } 118 119 private static class FailStopService extends NoOpService { 120 @Override doStop()121 protected void doStop() { 122 notifyFailed(new IllegalStateException("stop failure")); 123 } 124 } 125 testServiceStartupTimes()126 public void testServiceStartupTimes() { 127 if (isWindows() && isJava8()) { 128 // Flaky there: https://github.com/google/guava/pull/6731#issuecomment-1736298607 129 return; 130 } 131 Service a = new NoOpDelayedService(150); 132 Service b = new NoOpDelayedService(353); 133 ServiceManager serviceManager = new ServiceManager(asList(a, b)); 134 serviceManager.startAsync().awaitHealthy(); 135 ImmutableMap<Service, Long> startupTimes = serviceManager.startupTimes(); 136 assertThat(startupTimes).hasSize(2); 137 assertThat(startupTimes.get(a)).isAtLeast(150); 138 assertThat(startupTimes.get(b)).isAtLeast(353); 139 } 140 testServiceStartupDurations()141 public void testServiceStartupDurations() { 142 if (isWindows() && isJava8()) { 143 // Flaky there: https://github.com/google/guava/pull/6731#issuecomment-1736298607 144 return; 145 } 146 Service a = new NoOpDelayedService(150); 147 Service b = new NoOpDelayedService(353); 148 ServiceManager serviceManager = new ServiceManager(asList(a, b)); 149 serviceManager.startAsync().awaitHealthy(); 150 ImmutableMap<Service, Duration> startupTimes = serviceManager.startupDurations(); 151 assertThat(startupTimes).hasSize(2); 152 assertThat(startupTimes.get(a)).isAtLeast(Duration.ofMillis(150)); 153 assertThat(startupTimes.get(b)).isAtLeast(Duration.ofMillis(353)); 154 } 155 testServiceStartupTimes_selfStartingServices()156 public void testServiceStartupTimes_selfStartingServices() { 157 // This tests to ensure that: 158 // 1. service times are accurate when the service is started by the manager 159 // 2. service times are recorded when the service is not started by the manager (but they may 160 // not be accurate). 161 final Service b = 162 new NoOpDelayedService(353) { 163 @Override 164 protected void doStart() { 165 super.doStart(); 166 // This will delay service listener execution at least 150 milliseconds 167 Uninterruptibles.sleepUninterruptibly(150, TimeUnit.MILLISECONDS); 168 } 169 }; 170 Service a = 171 new NoOpDelayedService(150) { 172 @Override 173 protected void doStart() { 174 b.startAsync(); 175 super.doStart(); 176 } 177 }; 178 ServiceManager serviceManager = new ServiceManager(asList(a, b)); 179 serviceManager.startAsync().awaitHealthy(); 180 ImmutableMap<Service, Long> startupTimes = serviceManager.startupTimes(); 181 assertThat(startupTimes).hasSize(2); 182 assertThat(startupTimes.get(a)).isAtLeast(150); 183 // Service b startup takes at least 353 millis, but starting the timer is delayed by at least 184 // 150 milliseconds. so in a perfect world the timing would be 353-150=203ms, but since either 185 // of our sleep calls can be arbitrarily delayed we should just assert that there is a time 186 // recorded. 187 assertThat(startupTimes.get(b)).isNotNull(); 188 } 189 testServiceStartStop()190 public void testServiceStartStop() { 191 Service a = new NoOpService(); 192 Service b = new NoOpService(); 193 ServiceManager manager = new ServiceManager(asList(a, b)); 194 RecordingListener listener = new RecordingListener(); 195 manager.addListener(listener, directExecutor()); 196 assertState(manager, Service.State.NEW, a, b); 197 assertFalse(manager.isHealthy()); 198 manager.startAsync().awaitHealthy(); 199 assertState(manager, Service.State.RUNNING, a, b); 200 assertTrue(manager.isHealthy()); 201 assertTrue(listener.healthyCalled); 202 assertFalse(listener.stoppedCalled); 203 assertTrue(listener.failedServices.isEmpty()); 204 manager.stopAsync().awaitStopped(); 205 assertState(manager, Service.State.TERMINATED, a, b); 206 assertFalse(manager.isHealthy()); 207 assertTrue(listener.stoppedCalled); 208 assertTrue(listener.failedServices.isEmpty()); 209 } 210 testFailStart()211 public void testFailStart() throws Exception { 212 Service a = new NoOpService(); 213 Service b = new FailStartService(); 214 Service c = new NoOpService(); 215 Service d = new FailStartService(); 216 Service e = new NoOpService(); 217 ServiceManager manager = new ServiceManager(asList(a, b, c, d, e)); 218 RecordingListener listener = new RecordingListener(); 219 manager.addListener(listener, directExecutor()); 220 assertState(manager, Service.State.NEW, a, b, c, d, e); 221 assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy()); 222 assertFalse(listener.healthyCalled); 223 assertState(manager, Service.State.RUNNING, a, c, e); 224 assertEquals(ImmutableSet.of(b, d), listener.failedServices); 225 assertState(manager, Service.State.FAILED, b, d); 226 assertFalse(manager.isHealthy()); 227 228 manager.stopAsync().awaitStopped(); 229 assertFalse(manager.isHealthy()); 230 assertFalse(listener.healthyCalled); 231 assertTrue(listener.stoppedCalled); 232 } 233 testFailRun()234 public void testFailRun() throws Exception { 235 Service a = new NoOpService(); 236 Service b = new FailRunService(); 237 ServiceManager manager = new ServiceManager(asList(a, b)); 238 RecordingListener listener = new RecordingListener(); 239 manager.addListener(listener, directExecutor()); 240 assertState(manager, Service.State.NEW, a, b); 241 assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy()); 242 assertTrue(listener.healthyCalled); 243 assertEquals(ImmutableSet.of(b), listener.failedServices); 244 245 manager.stopAsync().awaitStopped(); 246 assertState(manager, Service.State.FAILED, b); 247 assertState(manager, Service.State.TERMINATED, a); 248 249 assertTrue(listener.stoppedCalled); 250 } 251 testFailStop()252 public void testFailStop() throws Exception { 253 Service a = new NoOpService(); 254 Service b = new FailStopService(); 255 Service c = new NoOpService(); 256 ServiceManager manager = new ServiceManager(asList(a, b, c)); 257 RecordingListener listener = new RecordingListener(); 258 manager.addListener(listener, directExecutor()); 259 260 manager.startAsync().awaitHealthy(); 261 assertTrue(listener.healthyCalled); 262 assertFalse(listener.stoppedCalled); 263 manager.stopAsync().awaitStopped(); 264 265 assertTrue(listener.stoppedCalled); 266 assertEquals(ImmutableSet.of(b), listener.failedServices); 267 assertState(manager, Service.State.FAILED, b); 268 assertState(manager, Service.State.TERMINATED, a, c); 269 } 270 testToString()271 public void testToString() throws Exception { 272 Service a = new NoOpService(); 273 Service b = new FailStartService(); 274 ServiceManager manager = new ServiceManager(asList(a, b)); 275 String toString = manager.toString(); 276 assertThat(toString).contains("NoOpService"); 277 assertThat(toString).contains("FailStartService"); 278 } 279 testTimeouts()280 public void testTimeouts() throws Exception { 281 Service a = new NoOpDelayedService(50); 282 ServiceManager manager = new ServiceManager(asList(a)); 283 manager.startAsync(); 284 assertThrows(TimeoutException.class, () -> manager.awaitHealthy(1, TimeUnit.MILLISECONDS)); 285 manager.awaitHealthy(5, SECONDS); // no exception thrown 286 287 manager.stopAsync(); 288 assertThrows(TimeoutException.class, () -> manager.awaitStopped(1, TimeUnit.MILLISECONDS)); 289 manager.awaitStopped(5, SECONDS); // no exception thrown 290 } 291 292 /** 293 * This covers a case where if the last service to stop failed then the stopped callback would 294 * never be called. 295 */ testSingleFailedServiceCallsStopped()296 public void testSingleFailedServiceCallsStopped() { 297 Service a = new FailStartService(); 298 ServiceManager manager = new ServiceManager(asList(a)); 299 RecordingListener listener = new RecordingListener(); 300 manager.addListener(listener, directExecutor()); 301 assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy()); 302 assertTrue(listener.stoppedCalled); 303 } 304 305 /** 306 * This covers a bug where listener.healthy would get called when a single service failed during 307 * startup (it occurred in more complicated cases also). 308 */ testFailStart_singleServiceCallsHealthy()309 public void testFailStart_singleServiceCallsHealthy() { 310 Service a = new FailStartService(); 311 ServiceManager manager = new ServiceManager(asList(a)); 312 RecordingListener listener = new RecordingListener(); 313 manager.addListener(listener, directExecutor()); 314 assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy()); 315 assertFalse(listener.healthyCalled); 316 } 317 318 /** 319 * This covers a bug where if a listener was installed that would stop the manager if any service 320 * fails and something failed during startup before service.start was called on all the services, 321 * then awaitStopped would deadlock due to an IllegalStateException that was thrown when trying to 322 * stop the timer(!). 323 */ testFailStart_stopOthers()324 public void testFailStart_stopOthers() throws TimeoutException { 325 Service a = new FailStartService(); 326 Service b = new NoOpService(); 327 final ServiceManager manager = new ServiceManager(asList(a, b)); 328 manager.addListener( 329 new Listener() { 330 @Override 331 public void failure(Service service) { 332 manager.stopAsync(); 333 } 334 }, 335 directExecutor()); 336 manager.startAsync(); 337 manager.awaitStopped(10, TimeUnit.MILLISECONDS); 338 } 339 testDoCancelStart()340 public void testDoCancelStart() throws TimeoutException { 341 Service a = 342 new AbstractService() { 343 @Override 344 protected void doStart() { 345 // Never starts! 346 } 347 348 @Override 349 protected void doCancelStart() { 350 assertThat(state()).isEqualTo(Service.State.STOPPING); 351 notifyStopped(); 352 } 353 354 @Override 355 protected void doStop() { 356 throw new AssertionError(); // Should not be called. 357 } 358 }; 359 360 final ServiceManager manager = new ServiceManager(asList(a)); 361 manager.startAsync(); 362 manager.stopAsync(); 363 manager.awaitStopped(10, TimeUnit.MILLISECONDS); 364 assertThat(manager.servicesByState().keySet()).containsExactly(Service.State.TERMINATED); 365 } 366 testNotifyStoppedAfterFailure()367 public void testNotifyStoppedAfterFailure() throws TimeoutException { 368 Service a = 369 new AbstractService() { 370 @Override 371 protected void doStart() { 372 notifyFailed(new IllegalStateException("start failure")); 373 notifyStopped(); // This will be a no-op. 374 } 375 376 @Override 377 protected void doStop() { 378 notifyStopped(); 379 } 380 }; 381 final ServiceManager manager = new ServiceManager(asList(a)); 382 manager.startAsync(); 383 manager.awaitStopped(10, TimeUnit.MILLISECONDS); 384 assertThat(manager.servicesByState().keySet()).containsExactly(Service.State.FAILED); 385 } 386 assertState( ServiceManager manager, Service.State state, Service... services)387 private static void assertState( 388 ServiceManager manager, Service.State state, Service... services) { 389 Collection<Service> managerServices = manager.servicesByState().get(state); 390 for (Service service : services) { 391 assertEquals(service.toString(), state, service.state()); 392 assertEquals(service.toString(), service.isRunning(), state == Service.State.RUNNING); 393 assertTrue(managerServices + " should contain " + service, managerServices.contains(service)); 394 } 395 } 396 397 /** 398 * This is for covering a case where the ServiceManager would behave strangely if constructed with 399 * no service under management. Listeners would never fire because the ServiceManager was healthy 400 * and stopped at the same time. This test ensures that listeners fire and isHealthy makes sense. 401 */ testEmptyServiceManager()402 public void testEmptyServiceManager() { 403 Logger logger = Logger.getLogger(ServiceManager.class.getName()); 404 logger.setLevel(Level.FINEST); 405 TestLogHandler logHandler = new TestLogHandler(); 406 logger.addHandler(logHandler); 407 ServiceManager manager = new ServiceManager(Arrays.<Service>asList()); 408 RecordingListener listener = new RecordingListener(); 409 manager.addListener(listener, directExecutor()); 410 manager.startAsync().awaitHealthy(); 411 assertTrue(manager.isHealthy()); 412 assertTrue(listener.healthyCalled); 413 assertFalse(listener.stoppedCalled); 414 assertTrue(listener.failedServices.isEmpty()); 415 manager.stopAsync().awaitStopped(); 416 assertFalse(manager.isHealthy()); 417 assertTrue(listener.stoppedCalled); 418 assertTrue(listener.failedServices.isEmpty()); 419 // check that our NoOpService is not directly observable via any of the inspection methods or 420 // via logging. 421 assertEquals("ServiceManager{services=[]}", manager.toString()); 422 assertTrue(manager.servicesByState().isEmpty()); 423 assertTrue(manager.startupTimes().isEmpty()); 424 Formatter logFormatter = 425 new Formatter() { 426 @Override 427 public String format(LogRecord record) { 428 return formatMessage(record); 429 } 430 }; 431 for (LogRecord record : logHandler.getStoredLogRecords()) { 432 assertThat(logFormatter.format(record)).doesNotContain("NoOpService"); 433 } 434 } 435 436 /** 437 * Tests that a ServiceManager can be fully shut down if one of its failure listeners is slow or 438 * even permanently blocked. 439 */ testListenerDeadlock()440 public void testListenerDeadlock() throws InterruptedException { 441 final CountDownLatch failEnter = new CountDownLatch(1); 442 final CountDownLatch failLeave = new CountDownLatch(1); 443 final CountDownLatch afterStarted = new CountDownLatch(1); 444 Service failRunService = 445 new AbstractService() { 446 @Override 447 protected void doStart() { 448 new Thread() { 449 @Override 450 public void run() { 451 notifyStarted(); 452 // We need to wait for the main thread to leave the ServiceManager.startAsync call 453 // to 454 // ensure that the thread running the failure callbacks is not the main thread. 455 Uninterruptibles.awaitUninterruptibly(afterStarted); 456 notifyFailed(new Exception("boom")); 457 } 458 }.start(); 459 } 460 461 @Override 462 protected void doStop() { 463 notifyStopped(); 464 } 465 }; 466 final ServiceManager manager = 467 new ServiceManager(Arrays.asList(failRunService, new NoOpService())); 468 manager.addListener( 469 new ServiceManager.Listener() { 470 @Override 471 public void failure(Service service) { 472 failEnter.countDown(); 473 // block until after the service manager is shutdown 474 Uninterruptibles.awaitUninterruptibly(failLeave); 475 } 476 }, 477 directExecutor()); 478 manager.startAsync(); 479 afterStarted.countDown(); 480 // We do not call awaitHealthy because, due to races, that method may throw an exception. But 481 // we really just want to wait for the thread to be in the failure callback so we wait for that 482 // explicitly instead. 483 failEnter.await(); 484 assertFalse("State should be updated before calling listeners", manager.isHealthy()); 485 // now we want to stop the services. 486 Thread stoppingThread = 487 new Thread() { 488 @Override 489 public void run() { 490 manager.stopAsync().awaitStopped(); 491 } 492 }; 493 stoppingThread.start(); 494 // this should be super fast since the only non-stopped service is a NoOpService 495 stoppingThread.join(1000); 496 assertFalse("stopAsync has deadlocked!.", stoppingThread.isAlive()); 497 failLeave.countDown(); // release the background thread 498 } 499 500 /** 501 * Catches a bug where when constructing a service manager failed, later interactions with the 502 * service could cause IllegalStateExceptions inside the partially constructed ServiceManager. 503 * This ISE wouldn't actually bubble up but would get logged by ExecutionQueue. This obfuscated 504 * the original error (which was not constructing ServiceManager correctly). 505 */ testPartiallyConstructedManager()506 public void testPartiallyConstructedManager() { 507 Logger logger = Logger.getLogger("global"); 508 logger.setLevel(Level.FINEST); 509 TestLogHandler logHandler = new TestLogHandler(); 510 logger.addHandler(logHandler); 511 NoOpService service = new NoOpService(); 512 service.startAsync(); 513 assertThrows(IllegalArgumentException.class, () -> new ServiceManager(Arrays.asList(service))); 514 service.stopAsync(); 515 // Nothing was logged! 516 assertEquals(0, logHandler.getStoredLogRecords().size()); 517 } 518 testPartiallyConstructedManager_transitionAfterAddListenerBeforeStateIsReady()519 public void testPartiallyConstructedManager_transitionAfterAddListenerBeforeStateIsReady() { 520 // The implementation of this test is pretty sensitive to the implementation :( but we want to 521 // ensure that if weird things happen during construction then we get exceptions. 522 final NoOpService service1 = new NoOpService(); 523 // This service will start service1 when addListener is called. This simulates service1 being 524 // started asynchronously. 525 Service service2 = 526 new Service() { 527 final NoOpService delegate = new NoOpService(); 528 529 @Override 530 public final void addListener(Listener listener, Executor executor) { 531 service1.startAsync(); 532 delegate.addListener(listener, executor); 533 } 534 535 // Delegates from here on down 536 @Override 537 public final Service startAsync() { 538 return delegate.startAsync(); 539 } 540 541 @Override 542 public final Service stopAsync() { 543 return delegate.stopAsync(); 544 } 545 546 @Override 547 public final void awaitRunning() { 548 delegate.awaitRunning(); 549 } 550 551 @Override 552 public final void awaitRunning(long timeout, TimeUnit unit) throws TimeoutException { 553 delegate.awaitRunning(timeout, unit); 554 } 555 556 @Override 557 public final void awaitTerminated() { 558 delegate.awaitTerminated(); 559 } 560 561 @Override 562 public final void awaitTerminated(long timeout, TimeUnit unit) throws TimeoutException { 563 delegate.awaitTerminated(timeout, unit); 564 } 565 566 @Override 567 public final boolean isRunning() { 568 return delegate.isRunning(); 569 } 570 571 @Override 572 public final State state() { 573 return delegate.state(); 574 } 575 576 @Override 577 public final Throwable failureCause() { 578 return delegate.failureCause(); 579 } 580 }; 581 IllegalArgumentException expected = 582 assertThrows( 583 IllegalArgumentException.class, 584 () -> new ServiceManager(Arrays.asList(service1, service2))); 585 assertThat(expected.getMessage()).contains("started transitioning asynchronously"); 586 } 587 588 /** 589 * This test is for a case where two Service.Listener callbacks for the same service would call 590 * transitionService in the wrong order due to a race. Due to the fact that it is a race this test 591 * isn't guaranteed to expose the issue, but it is at least likely to become flaky if the race 592 * sneaks back in, and in this case flaky means something is definitely wrong. 593 * 594 * <p>Before the bug was fixed this test would fail at least 30% of the time. 595 */ testTransitionRace()596 public void testTransitionRace() throws TimeoutException { 597 for (int k = 0; k < 1000; k++) { 598 List<Service> services = Lists.newArrayList(); 599 for (int i = 0; i < 5; i++) { 600 services.add(new SnappyShutdownService(i)); 601 } 602 ServiceManager manager = new ServiceManager(services); 603 manager.startAsync().awaitHealthy(); 604 manager.stopAsync().awaitStopped(10, TimeUnit.SECONDS); 605 } 606 } 607 608 /** 609 * This service will shut down very quickly after stopAsync is called and uses a background thread 610 * so that we know that the stopping() listeners will execute on a different thread than the 611 * terminated() listeners. 612 */ 613 private static class SnappyShutdownService extends AbstractExecutionThreadService { 614 final int index; 615 final CountDownLatch latch = new CountDownLatch(1); 616 SnappyShutdownService(int index)617 SnappyShutdownService(int index) { 618 this.index = index; 619 } 620 621 @Override run()622 protected void run() throws Exception { 623 latch.await(); 624 } 625 626 @Override triggerShutdown()627 protected void triggerShutdown() { 628 latch.countDown(); 629 } 630 631 @Override serviceName()632 protected String serviceName() { 633 return this.getClass().getSimpleName() + "[" + index + "]"; 634 } 635 } 636 testNulls()637 public void testNulls() { 638 ServiceManager manager = new ServiceManager(Arrays.<Service>asList()); 639 new NullPointerTester() 640 .setDefault(ServiceManager.Listener.class, new RecordingListener()) 641 .testAllPublicInstanceMethods(manager); 642 } 643 644 private static final class RecordingListener extends ServiceManager.Listener { 645 volatile boolean healthyCalled; 646 volatile boolean stoppedCalled; 647 final Set<Service> failedServices = Sets.newConcurrentHashSet(); 648 649 @Override healthy()650 public void healthy() { 651 healthyCalled = true; 652 } 653 654 @Override stopped()655 public void stopped() { 656 stoppedCalled = true; 657 } 658 659 @Override failure(Service service)660 public void failure(Service service) { 661 failedServices.add(service); 662 } 663 } 664 isWindows()665 private static boolean isWindows() { 666 return OS_NAME.value().startsWith("Windows"); 667 } 668 isJava8()669 private static boolean isJava8() { 670 return JAVA_SPECIFICATION_VERSION.value().equals("1.8"); 671 } 672 } 673