I remember the days, when the Spring framework was advertised as a lightweight alternative to Enterprise java beans (ejb); now Spring outgrew the pretence of being lightweight, don't know when that happened. A year and a half ago, i got back to work with java and spring boot, and i was overwhelmed by the prevalence of annotations in spring boot.
To cope with all this, i wrote this little project: https://github.com/MoserMichael/ls-annotations
It's a decompiler that is listing all annotations, so it becomes easier to grep a text file in order to detect the dependencies between annotations.
it is using the asm library https://asm.ow2.io/ to inspect the bytecode files, so as to extract the class signature, along with the reference and declaration of annotations included in a classpath, or class files included within a directory structure. A limitation/feature is, that it is inspecting already compiled bytecode files.
Indeed. I have a dozen or so microservices supported by team. Most are SpringBoot a couple of them I wrote myself with plain java and embedded tomcat. Needless to say Springboot stuff is rather complicated for such a simple business functionality. Errors are indecipherable being swamped by thousand line framework exception trace. But being an "enterprise standard" framework all projects must be move to this turd of a framework.
And in 99% of cases that little microservice will suddenly need thread pooling, logging, some more advanced db management or God help some random messaging service and you are back to re-implementing the myriad features of spring in a shittier way.
It is not an accident that things like ruby on rails are popular. These are well-tested toolboxes with a solution for almost every conceivable problem. There are exceptions where it is not needed, but for business applications those are not numerous.
I don't think people have any issues with the fact that spring is batteries included. It seems to me (and this is my personal experience too) that the large amounts of abstraction and indirection through annotations makes the code very hard to parse. It makes it hard to create a mental graph of how it all works together
I found the real challenge to be that it's very difficult - if not impossible - to determine how spring functions simply by reading code and using it. In simpler libraries or frameworks, I normally just read the source to understand how I should be using it. With Spring, I've had to spend a lot of time reading and re-reaading docs to understand what's going on.
I think that this is sometimes a hard shift for developers who otherwise have spent their lives with an ability to puzzle out the constructs that they come across.
I absolutely understand it, but I think the correct, although bit inconvenient approach is the one you mentioned — properly learning the framework either through docs or other materials.
Way too many developers try to write spring (but also jpa and many other useful, but complex tool) by trial and error, which let’s be honest, not a good tactic even if one can easily inspect the source. (The recently posted microsoft blog post “even if the precondition doesn’t do anything, you still have to call it” comes to mind)
You're, of course, right that you just need to study and learn a complex framework. Basically the same way that you learn a new programming language- they're all different and have different behaviors and idioms (pass-by-copy vs. pass-by-reference, etc).
However, there's another dimension here, and while it's not totally unique to Java, it's definitely present in larger magnitude in Java, in my experience. There are two parts:
1. None of these frameworks are 100% consistent. I haven't used Spring{,Boot} in years, but I can tell you that JPA/JDBC are full of little "surprises" and rough edges, like handling nullable database columns. If you are not careful with your annotations, you'll get a `0` value for your `Int` object field instead of the `null` that was in the database. You can then go for quite a while before you figure out that's what happened. Similarly, JacksonXML has all kinds of little gotchas when it comes to date-time types and timezones, primitives and null-ness, etc.
2. Most projects have more than one of these complex frameworks. See above. I listed JacksonXML and JPA/JDBC. Odds are that you have AT LEAST these three frameworks (including Spring) in your Java project, which means you have to study all three and learn all of their intricacies before being confident in the code you write. That's on top of learning how to write half-decent Java, which is hard enough with its type-erased generics, bug-prone null-handling, and very verbose class definition syntax. If it were just one thing, I'd be sympathetic and tell people to just RTFM. But, unless you plan on only writing Java code for the next decade+, I have come to believe that it's probably not worth it.
I don’t think that it is any easier in other languages either — object serialization, conversion between language’s object/json/xml/etc, and database access with object relational mapping is just complex. You can make the trivial way trivial, but you have to expose the hard ways as well and that will not be pretty either way. For what it worth, java has a really high quality ecosystem for all these things - I would be really interested in what you think as an alternative. Sure, there are alternatives as well within the jvm ecosystem (though with much smaller userbases), but .NET is famous for having worse copies of java libs, node.js is in my opinion terrible for enterprise scale applications, there is erlang, ruby, php and whatnot, and sure enough you can make good web applications in any of them, but I really don’t buy that any of them would be easier.
Also, java is solid as a language. Sure, it is not the most modern one, but it is reasonably productive, has great tooling, is very performant and perhaps most importantly, it is observable in a very fine way.
> I don’t think that it is any easier in other languages either — object serialization, conversion between language’s object/json/xml/etc, and database access with object relational mapping is just complex. You can make the trivial way trivial, but you have to expose the hard ways as well and that will not be pretty either way.
I agree that these things are just complex. But, what's interesting to me is that I fully agree with your second sentence and I see it as an indictment against the Java ecosystem around these operations. The common Java frameworks, IMO, serve to make the trivial stuff even more trivial, but then make the complex stuff even more complex. It's exactly the opposite of what I want.
Let's look at JacksonXML for serialization.
Here we have a framework that uses runtime reflection to more-or-less guess how to (de)serialize an object. Figuring it out at runtime is a tough engineering choice already, because it pretty much immediately means that you're going to have to figure out a caching system for what types you've already analyzed, so that performance isn't terrible. And we all know how hard cache is.
But, on top of that, Java uses type-erased generics, so you can't actually reliable use runtime reflection to figure it out! But the compiler certainly won't tell you it's a problem, because Jackson will try to (de)serialize anything you throw at it. You don't even need any annotations for most stuff. It "just works" (TM)... until it doesn't.
So, if you use generics or inheritance or any non-trivial mapping, you have to write a custom serializer. Okay, that's no big deal.
But then you realize that JacksonXML will IGNORE time zone information on an incoming serialized date field that is encoded as an ISO8601 string and just use the current JVM system time zone. Because why the fuck not, I guess?
So, in other words, Jackson makes already-trivial things a little less verbose, it makes non-trivial things a pain, and it even makes some things that should be trivial into a pain.
I can play the same game with JPA/JDBC. In particular, it also does really stupid things with time zones and date-time types. It also can't really handle complex types for similar reasons to JacksonXML.
> For what it worth, java has a really high quality ecosystem for all these things - I would be really interested in what you think as an alternative.
My favorite languages to work with at the moment are Rust, Swift, and Kotlin. All three have way better serialization stories than Java. Rust has serde, which is like JacksonXML, except it's compile-time and your types have to implement a Serialize "trait" (interface). I truly think I'm being honest when I say that I've NEVER had a runtime serialization (type) error in my Rust projects that have used Serde. If it compiles, it works. The same is almost true of Swift and Kotlin (with kotlinx.serialization), except I do think I've encountered some runtime issues in both of those (IIRC, there was a surprising limitation with Swift Dictionary keys needing to be Strings or something). Kotlin's approach is my least favorite of the three.
When it comes to ORM/SQL stuff, I've been using a query builder in Rust that's a delight to use. Basically, when you execute a query, you use the type system to indicate the type you expect the returned rows to be. As long as the type implements a `FromRow` trait, it will "just work" or return an error value or crash (you can choose to call a "safe", error-returning, function or a "crashy" version of the function that assumes success and crashes otherwise). Rust has ad-hoc tuple types, and the library implements its `FromRow` trait for all standard types as well as all tuples up to 13 or so elements, so often you can just write something like `let (id, name) = query<(u32, String)>.execute();` and it will just work. If the `id` column is `null`, then the call will FAIL instead of doing something insane like just making up data (like JDBC returning `0` instead of `null` for nullable int columns).
The Rust query library I'm using is the perfect example for the point you made earlier about making trivial things trivial. This library does require a little bit of boilerplate to implement `FromRow` for your custom object types. It's not really worse than JPA for the simple cases, but it's WAY ahead when it comes to dealing with the non-trivial cases.
> Also, java is solid as a language. Sure, it is not the most modern one, but it is reasonably productive, has great tooling, is very performant and perhaps most importantly, it is observable in a very fine way.
Credit where it's due: Java does have phenomenal tooling and it's very fast (except when you use the frameworks that we're discussing...). I think I'd lump in the observability with the phenomenal tooling.
But, no, I wouldn't call it a solid language. It's far too primitive and bug-prone for writing robust applications. The null issue doesn't really need to explained, the ease with which we can leak resources from Closeable things, the awkwardness of the type system (e.g., being unable to implement an interface for types you don't own, being extremely tedious to define "newtypes" like a `NonEmptyString`, etc), the incompatible-yet-ubiquitous use of runtime reflection and type-erased generics, etc.
I'm sure you're a Java expert, but I'd wonder how many years you think it took you to get to the point where you feel like you aren't bitten by all of the things I've described in this comment. If the answer is more than 1, then I'll go ahead and assert that Java is not a good programming tool for the domain in which you work. I've been working with JVM languages for about 5 years now, and I'll say that I'm now familiar enough to avoid these traps most of the time, but holy shit- it should not have taken nearly that long.
Thanks for the non-flame-baity answer! Hopefully I wasn’t too emotional in my previous reply, because it unfortunately does happen from time to time.
Regarding Jackson and JPA the only thing I can tell about these is that their age shows, and they come from a domain and age where the (in my opinion, bad) POJO and Java Beans conventions originate. So I fully agree that things could be much better, and hopefully there will come a renaissance replacing these tools with modern java equivalents, that don’t rely on runtime magic as much, and will use the modern datetime APIs by default, etc. Serialization is especially in need of a huge revamp, hopefully records will make it much better.
Regarding ORMs, have you by chance tried JOOQ? You may prefer it over JPA.
Also, just a small note on Rust - I find it to be an excellent language, but I really don’t think it fights in the same domain as Java. Systems programming is fundamentally different. So writing a huge business application in Rust (or in C++, equivalently) is a suicide mission in my opinion — initial write time may indeed be low for an experienced Rust dev, but with the often changing client requirements that mandate quick changes touching everything, the low level details that leak into the high level view of the app will slow one down (now you also have to change the memory model because this lifetime has to be extended, etc). But I only mention that as an explanation for why Rust is not a replacement for the huge, ever-living business app domain (at least for me)
So all in all, I think that Java fights a good fight, it remains religiously backwards compatible which is painful at times, but is perhaps the biggest value there is; but it is improving with a huge pace with records, sealed classes (giving us algebraic data types), upcoming `with`ers will provide a good syntactic sugar for “modifying” immutable objects, and full-blown pattern matching is coming built on top of these. But the real deal happens under the hood, Loom will make blocking codes magically non-blocking and Valhalla tries to heal the rift between primitives and objects and will provide excellent performance boost. So the part I actually like and defend about Java is this one, not the historic baggage it comes with. But I also work on CRUD apps, and that’s not an exciting domain no matter what.
> Thanks for the non-flame-baity answer! Hopefully I wasn’t too emotional in my previous reply, because it unfortunately does happen from time to time.
I didn't pick up any high emotions, but I get it. For some reason, I get fiery about this stuff, too. I don't know if it's that I get equally worked up no matter what I'm arguing about, or if it's worse because I'm passionate about computers and programming.
> Regarding ORMs, have you by chance tried JOOQ? You may prefer it over JPA.
I haven't used it, but I've read their docs and API. It looks great, albeit very large. It also preserves some of the... conventions... from JDBC and JPA that I find egregious, like converting null to actual values when mapping query results (https://www.jooq.org/doc/3.15/manual/sql-execution/fetching/...). At this point I have to assume that Java devs actually prefer this behavior, but I think it's crazy- if I expected a non-null int and I read out a null, I want to crash- not pretend like I got a valid int...
> Regarding Jackson and JPA the only thing I can tell about these is that their age shows, and they come from a domain and age where the (in my opinion, bad) POJO and Java Beans conventions originate. So I fully agree that things could be much better, and hopefully there will come a renaissance replacing these tools with modern java equivalents, that don’t rely on runtime magic as much, and will use the modern datetime APIs by default, etc. Serialization is especially in need of a huge revamp, hopefully records will make it much better.
> Also, just a small note on Rust - I find it to be an excellent language, but I really don’t think it fights in the same domain as Java. Systems programming is fundamentally different. So writing a huge business application in Rust (or in C++, equivalently) is a suicide mission in my opinion — initial write time may indeed be low for an experienced Rust dev, but with the often changing client requirements that mandate quick changes touching everything, the low level details that leak into the high level view of the app will slow one down (now you also have to change the memory model because this lifetime has to be extended, etc). But I only mention that as an explanation for why Rust is not a replacement for the huge, ever-living business app domain (at least for me)
At the risk of coming off as a combative asshole, I'm going to pick on you for second because it's relevant to the part about Rust.
In your previous comment, you asserted that serialization and ORM/data-access is just as difficult in every other language as it is in Java. You also asserted that Java has a "really high quality ecosystem for all these things".
But, here you're acknowledging that serialization and data mapping are "showing their age", follow "bad" conventions, "could be much better", shouldn't rely on runtime magic, and are in need of a "renaissance".
Even though I wasn't advocating for Rust as a great fit for enterprise web apps, I will say this: I think you're rationalizing. You've already made up your mind that Java is good for application development and has a good ecosystem. But I think I can make a solid case that Java is a primitive, unexpressive, bug-prone, language with a large-but-mediocre ecosystem (where the most widely used parts need a "renaissance").
Even after I argued (convincingly, I assume, since you changed your expressed opinion about serialization and data-mapping in Java) that Rust is better at both serialization and data-mapping, you're asserting that Rust would be a suicide mission for a large scale business app. Well, it's apparently better at two of the most fundamental parts of any web app, so I think it's looking pretty good as far as suicide missions go.
Hell- Java doesn't even have single-thread concurrency!
Rust's type system allows us to express more elaborate abstractions with less code than Java (enums vs sealed classes, type classes vs adapters and decorators)
Rust makes concurrent code safe. No need to remember to use mutexes or to make inefficient copies/clones of "immutable" classes- if you write code that would cause a data race, it just won't compile. In Java, you'll just get bugs and corrupt data.
In Rust we'll never get NPEs.
So, I don't agree with your assessment at all. Writing a large enterprisey business app in Rust will likely run faster, have fewer bugs, use less memory, and even scale out better. If you're hosting your app on a cloud provider, it'll cost you less money to operate as well.
I think that you just want to believe that Rust would be worse than Java, and I think the cargo cult agrees with you. But, having done significant work in both languages, I think that's incorrect. My Rust code is generally an order of magnitude less maintenance than my Java and Kotlin code have been. Furthermore, it took me LESS time to become truly proficient at Rust than it has to become proficient (as in writing relatively few bugs the first time) in Java and Kotlin.
Some day, Valhalla will land and Java will get some things that other languages have had for a while. And eventually, Java may even become a solid language for its major use-case. But today is not that day. And today we have languages that are already better. Literally everything you listed (sum types, records, pattern matching, and non-blocking) already exists in Rust and Swift (and Scala, and Kotlin).
So, I don't agree with your assessment at all. Writing a large enterprisey business app in Rust will likely run faster, have fewer bugs, use less memory, and even scale out better.
That's true but which is more flexible for the "ever changing living business app domain" the GP is alluding to? You seem to keep ignoring this part, flexibility matters. In rust is easy to code yourself into a corner and spent lots of time rewriting stuff over and over.
Fair. You're right that I didn't address that concern.
I guess the problem is that I don't know what we mean by "flexible". The GP did mention lifetimes around the same part of their comment, so I assume that there's some concern about business requirements changing in some way, and that Rust's lifetimes would get in the way of adapting to code to meet the new requirement.
Is this also what you mean by "code yourself into a corner"? Or are you thinking of a superset of that?
When we say "flexible" are we talking about the language being opinionated about the style of code we write or are we talking about the language making it harder to be agile in the face of requirement changes? It sounds like we're talking about the latter.
First, let me repeat myself that I don't believe Rust is the ideal enterprisey web app language. There's almost no reason that a web-app benefits from a language not having garbage collection or automatic ref-counting (like Swift).
But, I'm not backing down from my assertion that Rust is still probably a better web app language than Java, if we're willing to ignore Java's ecosystem's 30 year and billions of dollar head-start for niche, vendor-specific, libraries. Or, phrased another way, just because FooCorp gave you a jar file to connect to their smart sex swing, that doesn't make Java a better language in a fundamental sense, even if it does force your hand from a business and engineering perspective.
So, since I don't actually know what we're talking about with "flexibility", I'll just ramble about a few things.
First, lifetimes. Lifetimes are scary. But, I honestly don't see how or why lifetimes should be an issue in a high-level application, like a web app. If you have any specific scenarios, examples, or lived experiences, please share. Let me explain some of my experience with writing a couple of web services in Rust, with respect to lifetimes.
I've been using an http server called Actix-Web when I do Rust web stuff. It uses the same architecture style as Vert.x: it spins up N reactors (where N = number of CPUs by default) and each reactor is single-threaded and concurrent, which means that once a Request is routed to an available reactor, it never leaves that thread. This means that there are no complex lifetime issues with handling a Request- all of your logic can be single-threaded and treats the Request as having "static" lifetime (the Request outlives your handler function, so as far as your function knows, the Request lives forever. The caveat is that you borrow its content such as headers, uri, etc and would need to take copies if you wanted to send them elsewhere). I've never had non-trivial issues with lifetimes when it comes to the basics of request processing.
The SQL query builder I referred to in a previous comment gives us a transaction object with a legit lifetime, because it uses RAII to close the transactions and to return connections to the connection pool. The only time this has caused me grief was when I was trying to be clever by implementing a type class around transactions for some reason that I don't even remember now. I don't see how or why a changed business requirement would require us to extend a transaction's lifetime explicitly.
A further point: it's fairly easy to leak resources in Java because you can't do RAII except with the try-with-resources stuff. But, you can easily forget to try-with-resources and leak. Or, on the opposite side, since an object can still be referenced after you close it, you could pass an already-closed connection around and cause an error far from where the connection was first obtained and/or closed. In Rust, such mistakes would never compile.
Really, in an idiomatic Rust app, I would expect that the only place where you'd see explicit lifetimes is from RAII. Everything else is either going to be plain-old-data or some kind of ever-living actor/service. I'd be surprised to know that a high-level app is actively managing lifetimes of pretty much anything.
I'm not saying that it's impossible to end up with some ugly function signatures because of lifetimes. I can imagine writing a function that takes two parameters with independent lifetimes. But, I don't know why it would limit your agility.
Moving away from lifetimes.
Rust does preclude certain designs and architectures. You can't really do self-referencing structs (easily/simply/whatever), so you're not going to see a complex web of sibling objects referencing grand-parent objects, referencing the town they live in, referencing the grand-child objects. In this sense, yes, Rust is less flexible, and if you try to write Java style code in Rust, it's going to be painful. But does this make a Rust app less agile? In my opinion, no. Sure, you need to write your code in a different style than you would with a different language. And, sure, there's a learning curve to writing "good" Rust code, but are you willing to tell me that there isn't a learning curve to writing enterprise Java app style? The millions of pages printed and watts burned by people teaching and learning Gang Of Four design patterns, and Domain Driven Design, and Clean Architecture would suggest otherwise. Then the millions of watts burned on StackOverflow posts about == vs .equals(), and how static methods work with inheritance, and how to implement a generic interface for multiple types (you don't), and what the difference is between DAOs and Repositories and Services, etc, would also suggest otherwise.
In fact, here are some things that have made my Rust code MORE agile:
* You know how people praise static typing as allowing more confidence in refactoring? The idea of doing a big refactor of a Python or JavaScript code base makes me break into a cold sweat. Rust's type system is way stricter than Java's and I'm much more confident that when I refactor Rust code, I won't accidentally introduce a race condition or resource leak.
* If I want to extend a type with new functionality, I don't even have to own that type. Or, if I do, I don't even have to change the original file. I can define a new trait *and* write the implementation of that trait near the code that uses it. How do you do it in Java? You write your new interface and then write a wrapper class that delegates to the original class. Except now, you can't use that wrapper class in place of the original- you have to convert back and forth. Not so in Rust. Much more "flexible", IMO.
* modules > packages for namespacing and visibility.
* traits allow me to define/require "static" methods on implementing types.
* If you have two interfaces, Foo and Bar, in Java, and you want to write some code that does something special for a type that is both Foo and Bar, what do you do? It's been a while, but if I remember correctly, you have to define a new interface called FooBar that extends Foo and Bar and you have to go find every class that implements both Foo and Bar, and change them to implement FooBar, instead. In Rust, I can just write a function: `fn do_stuff(o: T)`. Done. Didn't have to define a new type, didn't have to touch old stable code, etc.
* I can implement a generic trait for multiple type parameters (eat that, Comparable!).
All of the above have allowed me to add or change functionality with minimal added code and minimal regressions.
Java being flexible is a truism, IMO. It's not flexible. We've just mastered it to the point that we don't even try to do things that we know are impossible, but are totally reasonable to want to do. We've gotten so used to its restrictions and limitations that we don't even see them anymore, or we just pretend like it's actually better this way.
If you have two interfaces, Foo and Bar, in Java, and you want to write some code that does something special for a type that is both Foo and Bar, what do you do? It's been a while, but if I remember correctly, you have to define a new interface called FooBar that extends Foo and Bar and you have to go find every class that implements both Foo and Bar, and change them to implement FooBar, instead. In Rust, I can just write a function: `fn do_stuff(o: T)`. Done. Didn't have to define a new type, didn't have to touch old stable code, etc.
Since Java 8 there is static and default methods in interfaces.
Curious, what rust sql query builder are you refer to?
That's not relevant to the part of my post that you quoted...
I was describing a hypothetical situation where you already have two interfaces, but would like to have functionality that only makes sense for an object that implements BOTH of those interfaces. The only way to do that in Java is to write a THIRD interface that combines the two and then go through and change your implementations to implement that new interface instead of the two separate ones.
However, in the bullet point above, I mentioned static methods on traits in Rust, which is different than what static methods on interface in Java are. In Java, a static method on an interface is just a function on the interface, itself. In Rust a trait can declare that an implementing type must have a static method matching the signature. This is because Rust traits are type classes, while Java interfaces are just object interfaces and cannot constrain the implementing type, itself.
> Curious, what rust sql query builder are you refer to?
I have been mostly using mysql_async (https://docs.rs/mysql_async/latest/mysql_async/), but recently started playing with sqlx (https://github.com/launchbadge/sqlx). I guess "query builder" isn't the right way to describe them, but I'm not sure what else to call them...