Shall I Make my REST Gateway a Library?

2019-09-20 18:16发布

问题:

Suppose there is a REST service with several clients. Each client has to do roughly the same to call the REST service: construct a URL, send a request, deserialize the response, map HTTP return codes to exceptions, etc.

This seems to be duplicate code so maybe it's a good idea to put the REST gateway code into a reusable library. In Java this could look like the following:

  • There is a jar file which depends on Jersey and contains the rest client code.
  • The rest client is a simple class or CDI bean.
  • All clients simply depend on this jar and call the members of the aforementioned class.

So no client has to worry about REST and can just call methods.

Is this a good idea?

(this question originates from this other question)

回答1:

This can get you into trouble with dependencies. It is a quite typical problem with dependencies and not bound to REST or Jersey. But let's have a look at the following scenario:

Dependencies

Suppose there are two servers (S1 and S2), two clients (C1 and C2) and two libraries containing the rest client code for accessing teh servers (L1 and L2). Both clients need to query both servers so the call structure looks like this:

C1 ---> L1 ---> S1
  \   ^
   \ /
    X
   / \
  /   v
C2 ---> L2 ---> S2

Furthermore both L1 and L2 depend on Jersey. Also the container (maybe you are running your application on a Glassfish or Wildfly application server) has a dependency on Jersey (or at least on the jax-rs API [1]).

The simplified dependency structure of C1 looks like this:

              C1
            /  | \
         /     |   \
       <       v     >
Container     L1       L2
   |           |        |
   v           v        v
Jersey      Jersey    Jersey

As long as all the three versions of Jersey are the same, everything is fine. No problem what so ever. But if the versions differ, you may run into nasty NoClassDefFoundErrors, MethodNotFoundErrors and the like [2].

Rigid Structures

If the versions are similar enough you won't run into these errors directly. Maybe it will work for quite a while. But then the following may happen:

  • You want to update your container. This updates the container version of Jersey which may get incompatible. ==> Boom. So using L1 and L2 prevents you from updating. You are stuck with the old version as long as L1 and L2 are not updated.
  • L2 is updated to a new version of Jersey. You can only use it if L1 and your container are also updated. So you stick to the old version (you can do that because of the loose coupling of REST). But then new functionality is added to S2 which is only usable with a new version of L2 and again you are stuck.

Keep in mind that the errors may or may not occur. There is no guarantee that you get into trouble and there is no guarantee that it will work. It's a risk, a ticking bomb.

On the other hand this is a simple example with just two services and two clients. When there are more, the risk is increasing, of course.

Stable Dependencies Principle

Dependencies should adhere to the Stable Dependencies Principle (SDP) which reads "Depend in the direction of stability." Uncle Bob defines it using the number afferent and efferent dependencies but SDP can be understood in a broader sense.

The connection to SDP becomes obvious when we look at the given dependency structure and replace Jersey with guava. This problem is really common with guava. Problem is that Guava is typically not downwards compatible. It's a rather unstable library that can be used in an application (which is unstable) but you should never use it in a reusable library (which ought to be stable).

With Jersey it's not that obvious because you may consider Jersey quite stable. In fact it is very stable with respect to Bob Martin's definition. But L2 may be quite stable, too. Maybe it's developed by another team, by some guy who left the company and nobody dares to touch it because last year somebody tried to do so which resulted in the C2 team having dependency issues. Maybe L2 is also stable because there is some ongoing quarrel between the managers of the teams developing C1 and L2 so the L2 manager claims there are no resources, no budget or whatever so updating L2 can only be done at the end of next year.

So depending on you code base, your organizational structure, etc. libraries can be more stable than you would which them to be.

Exceptions and Remedies

  • There are some languages which have "isolated dependencies", which means you can have multiple versions of the same library in the same application.
  • You could repackage Jersey (copy the source, change the package name and maintain it for yourself). Then you can have the normal Jersey in version X and your repackaged version of Jersey in version Y. This works but of course this has its own problems. It's much work and you have to maintain software which is not written by you.
  • The problem can be negligible if everything is developed in the same team. Then you have control over the whole thing and you don't depend on other teams doing something. Nevertheless as soon as your code base grows, updating to a new version can be much work as you cannot do it gradually anymore but you have to move many services to a new version at once.
  • Build tools like maven give you some control over the version you depend on. You may consider marking the Jersey dependency optional in L1 and L2 and provided in C1 and C2. Then it is not included in your war file so there is just one version of Jersey (the one in the container). As long as this version is interface compatible, it will work [3].

What to Do Instead

I would recommend to just put your representation classes, i.e. your DTOs, into a client jar and let each client decide on which library to use for making the REST call. Typically those REST gateways are quite simple and sharing them between applications is normally not worth the risk. At least think about the issues mentioned here before running into them without even knowing.

[1] In order to keep the explanation simple, lets neglect the difference between Jersey and the jax-rs API. The argument is the same.

[2] In reality it's a bit more complicated. Typically your build tool (maven, etc.) will select one version of Jersey and put it into the war file. The other version of Jersey is "omitted for conflict" with the other version. This is OK as long as the versions are compatible. So you end up with two versions the one in your container and the one in your war file. Whereas interface compatibility in the former case should be enough, it won't be enough for these remaining two versions.

[3] As there is only one version, the problem mentioned in [2] cannot occur. The problem with interface compatibility stays of course.