Java language has a great advantages over other languages such as reflection and annotations. All modern frameworks such as Spring, Hibernate, myBatis and etc. seek to use this advantages to the maximum. The idea of introduction annotations in Hystrix is obvious solution for improvement. Currently using Hystrix involves writing a lot of code that is a barrier to rapid development. You likely be spending a lot of time on writing a Hystrix commands. Idea of the Javanica project is make easier using of Hystrix by the introduction of support annotations.
First of all in order to use hystrix-javanica you need to add hystrix-javanica dependency in your project.
Example for Maven:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>x.y.z</version>
</dependency>
To implement AOP functionality in the project was used AspectJ library. If in your project already used AspectJ then you need to add hystrix aspect in aop.xml as below:
<aspects>
...
<aspect name="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"/>
...
</aspects>
More about AspectJ configuration read [here] (http://www.eclipse.org/aspectj/doc/next/devguide/ltw-configuration.html)
If you use Spring AOP in your project then you need to add specific configuration using Spring AOP namespace in order to make Spring capable to manage aspects which were written using AspectJ and declare HystrixCommandAspect
as Spring bean like below:
<aop:aspectj-autoproxy/>
<bean id="hystrixAspect" class="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"></bean>
Or if you are using Spring code configuration:
@Configuration
public class HystrixConfiguration {
@Bean
public HystrixCommandAspect hystrixAspect() {
return new HystrixCommandAspect();
}
}
It doesn't matter which approach you use to create proxies in Spring, javanica works fine with JDK and CGLIB proxies. If you use another framework for aop which supports AspectJ and uses other libs (Javassist for instance) to create proxies then let us know what lib you use to create proxies and we'll try to add support for this library in near future.
More about Spring AOP + AspectJ read [here] (http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html)
Javanica supports two weaving modes: compile and runtime. Load time weaving hasn't been tested but it should work. If you tried LTW mode and got any problems then raise javanica issue or create pull request with fix.
- CTW. To use CTW mode you need to use specific jar version: hystrix-javanica-ctw-X.Y.Z . This jar is assembled with aspects compiled with using AJC compiler. If you will try to use regular hystrix-javanica-X.Y.Z with CTW then you get
NoSuchMethodError aspectOf()
at runtime from building with iajc. Also, you need to start your app with using java property:-DWeavingMode=compile
. NOTE: Javanica depends on aspectj library and uses internal features of aspectj and these features aren't provided as a part of open API thus it can change from version to version. Javanica tested with latest aspectj version 1.8.7. If you updated aspectj version and noticed any issues then please don't hestitate to create new issue or contribute. - RTW works, you can use regular hystrix-javanica-X.Y.Z
- LTM hasn't been tested but it should work fine.
To run method as Hystrix command synchronously you need to annotate method with @HystrixCommand
annotation, for example
public class UserService {
...
@HystrixCommand
public User getUserById(String id) {
return userResource.getUserById(id);
}
}
...
In example above the getUserById
method will be processed synchronously within new Hystrix command.
By default the name of command key is command method name: getUserById
, default group key name is class name of annotated method: UserService
. You can change it using necessary @HystrixCommand
properties:
@HystrixCommand(groupKey="UserGroup", commandKey = "GetUserByIdCommand")
public User getUserById(String id) {
return userResource.getUserById(id);
}
To set threadPoolKey use @HystrixCommand#threadPoolKey()
To process Hystrix command asynchronously you should return an instance of AsyncResult
in your command method as in the exapmple below:
@HystrixCommand
public Future<User> getUserByIdAsync(final String id) {
return new AsyncResult<User>() {
@Override
public User invoke() {
return userResource.getUserById(id);
}
};
}
The return type of command method should be Future that indicates that a command should be executed [asynchronously] (https://github.com/Netflix/Hystrix/wiki/How-To-Use#wiki-Asynchronous-Execution).
To performe "Reactive Execution" you should return an instance of Observable
in your command method as in the exapmple below:
@HystrixCommand
public Observable<User> getUserById(final String id) {
return Observable.create(new Observable.OnSubscribe<User>() {
@Override
public void call(Subscriber<? super User> observer) {
try {
if (!observer.isUnsubscribed()) {
observer.onNext(new User(id, name + id));
observer.onCompleted();
}
} catch (Exception e) {
observer.onError(e);
}
}
});
}
The return type of command method should be Observable
.
HystrixObservable interface provides two methods: observe()
- eagerly starts execution of the command the same as HystrixCommand#queue()
and HystrixCommand#execute()
; toObservable()
- lazily starts execution of the command only once the Observable is subscribed to. To control this behaviour and swith between two modes @HystrixCommand
provides specific parameter called observableExecutionMode
.
@HystrixCommand(observableExecutionMode = EAGER)
indicates that observe()
method should be used to execute observable command
@HystrixCommand(observableExecutionMode = LAZY)
indicates that toObservable()
should be used to execute observable command
NOTE: EAGER mode is used by default
Graceful degradation can be achieved by declaring name of fallback method in @HystrixCommand
like below:
@HystrixCommand(fallbackMethod = "defaultUser")
public User getUserById(String id) {
return userResource.getUserById(id);
}
private User defaultUser(String id) {
return new User("def", "def");
}
Its important to remember that Hystrix command and fallback should be placed in the same class and have same method signature (optional parameter for failed execution exception).
Fallback method can have any access modifier. Method defaultUser
will be used to process fallback logic in a case of any errors. If you need to run fallback method defaultUser
as separate Hystrix command then you need to annotate it with HystrixCommand
annotation as below:
@HystrixCommand(fallbackMethod = "defaultUser")
public User getUserById(String id) {
return userResource.getUserById(id);
}
@HystrixCommand
private User defaultUser(String id) {
return new User();
}
If fallback method was marked with @HystrixCommand
then this fallback method (defaultUser) also can has own fallback method, as in the example below:
@HystrixCommand(fallbackMethod = "defaultUser")
public User getUserById(String id) {
return userResource.getUserById(id);
}
@HystrixCommand(fallbackMethod = "defaultUserSecond")
private User defaultUser(String id) {
return new User();
}
@HystrixCommand
private User defaultUserSecond(String id) {
return new User("def", "def");
}
Javanica provides an ability to get execution exception (exception thrown that caused the failure of a command) within a fallback is being executed. A fallback method signature can be extended with an additional parameter in order to get an exception thrown by a command. Javanica exposes execution exception through additional parameter of fallback method. Execution exception is derived by calling method getFailedExecutionException() as in vanilla hystrix.
Example:
@HystrixCommand(fallbackMethod = "fallback1")
User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
@HystrixCommand(fallbackMethod = "fallback2")
User fallback1(String id, Throwable e) {
assert "getUserById command failed".equals(e.getMessage());
throw new RuntimeException("fallback1 failed");
}
@HystrixCommand(fallbackMethod = "fallback3")
User fallback2(String id) {
throw new RuntimeException("fallback2 failed");
}
@HystrixCommand(fallbackMethod = "staticFallback")
User fallback3(String id, Throwable e) {
assert "fallback2 failed".equals(e.getMessage());
throw new RuntimeException("fallback3 failed");
}
User staticFallback(String id, Throwable e) {
assert "fallback3 failed".equals(e.getMessage());
return new User("def", "def");
}
// test
@Test
public void test() {
assertEquals("def", getUserById("1").getName());
}
As you can see, the additional Throwable
parameter is not mandatory and can be omitted or specified.
A fallback gets an exception thrown that caused a failure of parent, thus the fallback3
gets exception thrown by fallback2
, no by getUserById
command.
A fallback can be async or sync, at certain cases it depends on command execution type, below listed all possible uses :
Supported
case 1: sync command, sync fallback
@HystrixCommand(fallbackMethod = "fallback")
User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
@HystrixCommand
User fallback(String id) {
return new User("def", "def");
}
case 2: async command, sync fallback
@HystrixCommand(fallbackMethod = "fallback")
Future<User> getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
@HystrixCommand
User fallback(String id) {
return new User("def", "def");
}
case 3: async command, async fallback
@HystrixCommand(fallbackMethod = "fallbackAsync")
Future<User> getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
@HystrixCommand
Future<User> fallbackAsync(String id) {
return new AsyncResult<User>() {
@Override
public User invoke() {
return new User("def", "def");
}
};
}
Unsupported(prohibited)
case 1: sync command, async fallback command. This case isn't supported because in the essence a caller does not get a future buy calling getUserById
and future is provided by fallback isn't available for a caller anyway, thus execution of a command forces to complete fallbackAsync
before a caller gets a result, having said it turns out there is no benefits of async fallback execution. But it can be convenient if a fallback is used for both sync and async commands, if you see this case is very helpful and will be nice to have then create issue to add support for this case.
@HystrixCommand(fallbackMethod = "fallbackAsync")
User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
@HystrixCommand
Future<User> fallbackAsync(String id) {
return new AsyncResult<User>() {
@Override
public User invoke() {
return new User("def", "def");
}
};
}
case 2: sync command, async fallback. This case isn't supported for the same reason as for the case 1.
@HystrixCommand(fallbackMethod = "fallbackAsync")
User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
Future<User> fallbackAsync(String id) {
return new AsyncResult<User>() {
@Override
public User invoke() {
return new User("def", "def");
}
};
}
Same restrictions are imposed on using observable feature in javanica.
Based on this description, @HystrixCommand
has an ability to specify exceptions types which should be ignored.
@HystrixCommand(ignoreExceptions = {BadRequestException.class})
public User getUserById(String id) {
return userResource.getUserById(id);
}
If userResource.getUserById(id);
throws an exception which type is BadRequestException then this exception will be thrown without triggering fallback logic.
Javanica provides specific annotations in order to enable and manage request caching. This annotations look very similar to JSR107 but less extensive than those, by other hand Hystrix doesn't provide independent and complex caching system therefore there is no need to have such diversity of annotations as in JSR107. Javanica has only three annotations dedicated for request caching.
Annotation | Description | Properties |
---|---|---|
@CacheResult | Marks a methods that results should be cached for a Hystrix command.This annotation must be used in conjunction with HystrixCommand annotation. | cacheKeyMethod |
@CacheRemove | Marks methods used to invalidate cache of a command. Generated cache key must be same as key generated within link CacheResult context | commandKey, cacheKeyMethod |
@CacheKey | Marks a method argument as part of the cache key. If no arguments are marked all arguments are used. If @CacheResult or @CacheRemove annotation has specified cacheKeyMethod then a method arguments will not be used to build cache key even if they annotated with @CacheKey | value |
cacheKeyMethod - a method name to be used to get a key for request caching. The command and cache key method should be placed in the same class and have same method signature except cache key method return type that should be String. cacheKeyMethod has higher priority than an arguments of a method, that means what actual arguments of a method that annotated with @CacheResult will not be used to generate cache key, instead specified cacheKeyMethod fully assigns to itself responsibility for cache key generation. By default this returns empty string which means "do not use cache method. You can consider cacheKeyMethod as a replacement for common key generators (for example JSR170-CacheKeyGenerator) but with cacheKeyMethod cache key generation becomes more convenient and simple. Not to be unfounded let's compare the two approaches: JSR107
@CacheRemove(cacheName = "getUserById", cacheKeyGenerator = UserCacheKeyGenerator.class)
@HystrixCommand
public void update(@CacheKey User user) {
storage.put(user.getId(), user);
}
public static class UserCacheKeyGenerator implements HystrixCacheKeyGenerator {
@Override
public HystrixGeneratedCacheKey generateCacheKey(CacheKeyInvocationContext<? extends Annotation> cacheKeyInvocationContext) {
CacheInvocationParameter cacheInvocationParameter = cacheKeyInvocationContext.getKeyParameters()[0];
User user = (User) cacheInvocationParameter.getValue();
return new DefaultHystrixGeneratedCacheKey(user.getId());
}
}
Javanica cacheKeyMethod
@CacheRemove(commandKey = "getUserById", cacheKeyMethod=)
@HystrixCommand
public void update(User user) {
storage.put(user.getId(), user);
}
private String cacheKeyMethod(User user) {
return user.getId();
}
or even just
@CacheRemove(commandKey = "getUserById")
@HystrixCommand
public void update(@CacheKey("id") User user) {
storage.put(user.getId(), user);
}
You don't need to create new classes, also approach with cacheKeyMethod helps during refactoring if you will give correct names for cache key methods. It is recommended to append prefix "cacheKeyMethod" to the real method name, for example:
public User getUserById(@CacheKey String id);
private User getUserByIdCacheKeyMethod(String id);
Cache key generator
Jacanica has only one cache key generator HystrixCacheKeyGenerator that generates a HystrixGeneratedCacheKey based on CacheInvocationContext. Implementation is thread-safe. Parameters of an annotated method are selected by the following rules:
- If no parameters are annotated with @CacheKey then all parameters are included
- If one or more @CacheKey annotations exist only those parameters with the @CacheKey annotation are included
Note: If CacheResult or CacheRemove annotation has specified cacheKeyMethod then a method arguments will not be used to build cache key even if they annotated with CacheKey.
@CacheKey and value property
This annotation has one property by default that allows specify name of a certain argument property. for example: @CacheKey("id") User user
, or in case composite property: @CacheKey("profile.name") User user
. Null properties are ignored, i.e. if profile
is null
then result of @CacheKey("profile.name") User user
will be empty string.
Examples:
@CacheResult
@HystrixCommand
public User getUserById(@CacheKey String id) {
return storage.get(id);
}
// --------------------------------------------------
@CacheResult(cacheKeyMethod = "getUserByNameCacheKey")
@HystrixCommand
public User getUserByName(String name) {
return storage.getByName(name);
}
private Long getUserByNameCacheKey(String name) {
return name;
}
// --------------------------------------------------
@CacheResult
@HystrixCommand
public void getUserByProfileName(@CacheKey("profile.email") User user) {
storage.getUserByProfileName(user.getProfile().getName());
}
Get-Set-Get pattern To get more about this pattern you can read this chapter Example:
public class UserService {
@CacheResult
@HystrixCommand
public User getUserById(@CacheKey String id) { // GET
return storage.get(id);
}
@CacheRemove(commandKey = "getUserById")
@HystrixCommand
public void update(@CacheKey("id") User user) { // SET
storage.put(user.getId(), user);
}
}
// test app
public void test(){
User user = userService.getUserById("1");
HystrixInvokableInfo<?> getUserByIdCommand = getLastExecutedCommand();
// this is the first time we've executed this command with
// the value of "1" so it should not be from cache
assertFalse(getUserByIdCommand.isResponseFromCache());
user = userService.getUserById("1");
getUserByIdCommand = getLastExecutedCommand();
// this is the second time we've executed this command with
// the same value so it should return from cache
assertTrue(getUserByIdCommand.isResponseFromCache());
user = new User("1", "new_name");
userService.update(user); // update the user
user = userService.getUserById("1");
getUserByIdCommand = getLastExecutedCommand();
// this is the first time we've executed this command after "update"
// method was invoked and a cache for "getUserById" command was flushed
// so the response shouldn't be from cache
assertFalse(getUserByIdCommand.isResponseFromCache());
}
Note: You can use @CacheRemove annotation in conjunction with @HystrixCommand or without. If you want annotate not command method with @CacheRemove annotation then you need to add HystrixCacheAspect aspect to your configuration:
<aspects>
...
<aspect name="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCacheAspect"/>
...
</aspects>
<!-- or Spring conf -->
<aop:aspectj-autoproxy/>
<bean id="hystrixAspect" class="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCacheAspect"></bean>
Command properties can be set using @HystrixCommand's 'commandProperties' like below:
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
})
public User getUserById(String id) {
return userResource.getUserById(id);
}
Javanica dynamically sets properties using Hystrix ConfigurationManager. For the example above Javanica behind the scenes performs next action:
ConfigurationManager.getConfigInstance().setProperty("hystrix.command.getUserById.execution.isolation.thread.timeoutInMilliseconds", "500");
More about Hystrix command properties command and fallback
ThreadPoolProperties can be set using @HystrixCommand's 'threadPoolProperties' like below:
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "101"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
})
public User getUserById(String id) {
return userResource.getUserById(id);
}
Suppose you have some command which calls should be collapsed in one backend call. For this goal you can use @HystrixCollapser
annotation.
Example:
@HystrixCollapser(batchMethod = "getUserByIds")
public Future<User> getUserById(String id) {
return null;
}
@HystrixCommand
public List<User> getUserByIds(List<String> ids) {
List<User> users = new ArrayList<User>();
for (String id : ids) {
users.add(new User(id, "name: " + id));
}
return users;
}
Future<User> f1 = userService.getUserById("1");
Future<User> f2 = userService.getUserById("2");
Future<User> f3 = userService.getUserById("3");
Future<User> f4 = userService.getUserById("4");
Future<User> f5 = userService.getUserById("5");
A method annotated with @HystrixCollapser
annotation can return any value with compatible type, it does not affect the result of collapser execution, collapser method can even return null
or another stub.
There are several rules applied for methods signatures.
- Collapser method must have one argument of any type, desired a wrapper of a primitive type like Integer, Long, String and etc.
- A batch method must have one argument with type java.util.List parameterized with corresponding type, that's if a type of collapser argument is
Integer
then type of batch method argument must beList<Integer>
. - Return type of batch method must be java.util.List parameterized with corresponding type, that's if a return type of collapser method is
User
then a return type of batch command must beList<User>
.
Convention for batch method behavior
The size of response collection must be equal to the size of request collection.
@HystrixCommand
public List<User> getUserByIds(List<String> ids); // batch method
List<String> ids = List("1", "2", "3");
getUserByIds(ids).size() == ids.size();
Order of elements in response collection must be same as in request collection.
@HystrixCommand
public List<User> getUserByIds(List<String> ids); // batch method
List<String> ids = List("1", "2", "3");
List<User> users = getUserByIds(ids);
System.out.println(users);
// output
User: id=1
User: id=2
User: id=3
Why order of elements of request and response collections is important?
The reason of this is in reducing logic, basically request elements are mapped one-to-one to response elements. Thus if order of elements of request collection is different then the result of execution can be unpredictable.
Deduplication batch command request parameters.
In some cases your batch method can depend on behavior of third-party service or library that skips duplicates in a request. It can be a rest service that expects unique values and ignores duplicates. In this case the size of elements in request collection can be different from size of elements in response collection. It violates one of the behavior principle. To fix it you need manually map request to response, for example:
// hava 8
@HystrixCommand
List<User> batchMethod(List<String> ids){
// ids = [1, 2, 2, 3]
List<User> users = restClient.getUsersByIds(ids);
// users = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
List<User> response = ids.stream().map(it -> users.stream()
.filter(u -> u.getId().equals(it)).findFirst().get())
.collect(Collectors.toList());
// response = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
return response;
Same case if you want to remove duplicate elements from request collection before a service call. Example:
// hava 8
@HystrixCommand
List<User> batchMethod(List<String> ids){
// ids = [1, 2, 2, 3]
List<String> uniqueIds = ids.stream().distinct().collect(Collectors.toList());
// uniqueIds = [1, 2, 3]
List<User> users = restClient.getUsersByIds(uniqueIds);
// users = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
List<User> response = ids.stream().map(it -> users.stream()
.filter(u -> u.getId().equals(it)).findFirst().get())
.collect(Collectors.toList());
// response = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
return response;
To set collapser properties use @HystrixCollapser#collapserProperties
Read more about Hystrix request collapsing [here] (https://github.com/Netflix/Hystrix/wiki/How-it-Works#wiki-RequestCollapsing)
Collapser error processing Bath command can have a fallback method. Example:
@HystrixCollapser(batchMethod = "getUserByIdsWithFallback")
public Future<User> getUserByIdWithFallback(String id) {
return null;
}
@HystrixCommand(fallbackMethod = "getUserByIdsFallback")
public List<User> getUserByIdsWithFallback(List<String> ids) {
throw new RuntimeException("not found");
}
@HystrixCommand
private List<User> getUserByIdsFallback(List<String> ids) {
List<User> users = new ArrayList<User>();
for (String id : ids) {
users.add(new User(id, "name: " + id));
}
return users;
}
#Development Status and Future Please create an issue if you need a feature or you detected some bugs. Thanks
Note: Javanica 1.4.+ is updated more frequently than 1.3.+ hence 1.4+ is more stable.
It's recommended to use Javaniva 1.4.+