There is a potential visibility race, which may be fine but you should be aware of.
The removalListener is called after the hash table has been updated and is not protected by any exclusive read or write locks. This means that between the hash table removal and the listener's write, the entry will not be present. A thread may observe this and whether that is benign depends on your usage. This will be seen when a reader accesses an expired entry and a not found response is retuned. While Guava will clean the entry up and call your listener, it already determined the result and will return the cache miss.
Guava's Cache wasn't designed for variable expiration or users to override its decision of what to evict. That was mostly because a cache is transient, volatile, recomputable data so it tended to not matter too much. There are workarounds, but they can be a little ugly due to that design expectation.
Cheers.