[{"content":" Some time ago, I wrote an article about distributing Java command-line applications and how difficult it is to locate the proper java binary to run your application with. But let’s have a quick refresher on the problem before going further.\nThe Problem When distributing a Java application via package managers, your application should rely on one of the Java Runtime Environments (a.k.a. JRE) provided by the package manager. Ensuring the correct JRE will be installed as a dependency of your application is the job of the package manager.\nHowever, once installed, locating where is the JRE your application needs can be a daunting task. The JRE in the user’s $PATH may not match the required version, and the package manager might not provide consistent installation paths for the JREs nor a way to select one of them according to some criteria like the minimum or maximum Java version.\nThis got worse with Java’s frequent release cycle where new versions are published every six months. Each new version brings performance improvements, security enhancements, and feature additions, as well as deprecations and removals. It becomes vital to express which Java versions the application is compatible with.\nThis situation poses challenges for application developers, package maintainers, and end-users.\nDevelopers need a convenient way to ensure their program runs with the correct JRE.\nPackage maintainers are reluctant to provide JRE selection mechanisms (not talking about system-wide mechanisms like /etc/alternatives here) as it is Java-specific and not their primary responsibility. On top of that, every single package manager would need such a mechanism.\nEnd-users want a seamless experience without having to worry about which Java version is in their $PATH when they call your application.\nfindjava to the rescue findjava is a powerful tool designed to alleviate these challenges and simplify the process of finding the most suitable JRE (but not only) to run your Java applications. It is licensed under the Apache-2.0 license.\nIt offers the following benefits:\nJRE discovery findjava scans a list of directories, files, and environment variables to discover installed JREs on the system. Those are configurable and typically would be tailored for each package manager.\nJRE metadata extraction Each discovered JRE is analyzed, and relevant metadata is extracted, providing valuable information about the JRE characteristics.\nWhat Java specification version does it support?\nWhere is its java.home directory?\nIs it providing additional tools like javac, native-image, …​?\nJRE metadata are cached and invalidated when changes are detected (e.g., JRE updates, deletions, additions), ensuring efficient and up-to-date JRE information.\nJRE filtering findjava allows you to apply specific constraints when selecting the JRE, such as:\nMinimum/maximum Java specification version implemented by this JRE\nDesired programs part of this Java installation (e.g., java, javac, native-image)\nThe vendor of this JRE\nFlexible output modes The output mode of findjava is configurable. It can provide the path to the java.home directory of the selected JRE or directly the desired binary path.\nSystem and application level configuration JRE discovery and filtering can be configured at both the system and application levels.\nSystem-level configuration allows package managers to control JRE selection. For example, only JREs implementing Java specifications 11 to 17 should be proposed.\nApplication-level configuration enables exceptions for specific use cases. For example, an application requires a JRE 16 or above.\nfindjava will try to satisfy the system-level preferences. If it is not possible due to some more restrictive constraints for the given application, then it will step out of the system-level restrictions.\nFor example, if a system has JREs 8, 11, and 20 installed and the system is configured to prefer a JRE in the 11 to 17 range., the following will happen:\nfindjava ⇒ 11 as it is in the preferred range.\nfindjava --min-java-version 11 ⇒ 11 as it is in the preferred range.\nfindjava --min-java-version 16 ⇒ 20 as available JREs in the preferred range are not satisfying the specific constraints of that JRE.\nfindjava --max-java-version 10 ⇒ 8 as available JREs in the preferred range are not satisfying the specific constraints of that JRE.\nThis mechanism allows package maintainers to provide a system-wide behavior to prefer their battle-tested JREs while allowing individual applications to still express additional constraints.\nGoing further For more information, take a look at the findjava usage documentation.\nIntegration with package managers findjava is currently available in external (i.e. non-official) package repositories:\nUbuntu PPA\nFedora COPR\nHomebrew tap\nFind out how to install it via the installation instructions\nCall for help: Integration in official package managers But for findjava to be useful for you to use in your application, it needs to be widely available and therefore integrated into official package managers for Linux, macOS, and at some point Windows.\nI already started the process for Debian (#1039109), but I would need some Debian expert to move things forward.\nIf you are experienced in package manager integration or want to contribute in any capacity, your help is invaluable.\nContributing Contributions of any kind to findjava are welcome! Feel free to open issues, submit pull requests, or join in the discussions on the GitHub repository.\nLet’s work together to enhance the development, distribution, and user experience of Java applications!\n","permalink":"https://loicrouchon.com/posts/findjava-simplifying-java-application-start-script/","summary":"Some time ago, I wrote an article about distributing Java command-line applications and how difficult it is to locate the proper java binary to run your application with. But let’s have a quick refresher on the problem before going further.\nThe Problem When distributing a Java application via package managers, your application should rely on one of the Java Runtime Environments (a.k.a. JRE) provided by the package manager. Ensuring the correct JRE will be installed as a dependency of your application is the job of the package manager.","title":"Introducing findjava, a tool to simplify JRE selection for Java applications"},{"content":" Today, I would like to discuss an issue many developers might be facing when having multiple computers. Let’s say one for personal use and one provided by the employer.\nDevelopers are usually putting a lot of care into setting up their environment. Whether this means the configuration of tools like their shell, git, …​, or adding custom scripts or bash/zsh/fish/…​ aliases. In both cases, this is usually achieved through files present in the user’s $HOME directory.\nWhy should those files be synchronized? One of the main reasons developers put a lot of attention to those files is the productivity it gives them. Being in a familiar and tailored to your need environment helps in being more efficient.\nIn my experience, the cost of not having a consistent experience between my personal machine and my work one was high: Different user experiences and expectations due to different shell prompts, different/missing aliases/scripts, and so on.\nThe cost of maintaining those two environments in sync by hand was just too high and led to many mistakes.\nTime to automate this!\nThe Cloud file sync options In order to synchronize those files between the two machines, the first option that came to my mind was to use a file synchronization mechanism like those offered by Tresorit, Dropbox, Google Drive, …​\nYet, this has some major inconveniences.\nConfiguration of synchronization tools for a sparse files tree is at the very least extremely cumbersome.\nIt might not be allowed to install, configure or use a Cloud synchronization tool on company-provided devices\nIt is also not possible to backup files that should not be synchronized because they are device, OS, or context (personal versus professional) dependent like a ~/.ssh/config or ~/.gitconfig.\nRethinking the problem The main problem was the sparse file tree to keep in sync. So what if there could be a way to centralize them in a dedicated folder which we will call a repository.\nAt this stage, there are two options to install those files at the desired location which we will call the directory. Either install those files to their destination using a file copy via cp, rsync, or any other tool. Or install them by creating a symbolic link. For example: $HOME/.bashrc → $HOME/config/.bashrc.\nLet’s take a look at the two strategies: copying versus linking.\nCopying strategy The copying strategy creates a copy of the repository file in the directory. This means that the workflow to operate is to:\nModify a file in the repository\nTrigger the synchronization from repository to directory\nThis strategy has the following limitations:\nThe visibility of where the file is coming from is limited.\nChanges done by tools modifying the files directly in the directory will not be visible in the repository. A tool would be necessary to perform a comparative analysis of the directory and the repository content and perform a merge.\nIt is hard to find out that a file was deleted from the repository and should thus be deleted from the directory on the next synchronization. A history of the synchronization would need to be kept to tackle such use cases.\nLinking strategy The linking strategy creates a symbolic of from the directory file to the file in the repository. This means that the workflow to operate is to:\nModify a file in the repository or in the directory and changes are immediately effective\nOnly when adding new files or deleting old ones is it necessary to trigger the synchronization from repository to directory\nThis strategy has the following advantages:\nThe visibility of where the file is coming from is explicit and held by the symbolic link.\nChanges done by tools modifying the files in the directory will be immediately visible in the repository.\nIt is possible to create symbolic links to directories and not only to files.\nIt is easy to find out that a file was deleted from the repository as there would be a dangling symbolic link in the directory pointing to the repository. No synchronization log is needed as the links hold all the information themselves.\nIt is possible to use hard links instead of symbolic links, but then, the limitations of the copy strategy about deletions would kick back in.\nIntroducing Symly: from a sparse file tree to a centralized one Symly is a tool I created that replicates the repository files tree structure in the directory. It applies the linking strategy to create links to files in a repository in the desired directory.\nSymly is free and Open Source released under the Apache License Version 2.0.\nIt provides the following features:\nReplication of the repository files tree structure in the directory\nSymbolic link creation/update/deletion for files (default mode)\nSymbolic link creation/update/deletion for directories (on demand only)\nOrphan symbolic links deletion\nSupport for multiple repositories\nLayering of repositories allowing for custom files and defaults ones\nNot limited to dotfiles, can be used for any other directory/file types.\nIt is based on the following principles:\nThe repository file tree is the state\nThis principle has the following implications:\nNo command to add a file to a repository, just drop it there!\nNo command to delete a file from a repository, just delete it!\nNo command to edit a file in a repository, just edit it directly or through its symbolic link in the _directory! This allows for seamless integration with tools modifying dotfiles directly on the directory (like git config user.name …​, …​)\nImmediate visibility on modifications made on the directory files\nSymly is not a synchronization tool\nSymly is not a synchronization tool. It enables any synchronization tools by providing them a single centralized folder to work on: the repository. Whether you chose a cloud-based solution, a developer-like solution like git, or a simple rsync, it’s up to you. Updates, diffs, and conflicts management are thus under the responsibility of those tools.\nHere is a simple example where we consider the following ~/config/dotfiles repository:\n~/config/dotfiles |-- .bashrc |-- .gitconfig \\-- .config |-- starship.toml \\-- fish \\-- config.fish Let’s call Symly with the ~/config/dotfiles repository and use the user’s home folder as the directory .\n\u0026gt; symly link --dir ~ --repositories ~/config/dotfiles This will create the following symbolic links:\n$HOME/.bashrc -\u0026gt; $HOME/config/dotfiles/.bashrc $HOME/.gitconfig -\u0026gt; $HOME/config/dotfiles/.gitconfig $HOME/.config/starship.toml -\u0026gt; $HOME/config/dotfiles/.config/starship.toml $HOME/.config/fish/config.fish -\u0026gt; $HOME/config/dotfiles/.config/fish/config.fish Combining Symly and a synchronization tool One of Symly’s strengths, or weaknesses, depending on the point of view, is that it is not a synchronization tool. Hence, you can use the tool of your choice, or the one best suited for the job.\nYou do not need multiple computers to experience the diffs, backups, and other benefits of using the tool of your choice for this task.\nIn my case, even if I could use any cloud files synchronization tool like Tresorit, Dropbox, Google Drive, or even a simple rsync, I decided to use git.\nWhy git? Well, git is not a file synchronization tool, but I realized that the way I work with my dotfiles is actually more like a development workflow, hence git.\nWith the Symly + git combination I get:\nOverview of current changes with git diff/git status: no risk of a silent modification of my config files by some external tool to be unnoticed.\nHistory of changes with git log and revert to older versions in case of issues.\nBranches for experimental changes.\nIn short, all the power of git for managing my dotfiles without the need to learn a new tool.\nGoing further In this article, I explained the basic capabilities of the tool. In the next article, I will walk you through what I consider the major strength of Symly: repository layering. This amazing feature allows you to perform the linking in a different way depending on the context (work/personal, macOS/Linux, …​).\nUntil then, you can take a look at the documentation, installation instructions, and roadmap on GitHub.\nI’d be happy to hear your feedback and opinion.\n","permalink":"https://loicrouchon.com/posts/easy-dotfiles-management-with-symly/","summary":"Today, I would like to discuss an issue many developers might be facing when having multiple computers. Let’s say one for personal use and one provided by the employer.\nDevelopers are usually putting a lot of care into setting up their environment. Whether this means the configuration of tools like their shell, git, …​, or adding custom scripts or bash/zsh/fish/…​ aliases. In both cases, this is usually achieved through files present in the user’s $HOME directory.","title":"How to synchronize dotfiles between multiple machines?"},{"content":" Kubernetes is a highly configurable and complex open-source container orchestration engine. Therefore, it is very easy to feel completely overwhelmed when learning it. The goal of this article is to present the very basic concepts at the core of it while keeping the focus on the development side.\nSome concepts First, let’s start with some concepts we will play within this article.\nKubernetes is running and orchestrating containers. I assume here that you are already familiar with containers, if not, have a look at containers first.\nWorkloads A workload is an application running on Kubernetes. Workloads are run inside a Pod.\nPod Pods are the smallest deployable units of computing that you can create and manage in Kubernetes.\nA Pod (as in a pod of whales or pea pod) is a group of one or more containers, with shared storage and network resources, and a specification for how to run the containers. A Pod’s contents are always co-located and co-scheduled, and run in a shared context. A Pod models an application-specific \u0026#34;logical host\u0026#34;: it contains one or more application containers which are relatively tightly coupled. In non-cloud contexts, applications executed on the same physical or virtual machine are analogous to cloud applications executed on the same logical host.\nWorkload Resources Kubernetes provides several built-in workload resources. We will focus here on two of those, the Deployment and the ReplicaSet.\nDeployment A Deployment provides declarative updates for Pods and ReplicaSets.\nYou describe a desired state in a Deployment, and the Deployment Controller changes the actual state to the desired state at a controlled rate. You can define Deployments to create new ReplicaSets, or to remove existing Deployments and adopt all their resources with new Deployments.\nReplicat Set A ReplicaSet\u0026#39;s purpose is to maintain a stable set of replica Pods running at any given time. As such, it is often used to guarantee the availability of a specified number of identical Pods. How a ReplicaSet works\nNetworking In order for applications to work together, they need to communicate and be accessible to other applications and the outside world. This is managed through networking.\nWe will focus here on the Service and Ingress notions.\nService An abstract way to expose an application running on a set of Pods as a network service. With Kubernetes you don’t need to modify your application to use an unfamiliar service discovery mechanism. Kubernetes gives Pods their own IP addresses and a single DNS name for a set of Pods, and can load-balance across them. Ingress In order to redirect public traffic into the cluster, we need to define an Ingress.\nAn API object that manages external access to the services in a cluster, typically HTTP. Ingress may provide load balancing, SSL termination and name-based virtual hosting. Wrapping it up If we had to summarize those concepts in a few words, we would have this:\nPod A group of one or more containers, sharing storage and network resources, with their run instructions that are deployed together.\nDeployment A declarative way of defining the desired state for Pods and how to deploy and roll it out.\nReplicatSet Guarantees the availability of a specified number of identical Pods.\nService A single access point (IP/port) with load balancing to a set of Pods.\nIngress A gateway to the cluster with routing to services.\nInteracting with a Kubernetes cluster The kubectl command-line tool is the best way to interact with a Kubernetes cluster.\nIn order to understand the example application that will follow, let’s introduce the main concepts and patterns behind it.\nkubectl \u0026lt;verb\u0026gt; \u0026lt;resource\u0026gt; A general pattern for the kubectl command is the kubectl \u0026lt;verb\u0026gt; \u0026lt;resource\u0026gt; one.\nWhere:\n\u0026lt;verb\u0026gt; is (but not limited to): create get, describe, patch, delete, expose, …​\n\u0026lt;resource\u0026gt; is a Kubernetes resource: pod, deployment, service, …​\nNote You can add an s to a resource name when doing a get, it feels more natural or you can use short versions. For example, the following commands will all return the list of services:\nkubectl get service\nkubectl get services\nkubectl get svc\nkubectl apply A typical usage is to apply a Kubernetes resource file. Such a file (usually yaml or JSON) contains the definition of one or more resources that will be applied to the cluster.\nFor example: kubectl apply -f https://uri.to/resources.yaml\nA sample application Prerequisites It is required to have a Kubernetes cluster available with:\nkubectl configured to target the Kubernetes cluster for reproducing the examples\nA Dashboard installed\nA Metrics server installed\nAn Ingress Controller installed\nIn case you do not already have this, follow instructions in the appendix.\nDeploy it Let’s use a sample echoserver application as an example. This application will echo the HTTP request information received (URI, method, headers, body, …​) in the response it will return. It is provided as a container image named k8s.gcr.io/echoserver:1.4.\nWe can create a deployment for this application with the following command:\n❯ kubectl create deployment echoserver --image=k8s.gcr.io/echoserver:1.4 deployment.apps/echoserver created We can validate our deployment has correctly been created\n❯ kubectl get all NAME READY STATUS RESTARTS AGE pod/echoserver-75d4885d54-cbvhh 1/1 Running 0 11m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 2d22h NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/echoserver 1/1 1 1 11m NAME DESIRED CURRENT READY AGE replicaset.apps/echoserver-75d4885d54 1 1 1 11m We do see our deployment deployment.apps/echoserver has been created. It instantiated a ReplicaSet replicaset.apps/echoserver-75d4885d54 which in turn created pod pod/echoserver-75d4885d54-cbvhh which runs our container.\nIn the dashboard, the Workloads overview will look like this.\nFigure 1. Workloads overview in the Kubernetes dashboard and our deployment:\nFigure 2. Deployments in the Kubernetes dashboard Make it accessible Now that the application is deployed, we need to make it accessible. In order to do so, we need to create a service pointing to our pods and expose it externally via an ingress.\nLet’s create a service that will listen on port 80 and forward the traffic to one of the pod in the deployment on its port 8080.\n❯ kubectl expose deployment echoserver --port 80 --target-port 8080 service/echoserver exposed Now, let’s expose the service externally and route traffic matching for which the HTTP host is echoserver.localdev.me.\n❯ kubectl create ingress echoserver --class=nginx --rule=\u0026#39;echoserver.localdev.me/*=echoserver:80\u0026#39; ingress.networking.k8s.io/echoserver created Note The ingress controller will in our example read the host field of incoming HTTP requests and perform routing to internal services based on its value. However, a prerequisite is that the request reaches the ingress controller and this implies the DNS echoserver.localdev.me has to be routed to the Kubernetes cluster which here is localhost. So how does this echoserver.localdev.me work?\nlocaldev.me is a service that defines its own DNS servers and will answer with the loopback IP address (127.0.0.1) to any subdomains of localedev.me DNS requests. You can have a look at their DNS records. This is handy as it allows to use subdomains in local to perform ingress routing without the need for manual /etc/hosts modifications.\nWe can see the service and ingress.\n❯ kubectl get all,ingress NAME READY STATUS RESTARTS AGE pod/echoserver-75d4885d54-cbvhh 1/1 Running 0 21m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/echoserver ClusterIP 10.99.239.96 \u0026lt;none\u0026gt; 8080/TCP 2m16s service/kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 2d22h NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/echoserver 1/1 1 1 21m NAME DESIRED CURRENT READY AGE replicaset.apps/echoserver-75d4885d54 1 1 1 21m NAME CLASS HOSTS ADDRESS PORTS AGE ingress.networking.k8s.io/echoserver nginx echoserver.localdev.me localhost 80 66m In the dashboard, our service will appear as below.\nFigure 3. Services in the Kubernetes dashboard Now that everything is in place, we can target http://echoserver.localdev.me with curl:\n❯ curl echoserver.localdev.me CLIENT VALUES: client_address=10.1.0.34 command=GET real path=/ query=nil request_version=1.1 request_uri=http://echoserver.localdev.me:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: accept=*/* host=echoserver.localdev.me user-agent=curl/7.64.1 x-forwarded-for=192.168.65.3 x-forwarded-host=echoserver.localdev.me x-forwarded-port=80 x-forwarded-proto=http x-forwarded-scheme=http x-real-ip=192.168.65.3 x-request-id=c447aa00579e6df16f6b1b854867de12 x-scheme=http BODY: -no body in request- Scale Scale Scale One interesting feature of Kubernetes is its ability to understand application requirements in terms of resources like CPU and/or memory. Thanks to a metrics server, resource consumption is measured and the cluster can react on it.\nBut in order for the cluster to react, it first needs to know the application requirements. Kubernetes defines two levels: requests and limits.\nRequests are the minimum resources that will be allocated. If it is not possible to allocate those resources, the pod or container creation will fail.\nLimits are the maximum resources that will be allocated. A higher limit than the request value allows for the application to burst for short periods of time. This is especially useful if a warmup phase is necessary or for unpredictable workloads that can’t wait for additional pods to be scheduled and ready to serve traffic.\nLet’s patch our deployment to define CPU requests and limits. Here we will define very low limits in order to play with autoscaling. Let’s say a request of 1 milliCPU and a limit of 4 milliCPU. A milliCPU is an abstract CPU unit that aims to be consistent across the cluster. 1000 milliCPU usually boils down to 1 hyperthread on hyperthreaded processors.\n❯ kubectl patch deployment echoserver --patch \u0026#39;{\u0026#34;spec\u0026#34;: {\u0026#34;template\u0026#34;: {\u0026#34;spec\u0026#34;: {\u0026#34;containers\u0026#34;: [{\u0026#34;name\u0026#34;: \u0026#34;echoserver\u0026#34;, \u0026#34;resources\u0026#34;: {\u0026#34;requests\u0026#34;: {\u0026#34;cpu\u0026#34;: \u0026#34;1m\u0026#34;}, \u0026#34;limits\u0026#34;: {\u0026#34;cpu\u0026#34;: \u0026#34;4m\u0026#34;}}}]}}}}\u0026#39; deployment.apps/echoserver patched We can now enable Horizontal Pod Autoscaling (HPA) for our deployment from 1 to 4 instances with a target CPU threshold of 50%.\n❯ kubectl autoscale deployment echoserver --cpu-percent=50 --min=1 --max=4 horizontalpodautoscaler.autoscaling/echoserver autoscaled We can then generate load with JMeter and see the number of pod replicas increasing until the maximum of 4 that is defined in our configuration.\n❯ kubectl get hpa echoserver --watch NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE echoserver Deployment/echoserver \u0026lt;unknown\u0026gt;/50% 1 4 0 5s echoserver Deployment/echoserver 1000%/50% 1 4 1 60s echoserver Deployment/echoserver 700%/50% 1 4 4 2m At the beginning, we had 0 replicas in the echoserver hpa as the hpa was just started. However a pod from the previous deployment was handling the traffic, but that pod wasn’t part of the hpa listed below.\nMeanwhile, in the dashboard, we can see the load of each pod replica.\nFigure 4. ReplicaSet autoscaling in the Kubernetes dashboard Declarative configuration Until now we used the kubectl tool in an imperative way. It is an easy way to start, but not really the best way to deploy an application in a reproducible way.\nTo this end we can define our Kubernetes resources (deployments, services, ingress) as JSON or yaml files and apply them to the cluster using the kubectl apply -f \u0026lt;file\u0026gt; pattern.\nFirst, let’s delete our application\n❯ kubectl delete deployment,svc,ingress,hpa echoserver deployment.apps \u0026#34;echoserver\u0026#34; deleted service \u0026#34;echoserver\u0026#34; deleted ingress.networking.k8s.io \u0026#34;echoserver\u0026#34; deleted horizontalpodautoscaler.autoscaling \u0026#34;echoserver\u0026#34; deleted Now let’s apply the following Kubernetes resources configuration.\n❯ kubectl apply -f https://loicrouchon.com/posts/kubernetes-introductions-for-developers/echoserver.yml service/echoserver created ingress.networking.k8s.io/echoserver created deployment.apps/echoserver created horizontalpodautoscaler.autoscaling/echoserver created Here is the content of the resources yaml file. It contains the following resources:\nThe echoserver Deployment with CPU requests and limits.\nThe echoserver Service pointing to the echoserver Deployment.\nThe echoserver Ingress exposed on echoserver.localdev.me and targeting the echoserver Service.\nthe echoserver Horizontal Pod Autoscaler.\n--- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/name: echoserver app.kubernetes.io/part-of: echoserver-app app.kubernetes.io/version: 1.0.0-SNAPSHOT name: echoserver spec: ports: - name: http port: 80 targetPort: 8080 selector: app.kubernetes.io/name: echoserver app.kubernetes.io/part-of: echoserver-app app.kubernetes.io/version: 1.0.0-SNAPSHOT type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: labels: app.kubernetes.io/name: echoserver app.kubernetes.io/part-of: echoserver-app app.kubernetes.io/version: 1.0.0-SNAPSHOT name: echoserver spec: ingressClassName: nginx rules: - host: echoserver.localdev.me http: paths: - backend: service: name: echoserver port: number: 80 path: / pathType: Prefix status: loadBalancer: ingress: - hostname: localhost --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/part-of: echoserver-app app.kubernetes.io/version: 1.0.0-SNAPSHOT app.kubernetes.io/name: echoserver name: echoserver spec: replicas: 1 selector: matchLabels: app.kubernetes.io/part-of: echoserver-app app.kubernetes.io/version: 1.0.0-SNAPSHOT app.kubernetes.io/name: echoserver template: metadata: labels: app.kubernetes.io/part-of: echoserver-app app.kubernetes.io/version: 1.0.0-SNAPSHOT app.kubernetes.io/name: echoserver spec: containers: - env: - name: KUBERNETES_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace image: k8s.gcr.io/echoserver:1.5 imagePullPolicy: Always name: echoserver ports: - containerPort: 8080 name: http protocol: TCP resources: limits: cpu: 10m memory: 20Mi requests: cpu: 5m memory: 5Mi readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 2 periodSeconds: 2 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 --- apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata: labels: app.kubernetes.io/part-of: echoserver-app app.kubernetes.io/version: 1.0.0-SNAPSHOT app.kubernetes.io/name: echoserver name: echoserver spec: maxReplicas: 4 minReplicas: 1 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: echoserver targetCPUUtilizationPercentage: 50 You can find out more about this declarative approach by reading:\nWorking with Kubernetes objects\nThe Reference API\nRolling out a new version of an application Let’s say we want to roll out a new version of our application. Instead of k8s.gcr.io/echoserver:1.4 we want to use the new k8s.gcr.io/echoserver:1.5 container image.\nTo do that we can edit the echoserver.yml we saw above and replace the image: k8s.gcr.io/echoserver:1.4 in the containers specification of the Deployment resource with image: k8s.gcr.io/echoserver:1.5.\nNow let’s apply the configuration again:\n❯ kubectl apply -f https://loicrouchon.com/posts/kubernetes-introductions-for-developers/echoserver.yml service/echoserver unchanged (1) ingress.networking.k8s.io/echoserver unchanged (1) deployment.apps/echoserver configured (2) horizontalpodautoscaler.autoscaling/echoserver unchanged (1) Unchanged resources, will not be updated\nUpdated resource, the deployment will be retriggered\nIn the console, we can see our deployment being updated with the new image. A new ReplicaSet is started and a new pod is created.\nFigure 5. Deployment rollout in the Kubernetes dashboard If in parallel you were shooting requests on a regular basis with a similar command\nwatch -n 0.5 curl -s echoserver.localdev.me You would have seen the output being updated when the traffic was routed to the new version of the application. This, without downtime as the Deployment configuration contains information to help Kubernetes understand if the application is ready and live. Those are called Liveness, Readiness and Startup Probes and are a must for ensuring traffic is only routed to healthy instances of your application.\nGoing further Today we covered some of the basic concepts of Kubernetes. How to deploy an application, expose it outside the cluster, scale it and perform rollouts. But there’s much more to cover.\nConfiguration: How ConfigMap and Secrets can help you to configure your applications.\nStorage: How to deal with volumes, Persistent or Ephemeral.\nCronJob to run recurring scheduled tasks.\nStatefulSet for managing stateful applications.\n…​\nThe Kubernetes documentation is great, but it might be a bit overwhelming at first. I highly recommend these sketchnotes series by Aurélie Vache or her Understanding Kubernetes in a visual way ebook. They are a great way to understand those concepts in an easy-to-digest format.\nAppendix A: Installing a Kubernetes cluster locally The easiest ways to install a local Kubernetes cluster are through one of:\nDocker Desktop\nkind\nminikube\nColima (for macOS users)\nFor example with Colima:\nbrew install colima docker context use colima colima start --cpu 4 --memory 8 --with-kubernetes Start the Kubernetes proxy on port 8001 In a background terminal start kubectl proxy. It will start a proxy that listens on http://localhost:8001/\nInstalling an ingress controller In our case, we will use nginx as ingress controller, but any other ingress controller is perfectly fine.\nYou can install it as below:\nkubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.1/deploy/static/provider/cloud/deploy.yaml kubectl wait pod --namespace ingress-nginx --for=condition=ready --selector=\u0026#39;app.kubernetes.io/component=controller\u0026#39; --timeout=120s kubectl port-forward --namespace=ingress-nginx service/ingress-nginx-controller 8080:80 Additional installation options are available in the ingress-nginx documentation.\nInstalling metrics Installation of metrics allows supporting autoscaling based on pods CPU consumption.\nkubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.1/components.yaml kubectl patch deployments.app/metrics-server -n kube-system --type=json --patch \u0026#39;[{\u0026#34;op\u0026#34;: \u0026#34;add\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/spec/template/spec/containers/0/args/-\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;--kubelet-insecure-tls\u0026#34; }]\u0026#39; Install the Kubernetes dashboard Install the dashboard and the metrics server with\nkubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml The dashboard is accessible at URL http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy\nTo log in to the dashboard, it is necessary to use a token. You can obtain a token using the following command:\nkubectl -n kube-system describe secret (kubectl -n kube-system get secret | awk \u0026#39;/^namespace-controller-token-/{print $1}\u0026#39;) | awk \u0026#39;$1==\u0026#34;token:\u0026#34;{print $2}\u0026#39; ","permalink":"https://loicrouchon.com/posts/kubernetes-introductions-for-developers/","summary":"Kubernetes is a highly configurable and complex open-source container orchestration engine. Therefore, it is very easy to feel completely overwhelmed when learning it. The goal of this article is to present the very basic concepts at the core of it while keeping the focus on the development side.\nSome concepts First, let’s start with some concepts we will play within this article.\nKubernetes is running and orchestrating containers. I assume here that you are already familiar with containers, if not, have a look at containers first.","title":"Introduction to Kubernetes for application developers"},{"content":" Let’s talk about git repositories! I don’t want to get into the mono versus multi-repository discussion. Some advocates for mono-repositories and others against them.\nBut no matter which side you’re on, chances are you contribute to repositories parts of different organizations: personal projects, professionals, or various open-source ones. In this case, it is for sure not possible to use a mono-repository approach.\nHence, I thought of a solution to help me keep some repositories up-to-date with their remotes or ensure I did push everything. No need for fancy things, a small shell script can do the work.\nPlease welcome gitr! gitr is an alias in my fish.config/.bashrc for a simple shell script I wrote.\nYou said gitr, what’s that? Gitr stands for git recurse. It expects a git command and will execute it in every subdirectory that is a git repository. It is very basic and only tailored to my needs. So don’t expect it to handle symbolic links or git submodules.\nUsage Let’s assume you are in a directory and have the following git repositories under it:\noss/jreleaser\noss/picocli\npersonal/xxx.github.io\npersonal/website\nLet’s now assume you want to fetch the different remotes to prepare for offline work. git has a command for that, git fetch --all. with gitr, all you need to do is replace git with gitr.\nHence: gitr fetch --all\n$ gitr fetch --all ## oss/jreleaser Fetching origin ## oss/picocli Fetching origin Fetching upstream ## personal/xxx.github.io Fetching origin ## personal/website Fetching origin Want to check if you committed and pushed everything? gitr status got you covered!\n$ gitr status ... ## personal/xxx.github.io On branch main Your branch is up to date with \u0026#39;origin/main\u0026#39;. nothing to commit, working tree clean ## personal/website On branch main Your branch is up to date with \u0026#39;origin/main\u0026#39;. Untracked files: (use \u0026#34;git add \u0026lt;file\u0026gt;...\u0026#34; to include in what will be committed) content/posts/dealing-multiple-git-repositories.md nothing added to commit but untracked files present (use \u0026#34;git add\u0026#34; to track) Obviously, I need to finish the content/posts/dealing-multiple-git-repositories.md article and publish it.\nSummary gitr is a very small script that can help manage multiple git repositories by calling git itself in every repository. You get all the power of git fetch, pull, status, …​\nOne last thing, be careful with it and think twice before calling gitr reset --hard origin/main or gitr commit -m \u0026#34;xxx\u0026#34;.\nMay it be helpful to you!\ngitr.sh can be downloaded here or you can see the source below. #!/bin/sh set -eu recurse() { ( cd \u0026#34;$1\u0026#34; shift if [ -d \u0026#34;.git\u0026#34; ]; then # If current directory is a git directory, # call the git command printf \u0026#34;\\033[1;34m## $(pwd)\\033[0m\\n\u0026#34; git \u0026#34;$@\u0026#34; printf \u0026#34;\\n\u0026#34; else # Otherwise, recurse in sub-directories for d in ./*; do if [ -d \u0026#34;$d\u0026#34; ]; then recurse \u0026#34;$d\u0026#34; \u0026#34;$@\u0026#34; fi done fi ) } recurse \u0026#34;.\u0026#34; \u0026#34;$@\u0026#34; ","permalink":"https://loicrouchon.com/posts/dealing-with-multiple-git-repositories/","summary":"Let’s talk about git repositories! I don’t want to get into the mono versus multi-repository discussion. Some advocates for mono-repositories and others against them.\nBut no matter which side you’re on, chances are you contribute to repositories parts of different organizations: personal projects, professionals, or various open-source ones. In this case, it is for sure not possible to use a mono-repository approach.\nHence, I thought of a solution to help me keep some repositories up-to-date with their remotes or ensure I did push everything.","title":"Dealing with multiple git repositories"},{"content":" As a professional Java developer, I am comfortable with writing code in Java. Also, when I decided to write a command-line application, I asked myself if Java would be a good option in 2021 or if I should look more into alternative stacks like Rust.\nSo let’s take a closer look at the challenges one has to face to implement a command-line application in Java.\nCreating a command-line application jar To write an application that can be used from the command-line, a simple public static void main(String[] args) is all you need in Java. However, do we really want to stop there?\nWhat about:\nmandatory/optional arguments/options parsing\ndefault values\nvalidations\nsub-commands support\nautomatic usage documentation\nautomatic help\nauto-completion\nman page generation\nFor all of this, no need to look for long, picocli has it all, and more. You can find a sample application in picocli’s documentation.\nSome other serious alternatives are JCommander and JLine. JLine is however a bit different as it is made to handle command-line inputs in a similar way to editline/readline. It can however be integrated with picocli if need be.\nFrom jar to application So now that we have an application that can be run as a jar, what do we need to do to make it a Command-Line Application?\nFirst, a Command-Line Application only deserves such a title if it is executable. A jar is by itself, not executable, it needs to be executed by the Java Virtual Machine by calling the java -jar command and giving the proper options required by our application. As we do not want to have to specify all those arguments when calling our application, we need a launcher script that will do this for us.\nIf you’re using Gradle, the application plugin comes in handy. It includes tasks to create a distributable version of the software which contains all runtime dependencies of your application (except the JVM) as well as launcher scripts for Unix (Linux, macOS, etc.) and Windows.\nOn the maven side, the assembly and shade plugins will help you to prepare the application jar and its dependencies, but you will still have to write the launcher script by yourself.\nOfficial versus custom package repositories At this point, we have an application as a jar, the runtime dependencies as jars, and a launcher script. It should not be so hard to create a package out of it.\nHowever, before creating a package, it is important to take a look at the policies of the target package managers and their official repositories as they define particular constraints on how packages should be created.\nFor example, the Debian Java policy states the following:\nSince it is common for Java source tarball to ship jar files of third-party libraries, creating a repack script to remove them from the upstream tarball is mandatory.\nThis implies that integrating your application in the official repositories will not be possible. Such integration would require the libraries you depend on to be available in official repositories with the proper versions, and that your build system relies on those. This is most of the time not possible as very few jar libraries are making it to official repositories or are only available in really old versions.\nIf you do not want to be subject to those constraints, you will then have to have to find a way to host your own .deb or .rpm repository. Some companies like packagecloud are providing such services.\nFor Homebrew, the homebrew tap (alternative repositories) relies on git and a simple GitHub repository is enough so this is not a major issue.\nThey are plenty of package managers out there. In this article, we will only focus on deb/rpm packages for Linux and homebrew for macOS as they are the most common ones.\nRuntime dependency on the JVM As the launcher relies on the java command to launch the application, the Java Virtual Machine must be installed and be present on the PATH. If you’re part of the lucky ones having a smart launcher like the ones generated by the Gradle application plugin, it will also look for you in the JAVA_HOME environment variable.\nIf the target public is Java developers, chances are they already have a JVM installed which is not the one from their system’s package manager. I personally use SDKMAN! for managing my JVMs and I don’t have Java installed via apt on Ubuntu or Homebrew on macOS.\nThis implies that requiring a dependency in the packaging tool on Java will for those users:\nInstall a potentially unnecessary but heavy package.\nHave no or little effect as the JAVA_HOME and the PATH environment variable will be set to something different than the one from the JVM installed by the default package manager.\nIn particular, Java developers do not always have the choice of the version of Java to use. Therefore it can happen their default JVM is older than you think. It might be imposed by the project. In particular, even though the public support of Java 8 stopped in January 2019, commercial support by different organizations is still very much alive.\nAccording to the The State of Developer Ecosystem in 2021 by JetBrains, Java 8 is in July 2021, still the most used version. This was already more than two years after the Java 11 release which is also an LTS release and only a few months before the Java 17 release.\nAs a consequence, the maximum version of Java the application should rely on is Java 8, as otherwise, the out-of-box installation might not work for users having the main JVM installed in their $PATH being older than Java 17 or even 11.\nHow not to rely on java in the user’s PATH? If like me you would like to use a more recent JDK for developing your application, a more sophisticated solution is required.\nTo be able to rely on a specific JRE version for running your applications, a possible solution is to force the path to the java binary or JAVA_HOME to a particular one. This can be achieved via the launcher script, but it requires customization of this script depending on the system.\nAt this stage, two approaches are possible. Specify the JVM dependency to the exact, or the minimum version required. Let’s take a look at both approaches after a quick clarification on rolling vs non-rolling package managers.\nRolling repositories vs non-rolling ones In order to provide users with applications, package managers rely on software repositories. Those repositories can be managed in two ways.\nStatic repositories as for Linux distributions like Debian/Ubuntu/Fedora/RHEL. In this mode, you will have independent software repositories for version X and version X+1 of the same system (i.e. Linux distribution). This allows for better stability and reproducibility as all the dependencies are pretty much fixed. For example, it is still possible to download Ubuntu 14:04 which is seven years old and install packages in a fully functional way with the versions that existed at that time.\nRolling repositories as for Homebrew and some other Linux distributions like Arch Linux for example. In this mode, new software will continuously be updated/added to the same repository. This makes it easier for integrating new software as there is only one place to publish new releases and for users to use as no need to update the repositories when upgrading the OS. However, it is impossible or very hard to go back in time like for static repositories.\nIf you release your software via your own repositories, you can choose the rolling mode even if it is not the case with the official repositories of the target system. However, it is very likely the JRE you will depend on will come from the official repositories. This will have some impact as we’ll see later on.\nDepending on a fixed JRE version Depending on a fixed JRE version is the easiest thing to do. Expressing the dependency is easy in every package manager. The path to the java executable is statically known. And as the JVM version is fixed, there is no risk for the app to stop working if a later JVM removes a feature or add restrictions like the JDK 17 did with JEP 403: Strongly Encapsulate JDK Internals and JEP 407: Remove RMI Activation.\nHowever, this comes at a cost.\nBecause of the fast release cadence of the JDK, a new JDK will be available every 6 months. If multiple Java-based applications are using this model, it is very likely they won’t all upgrade immediately their dependencies to the latest JRE. This will cause a proliferation of installed JREs which are quite space-consuming.\nOn top of that, performance improvements and security fixes of newer major versions won’t be available unless your application dependency on the JRE is updated to target the newer JREs.\nLast but not least, some package managers might not keep non-LTS versions of the JDK for a long time after the next one is released. For example, in Fedora 35 repositories, the JDK 11 and 17 are available, but the JDK 12 to 16 aren’t anymore. In Ubuntu 21.10 repositories, JDK 16 is still available but JDK 12 to 15 aren’t anymore.\nFor all those reasons, depending on a fixed JRE version is not recommended unless you also provide software in a static repository that matches the publication rhythm of the target system.\nDepending on a rolling JRE version Depending on a rolling JRE version is way harder. Two problems need to be solved.\nHow to express the dependency to a rolling JRE version instead of a fixed version.\nHow to know the path to the java executable of this rolling JRE version.\nUnfortunately for us, those two problems are highly system-dependent and therefore require tuning per package manager/system.\nExpressing a dependency on a rolling JRE The dependency part can be addressed in some package managers by depending on a latest JVM package or a virtual package.\nA latest package can be found in Fedora (java-latest-openjdk-headless) or using Homebrew (openjdk).\nOn Debian/Ubuntu, no such latest JRE package exists and therefore a dependency on a javaXX-runtime-headless virtual package will be needed where XX is the minimal java version required for the application to run. Each JRE package provides multiple javaXX-runtime-headless features. For example, the package openjdk-16-jre-headless provides java16-runtime-headless, java15-runtime-headless, …​ And the package openjdk-17-jre-headless also provides all of them plus the java17-runtime-headless.\nHowever, when used as a dependency, the Debian/Ubuntu package manager (apt) will take the latest version of OpenJDK providing it. Even if that latest version is an Early Access (EA) version.\nFor example:\n$ apt install software-depending-on-java17-runtime-headless ... The following NEW packages will be installed: ca-certificates-java java-common openjdk-18-jre-headless Here, openjdk-18-jre-headless version is an EA one 18~15ea-4 which is not the desired behavior.\nTo work around this, the dependency for the Debian package has to be expressed on the exact desired JRE (i.e. openjdk-17-jre-headless) with an alternative package being the virtual package (i.e. java17-runtime-headless).\nThis way, if no installed package provides java17-runtime-headless, the openjdk-17-jre-headless will be installed. Note however that if the user installs later a higher JRE, openjdk-18-jre-headless for example, the openjdk-17-jre-headless will not be uninstalled automatically.\nFinding the path to a rolling JRE java executable Here again, depending on the target system, the task will be an easy one or a hard one.\nFor Homebrew, both for macOS and Linux, the situation is quite easy as the /usr/local/opt/openjdk path points to the latest installed OpenJDK.\n$ ls -l /usr/local/opt/openjdk* lrwxr-xr-x 1 user admin 24 Nov 5 19:59 /usr/local/opt/openjdk -\u0026gt; ../Cellar/openjdk/17.0.1 lrwxr-xr-x 1 user admin 28 Nov 15 16:13 /usr/local/opt/openjdk@11 -\u0026gt; ../Cellar/openjdk@11/11.0.12 lrwxr-xr-x 1 user admin 24 Nov 5 19:59 /usr/local/opt/openjdk@17 -\u0026gt; ../Cellar/openjdk/17.0.1 So JAVA_HOME can be set to /usr/local/opt/openjdk in the application start script.\nOn Linux systems, the situation is a bit more complicated. The JREs are generally installed by system package managers under the /usr/lib/jvm directory. This directory contains one folder per installed JVM and some possible symlinks aliases.\nFor example on Ubuntu, the following structure can be found:\n$ ls -1 /usr/lib/jvm/ java-1.11.0-openjdk-amd64 -\u0026gt; java-11-openjdk-amd64/ java-1.16.0-openjdk-amd64 -\u0026gt; java-16-openjdk-amd64/ java-1.17.0-openjdk-amd64 -\u0026gt; java-17-openjdk-amd64/ java-1.8.0-openjdk-amd64 -\u0026gt; java-8-openjdk-amd64/ java-11-openjdk-amd64/ java-16-openjdk-amd64/ java-17-openjdk-amd64/ java-8-openjdk-amd64/ And on Fedora:\n$ ls -l /usr/lib/jvm java-1.8.0-openjdk-1.8.0.312.b07-1.fc35.x86_64 java-11-openjdk-11.0.13.0.8-2.fc35.x86_64 java-17-openjdk-17.0.1.0.12-1.rolling.fc35.x86_64 ... In this case, the JAVA_HOME can be set in the application start script to the latest JRE (17) by using the command:\nJAVA_HOME=/usr/lib/jvm/$(ls /usr/lib/jvm | grep -E \u0026#34;java-[0-9]+\u0026#34; | sort -V -r | head -n 1) This command lists all the folders in /usr/lib/jvm.\nThe grep -E \u0026#34;java-[0-9]+\u0026#34; filters to only keep the ones starting by java-[0-9]+. This includes java-17-openjdk-amd64 as well as more complicated folder names like java-1.8.0-openjdk-1.8.0.312.b07-1.fc35.x86_64.\nThe sort -V -r sorts them in reverse order (-r) using a version-based sorting algorithm (-V). First java-17-…​, then java-11-…​, then java-1.8.0-…​.\nFinally the head -n 1 only keeps the first result and the result is set in the JAVA_HOME variable.\nThis command works on Ubuntu, Fedora, and Alpine docker images without any other packages installed than a headless JRE.\nBumping the version of the JRE dependency Whether you express the dependency on the JVM via a specific version or only the minimum version required, you’ll probably want to upgrade it one day. This has to be done in a very careful way. Failing to do so will result in your application, even an old version of it, not being installable on old systems.\nFor example, the JDK 17 is available in the following package managers.\nAlpine: 3.15+\nFedora: 33+\nUbuntu: 18.04 and 20.04+\nIf at one point, you want to upgrade the JRE version to the JDK 18 or later, you can either:\nUpgrade the dependency version (rolling mode) and let down users on systems not having that package available yet. (if ever for static software repositories).\nCreate a new repository for the upgraded version (static mode) and inform/request your users to update their repositories configuration.\nSumming it up To conclude I would highlight the following points:\nA dependency on a fixed JRE version should be avoided. Instead, one should try to depend on default, latest versions or specify the minimum version.\nThe java command on the path doesn’t always point to the latest available JRE. This is especially true for Java developers in a corporate environment. Therefore, to improve the out-of-the-box installation for such users, the full path to the JRE should be specified in the start script.\nUpgrading the dependency on the JVM is hard. If the package manager’s official repositories are in rolling mode, the upgrade can be done without too much trouble. If it is a static one, a decision has to be made between not supporting old systems or communicating a repository configuration update to your users.\nGoing further and getting rid of the JVM runtime dependency As we saw, having a runtime dependency on the JVM brings the following problems:\nSize of the JVM package is quite significant\nWhich JVM package to depend on?\ndefault, latest, \u0026gt;= 17, = 17, = 11, …​\nWhich java executable to select?\nUse the one found in the PATH?\nReference a tagged one: default, latest\nReference a specific one: 16, 11, …​\nManually lookup for the ones installed and select one at runtime\nJars dependencies restrictions for integration in official package managers\nUpgrading the version of the JRE dependency\nWe saw a few techniques addressing those issues but they are still highly system-dependent and require build/packaging/CI integration effort.\nCould there be a way to avoid all or some of those problems? There are currently two ways to do so.\nThe first one consists in shipping the JRE runtime with our application. We can achieve this through the jpackage tool introduced in JDK 14 via JEP 392: Packaging Tool. This tool can generate a native package deb/rpm for Linux, pkg/dmg for macOS, or msi/exe for Windows. The included JRE will only package the required components and therefore be much slimmer than the JRE available through the OS package managers. The problem is that this slim JRE will not be shared with any other potential Java program and will therefore be duplicated for every single Java application packaged this way. This also does not remove the constraints on jar dependencies for official repositories integration.\nThe other solution is the generation of an OS/architecture native binary using GraalVM native-image technology, but this will be the topic of the next article.\n","permalink":"https://loicrouchon.com/posts/distributing-java-cli-app/","summary":"As a professional Java developer, I am comfortable with writing code in Java. Also, when I decided to write a command-line application, I asked myself if Java would be a good option in 2021 or if I should look more into alternative stacks like Rust.\nSo let’s take a closer look at the challenges one has to face to implement a command-line application in Java.\nCreating a command-line application jar To write an application that can be used from the command-line, a simple public static void main(String[] args) is all you need in Java.","title":"Distributing a Java command-line application"},{"content":" Old problems, new ways of solving it? If all you have is a hammer, everything looks like a nail.\nThis sentence summarizes pretty much how I felt about the idiomatic polymorphism approach in Java to most problems until recently. Polymorphism is just a tool and as for any tool, it excels at some tasks and performs poorly at others.\nSo what are the tasks where polymorphism does a good job at?\nI would say it is a good tool when you have a few functions operating on a potentially large or unknown number of data types. Or when the number of types will vary over time. This model is more behavior-centric where every data type brings its behavior to a fixed and small set of functions.\nHowever, there is another use case where a fixed and small set of data types exists, and an unknown number of functions rely on those types as a group. A typical example is a tree, graph, or other recursive data structure where it is difficult for a function to have a meaning as part of the types themselves.\nThis second use case is usually covered in Java using the Visitor pattern. This is, unfortunately, not the most transparent or straightforward design pattern.\nBut with the recent improvements and preview features in the JDK 17, can we do better?\nExample: Simplifying a boolean expression Before going into a code example, let’s first agree on the problem to tackle. We will implement a boolean expression NOT simplifier.\nWhy choosing a boolean expression for this example? A boolean expression is something very simple to model and understand. However, there are very few core operations on a boolean expression. Evaluating is one of those, but simplifying an expression is not part of it as there are too many moving parts or ways to do so. Hence the regular approach would use a visitor pattern.\nThe data model We will have an Expression interface with a few default methods and static factories and four records types implementing it:\npublic interface Expression { default Expression not() { return new Not(this); } default Expression and(Expression other) { return new And(this, other); } default Expression or(Expression other) { return new Or(this, other); } static Expression not(Expression expression) { return expression.not(); } } /** * A simple boolean variable having a name */ public record Variable(String name) implements Expression {} /** * Negates an expression */ public record Not(Expression expression) implements Expression {} /** * Combines two expressions using a logical \u0026lt;strong\u0026gt;and\u0026lt;/strong\u0026gt; */ public record And(Expression left, Expression right) implements Expression {} /** * Combines two expressions using a logical \u0026lt;strong\u0026gt;or\u0026lt;/strong\u0026gt; */ public record Or(Expression left, Expression right) implements Expression {} Records were introduced in JDK 16 via JEP 395: Records after two previews in JDK 14 and JDK 15. I encourage you to read the article Why Java’s Records Are Better* Than Lombok’s @Data and Kotlin’s Data Classes from Nicolai Parlog. It is a very detailed dive into what records are and what they’re not.\nThe goal The goal is to implement a method with the following signature:\n/** * \u0026lt;p\u0026gt;Takes an {@link Expression} and builds an other {@link Expression} where the {@link Not} * are pushed to {@link Variable} leaves. * For example: * \u0026lt;ul\u0026gt; * \u0026lt;li\u0026gt;{@code NOT(A AND B) =\u0026gt; (NOT A) OR (NOT B)}\u0026lt;/li\u0026gt; * \u0026lt;li\u0026gt;{@code NOT(A OR B) =\u0026gt; (NOT A) AND (NOT B)}\u0026lt;/li\u0026gt; * \u0026lt;/ul\u0026gt; * \u0026lt;/p\u0026gt; * \u0026lt;p\u0026gt;When two {@link Not} are consecutive, they are eliminated. * For example: {@code NOT(NOT(A)) =\u0026gt; A}\u0026lt;/p\u0026gt; * * @param e the {@link Expression} to simplify * @return the simplified {@link Expression}. */ public static Expression simplify(Expression e) { throw new UnsupportedOperationException(); } The visitor pattern I have some respect for you and I will therefore spare you the full implementation of the visitor pattern. If you’re still interested in the implementation, it is available on Github.\nThe naive non-visitor pattern implementation with if statements In Java 15 and before, without using preview features, we would have had to write something like this:\npublic static Expression simplify(Expression e) { if (e instanceof Variable) { return e; } if (e instanceof And) { And and = (And) e; return simplify(and.left()).and(simplify(and.right())); } if (e instanceof Or) { Or or = (Or) e; return simplify(or.left()).or(simplify(or.right())); } if (e instanceof Not) { return simplifyNot((Not)e); } throw new UnsupportedOperationException(\u0026#34;Expression type not supported \u0026#34; + e.getClass()); } public static Expression simplifyNot(Not not) { Expression e = not.expression(); if (e instanceof Variable) { // nothing to simplify return not; } if (e instanceof And) { // NOT(A AND B) =\u0026gt; NOT(A) OR NOT(B) And and = (And) e; return simplify(not(and.left())).or(simplify(not(and.right()))); } if (e instanceof Or) { // NOT(A OR B) =\u0026gt; NOT(A) AND NOT(B) Or or = (Or) e; return simplify(not(or.left())).and(simplify(not(or.right()))); } if (e instanceof Not) { // NOT(NOT(A)) =\u0026gt; A return simplify(((Not)e).expression()); } throw new UnsupportedOperationException(\u0026#34;Expression type not supported \u0026#34; + e.getClass()); } This approach has two issues:\nIt is lengthy as we do redundant instanceof/casts\nIt is not proven complete at compilation time, hence the need to throw UnsupportedOperationException if no condition is met. In case a sub-type to Expression is added, the compiler will not tell us, and we might only discover it only at runtime.\nSince Java 15, we can improve the first point by leveraging on JEP 394: Pattern Matching for instanceof. It avoids the cumbersome casts and allows to rewrite code:\nif (obj instanceof String) { String s = (String) obj; // mandatory cast } into this one:\nif (obj instanceof String s) { // s is a String } Applying it to our example, it would look like this:\npublic static Expression simplify(Expression e) { if (e instanceof Variable v) { return v; } if (e instanceof And and) { return simplify(and.left()).and(simplify(and.right())); } if (e instanceof Or or) { return simplify(or.left()).or(simplify(or.right())); } if (e instanceof Not not) { return simplifyNot(not); } throw new UnsupportedOperationException(\u0026#34;Expression type not supported \u0026#34; + e.getClass()); } public static Expression simplifyNot(Not not) { Expression e = not.expression(); if (e instanceof Variable v) { return not; } if (e instanceof And and) { return simplify(not(and.left())).or(simplify(not(and.right()))); } if (e instanceof Or or) { return simplify(not(or.left())).and(simplify(not(or.right()))); } if (e instanceof Not notnot) { return simplify(notnot.expression()); } throw new UnsupportedOperationException(\u0026#34;Expression type not supported \u0026#34; + e.getClass()); } That’s how far we can get with the if/else approach. We cannot unfortunately have completeness checked by the compiler at this point.\nThe switch-based approach Say hello to the switch expressions Using a switch-based approach, we would improve readability and could potentially tackle the completeness issue. But for this, we need JEP 361: Switch Expressions which was introduced in JDK 14 after two previews.\nThis allows to rewrite the following code in a much nicer way:\nint numLetters; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: numLetters = 6; break; case TUESDAY: numLetters = 7; break; case THURSDAY: case SATURDAY: numLetters = 8; break; case WEDNESDAY: numLetters = 9; break; default: throw new IllegalStateException(\u0026#34;Wat: \u0026#34; + day); } int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -\u0026gt; 6; case TUESDAY -\u0026gt; 7; case THURSDAY, SATURDAY -\u0026gt; 8; case WEDNESDAY -\u0026gt; 9; }; As you can see, the throw clause disappeared because a completeness check is performed. A compilation error is thrown if not all cases are covered. Assuming we remove the case WEDNESDAY, the following compilation error is raised:\n| Error: | the switch expression does not cover all possible input values | int numLetters = switch (day) { | ^-------------... This is a great addition that makes switch more robust than if/else as you’ll be warned at compilation time if not all branches are covered. The JEP also states the following to cover potential runtimes issues:\nin the case of an enum switch expression that covers all known constants, a default clause is inserted by the compiler to indicate that the enum definition has changed between compile-time and runtime.\nNow, this is not enough for us as we would like to perform our switch on the object type.\nAlso greet the pattern matching for switch To achieve this, we need to rely on JEP 406: Pattern Matching for switch (Preview) from JDK 17.\nTo give a rough idea of what this is about, you can think of it as if JEP 394: Pattern Matching for instanceof and JEP 361: Switch Expressions had a baby.\nSo, let’s take a look at our code with this amazing feature:\npublic static Expression simplify(Expression e) { return switch (e) { case Variable v -\u0026gt; v; case And and -\u0026gt; simplify(and.left()).and(simplify(and.right())); case Or or -\u0026gt; simplify(or.left()).or(simplify(or.right())); case Not not -\u0026gt; simplifyNot(not); default -\u0026gt; throw new UnsupportedOperationException(\u0026#34;Expression type not supported \u0026#34; + e.getClass()); }; } public static Expression simplifyNot(Not not) { return switch (not.expression()) { case Variable v -\u0026gt; not; case And and -\u0026gt; simplify(and.left().not()).or(simplify(and.right().not())); case Or or -\u0026gt; simplify(or.left().not()).and(simplify(or.right().not())); case Not nonot -\u0026gt; simplify(nonot.expression()); default -\u0026gt; throw new UnsupportedOperationException(\u0026#34;Expression type not supported \u0026#34; + not.getClass()); }; } Wait, why do we have to include this default clause? Remember when we said that switch expressions must be complete? Not doing so would result in a compiler error: the switch expression does not cover all possible input values.\nThat’s not very nice to have. Especially as we know, that they are only four implementations of the Expression interface. However, the compiler does not know it.\nBut there is a way to let the compiler know and to enforce it. This is thanks to the JEP 409: Sealed Classes from JDK 17. So much goodness in this JDK 17.\nWe can now explicitly say that Expression is one of Variable, And, Or or Not and nothing else.\nLet’s update the interface declaration to represent it:\npublic sealed interface Expression permits Variable, And, Or, Not { // ... } If we now try to declare a Xor expression which can also be expressed as: (A OR B) AND (NOT(A) OR NOT(B)) we will get the following compilation error:\nclass is not allowed to extend sealed class: fr.loicrouchon.novisits.Expression (as it is not listed in its permits clause) Going back to the switch we can now express it without the need for the default clause:\npublic static Expression simplify(Expression e) { return switch (e) { case Variable v -\u0026gt; v; case And and -\u0026gt; simplify(and.left()).and(simplify(and.right())); case Or or -\u0026gt; simplify(or.left()).or(simplify(or.right())); case Not not -\u0026gt; simplifyNot(not); }; } public static Expression simplifyNot(Not not) { return switch (not.expression()) { case Variable v -\u0026gt; not; case And and -\u0026gt; simplify(and.left().not()).or(simplify(and.right().not())); case Or or -\u0026gt; simplify(or.left().not()).and(simplify(or.right().not())); case Not nonot -\u0026gt; simplify(nonot.expression()); }; } This is it, we reached our goal thanks to JEP 406: Pattern Matching for switch (Preview).\nThe guard pattern We did not need this feature in our little example, but this JEP also defines the notion of guard patterns. With guard patterns, an additional boolean expression can be added next to the type pattern.\nOne can replace the following code:\nstatic void test(Object o) { switch (o) { case String s: if (s.length() == 1) { ... } else { ... } break; ... } } by:\nstatic void test(Object o) { switch (o) { case String s \u0026amp;\u0026amp; (s.length() == 1) -\u0026gt; ... case String s -\u0026gt; ... ... } } About instanceof/casting and switch on types It used to be strongly discouraged to perform instanceof/cast operations in if statements and conceptually out of reach for switch statements.\nWhy do we start to do so now?\nI haven’t played enough with instanceof and casting in my career to have an answer to this part. I was most of the time creative enough to avoid being in such a situation and I feel I would need to unlearn first to be able to provide a decent answer here.\nHowever, when it comes to the switch, it feels like a very handy replacement for the visitor design pattern.\nThey were two major complaints against the switch:\nimplementation far from the type and scattered across the codebase\nThe visitor pattern has the same issue, so wherever the visitor pattern was good for, the pattern matching for switch is also good enough.\ncompleteness: When a new type/constant is added, the if/else/switch will not complain at compilation time and issues will arise at runtime.\nHere, the switch expression solves the issue by checking for completeness. If a new type/case is added, a compilation error will arise. Unless a manual default clause has been written. So when writing switch-expressions, be aware of the hidden cost of the default clause and try to favor the usage of sealed types.\nLooking at the Java pattern future This article covered the yet-to-be-released JEP 406: Pattern Matching for switch (Preview) and how it enables the Java language to express more, more clearly.\nThe code can be found on Github.\nBut can we look even further? In JDK 18 and beyond for example? As of June 20th, 2021, the list of JEP that will land in JDK 18 is not yet known. But we do know about one candidate which is JEP 405: Record Patterns \u0026amp; Array Patterns (Preview).\nThis will bring deconstruction patterns for both Arrays and Records.\nWe could then imagine writing things like:\npublic static Expression simplify(Expression e) { return switch (e) { case Variable v -\u0026gt; v; case And(var left, var right) -\u0026gt; simplify(left).and(simplify(right)); case Or(var left, var right) -\u0026gt; simplify(left).or(simplify(right)); case Not(var expression) -\u0026gt; simplifyNot(expression); }; } public static Expression simplifyNot(Expression e) { return switch (e) { case Variable v -\u0026gt; not(e); case And(var left, var right) -\u0026gt; simplify(not(left)).or(simplify(not(right))); case Or(var left, var right) -\u0026gt; simplify(not(left)).and(simplify(not(right))); case Not(var expression) -\u0026gt; simplify(expression); }; } Or even, all in one switch:\npublic static Expression simplify(Expression e) { return switch (e) { case Variable v -\u0026gt; e; case And(var left, var right) -\u0026gt; simplify(left).and(simplify(right)); case Or(var left, var right) -\u0026gt; simplify(left).or(simplify(right)); case Not(Variable v) -\u0026gt; e; case Not(And(var left, var right) -\u0026gt; simplify(not(left)).or(simplify(not(right))); case Not(Or(var left, var right) -\u0026gt; simplify(not(left)).and(simplify(not(right))); case Not(Not(var expression)) -\u0026gt; simplify(expression); }; } The JEP even says the following:\nToday, to express ad-hoc polymorphic calculations like this we would use the cumbersome visitor pattern. In the future, using pattern matching will lead to code that is transparent and straightforward\nKeep in mind, this last part regarding JEP 405: Record Patterns \u0026amp; Array Patterns (Preview) is based on my current understanding of the JEP and that the JEP will evolve before making it into the JDK.\nTo conclude I’d like to say this:\nWhat seemed to be unrelated JEPs in the beginning, switch expressions, records, pattern matching (instance of, switch), deconstruction patterns, is slowly but surely converging toward a highly consistent and well-thought design which I can’t stop being amazed at.\n","permalink":"https://loicrouchon.com/posts/pattern-matching-for-switch-in-java/","summary":"Old problems, new ways of solving it? If all you have is a hammer, everything looks like a nail.\nThis sentence summarizes pretty much how I felt about the idiomatic polymorphism approach in Java to most problems until recently. Polymorphism is just a tool and as for any tool, it excels at some tasks and performs poorly at others.\nSo what are the tasks where polymorphism does a good job at?","title":"Pattern matching for switch in Java (JDK 17)"},{"content":" Hello, my name is Loïc Rouchon as you probably have guessed. I’m a software engineer working mostly with JVM-based technologies. I’m passionate about software engineering in the broad meaning of the term. This includes languages, designs, frameworks, tooling, methodologies, and so on. Everything that helps to write better software in a faster way.\nIn general, I like to understand how things work. Therefore, I have also strong interests in sciences and in particular in theoretical physics.\nIf you want to know more, you can follow me on the following social media.\n","permalink":"https://loicrouchon.com/about/","summary":"Hello, my name is Loïc Rouchon as you probably have guessed. I’m a software engineer working mostly with JVM-based technologies. I’m passionate about software engineering in the broad meaning of the term. This includes languages, designs, frameworks, tooling, methodologies, and so on. Everything that helps to write better software in a faster way.\nIn general, I like to understand how things work. Therefore, I have also strong interests in sciences and in particular in theoretical physics.","title":"About me!"}]