JNI is IMHO complicated for two reasons:
1) a JNI library can be reused in binary form with whatever available JVM. That means that every access to an object must happen through a call to the JNI support library.
2) unlike, say, Python, you have a garbage collector instead of refcounting. Thus you need to use a different kind of API. I'm not sure if one is more complicated than another, but it is anyway a difference.
3) The unreliability of finalizers can easily cause leaks of native resources. Interestingly, .NET 1.0 got it even more wrong, by giving finalizers the familiar syntax of C++ destructor while they're a completely different beast (as well argued by Hans Boehm).