In production, your deployable projects, by their nature, run in separate processes.
In development, you are running from a single REPL process, which can lead to classpath issues you will never encounter in production.
Profiles allow you to work on all your projects from the development
project without classpath issues.
Tip
|
You likely don’t need profiles if your workspace does not re-specify one interface. |
In our ongoing tutorial, we last left off exploring testing.
If you’ve been following along, your development
project currently mirrors the command-line
project:
The cli
base references the user
component in both projects.
Let’s pretend that we have discovered performance problems in the user
component and have learned that distributing the load by delegating to a new service should solve the problem:
The cli
base now references a new user-remote
component, which calls a new user-api
base in a new user-service
project, which references our original user
component.
The production environment looks good, but how about the development
environment?
We have a problem.
The user
and user-remote
components both specify the user
interface, resulting in two se.example.user.interface
namespaces on the classpath.
Duplicate items on the classpath will confuse classloaders and IDEs.
Note
|
Classloader issues are only a
|
The poly
solution is to use profiles:
We can add default
and remote
profiles to avoid duplication on the classpath, allowing us to develop all our code and projects from a single place without issues.
If no other profiles are selected, poly
automatically merges the default
profile into the development
project.
The name default
is specified by :default-profile-name
in workspace.edn.
First, we need some mechanism for the command-line
tool to communicate over the wire with the user-service
.
After some searching, we found the slacker library.
It supports simple remote procedure calls.
Perfect for our tutorial.
Here’s a checklist to implement our design:
1. Create the user-api
base
2. Create the user-remote
component
3. Switch from user
to user-remote
in the command-line
project
4. Create the user-service
project
5. Build the user-service
a. Create the user-api
base
b. Add the slacker
library to the user-api
base
c. Add user-api
base to ./deps.edn
:
d. Implement the server for the user-api
base
Edit user-api
base deps.edn
:
{:paths ["src" "resources"]
:deps {slacker/slacker {:mvn/version "0.17.0"}} ;; (1)
:aliases {:test {:extra-paths ["test"]
:extra-deps {}}}}
-
Add
slacker
library dependency for server-side communication support
Adjust ./deps.edn
like so:
:aliases {:dev {:extra-deps [...
poly/user-api {:local/root "bases/user-api"} ;; (1)
...]
:test {:extra-paths [...
"bases/user-api/test" ;; (2)
-
Add
user-api
source dep under:dev
alias -
Add
user-api
test path under:test
alias
Create the api
namespace in the user-api
base:
example
├── bases
│ └── user-api
│ └── src
│ ├── se.example.user_api.api.clj # (1)
│ └── se.example.user_api.core.clj
-
Create the new
api.clj
file
Set the content of api.clj
to:
(ns se.example.user-api.api
(:require [se.example.user.interface :as user]))
(defn hello-remote [name]
(user/hello (str name " - from the server")))
Update core.clj
to:
(ns se.example.user-api.core
(:require [se.example.user-api.api]
[slacker.server :as server])
(:gen-class))
(defn -main [& args]
(server/start-slacker-server [(the-ns 'se.example.user-api.api)] 2104)
(println "server started: http://127.0.0.1:2104"))
a. Create the user-remote
component
b. Add the slacker
library to user-remote
component
c. Remove the user
component from ./deps.edn
:
d. Add the default
and remote
profiles to ./deps.edn
:
e. Activate the remote
profile in your IDE
f. Implement user-remote
g. Activate the default
profile in your IDE
Edit user-remote
component deps.edn
:
{:paths ["src" "resources"]
:deps {slacker/slacker {:mvn/version "0.17.0"}} ;; (1)
:aliases {:test {:extra-paths ["test"]
:extra-deps {}}}}
-
Add
slacker
lib dependency for client-side communication support
{:aliases {:dev {...
:extra-deps {poly/user {:local/root "components/user"} ;; (1)
poly/cli {:local/root "bases/cli"}
poly/user-api {:local/root "bases/user-api"}
org.clojure/clojure {:mvn/version "1.11.1"}}}
:test {:extra-paths ["components/user/test" ;; (2)
"bases/cli/test"
"projects/command-line/test"
"bases/user-api/test"]}
-
Delete
poly/user {:local/root "components/user"}
-
Delete
"components/user/test"
:aliases {...
:+default {:extra-deps {poly/user {:local/root "components/user"}} ;; (1)
:extra-paths ["components/user/test"]}
:+remote {:extra-deps {poly/user-remote {:local/root "components/user-remote"}} ;; (2)
:extra-paths ["components/user-remote/test"]}
-
Respecify your deleted
user
component under thedefault
profile alias -
Specify your new
user-remote
component under theremote
profile alias
Notice that profile aliases are prefixed with a +
.
Note
|
At the time of this writing, we only have instructions for Cursive. |
Create the core
namespace in the user-remote
component:
example
├── components
│ └── user-remote
│ └── src
│ ├── se.example.user.core.clj ;; (1)
│ └── se.example.user.interface.clj
-
Create new
core.clj
file
Set core.clj
content to:
(ns se.example.user.core
(:require [slacker.client :as client]))
(declare hello-remote)
(defn hello [name]
(let [connection (client/slackerc "localhost:2104")
_ (client/defn-remote connection se.example.user-api.api/hello-remote)]
(hello-remote name)))
And update the interface.clj
content to:
(ns se.example.user.interface
(:require [se.example.user.core :as core]))
(defn hello [name]
(core/hello name))
Note
|
At the time of this writing, we only have instructions for Cursive users. |
Tip
|
Cursive users: Edit the REPL configuration: …and add the We had you add We now need to include the We have segregated the two components that specify a For the changes to take effect, you need to restart the REPL. Normally, a REPL restart is not required, but when adding profiles, it’s necessary. |
Make the following changes to the command-line
project deps.edn
:
{:deps {poly/user {:local/root "../../components/user-remote"} ;; (1)
poly/cli {:local/root "../../bases/cli"}
org.clojure/clojure {:mvn/version "1.11.1"}
org.slf4j/slf4j-nop {:mvn/version "2.0.9"}} ;; (2)
:aliases {:test {:extra-paths ["test"]
:extra-deps {}}
:uberjar {:main se.example.cli.core}}}
-
Rename
components/user
tocomponents/user-remote
. It’s okay to leavepoly/user
as is; it’s unique within the project. -
Add logging library (slacker lib does some logging that we’ll ignore)
a. Create the user-service
project:
b. Configure the user-service
c. Add a poly
alias for the user-service
Set the user-service
project deps.edn
content to:
{:deps {poly/user {:local/root "../../components/user"} ;; (1)
poly/user-api {:local/root "../../bases/user-api"} ;; (2)
org.clojure/clojure {:mvn/version "1.11.1"}
org.slf4j/slf4j-nop {:mvn/version "2.0.9"}} ;; (3)
:aliases {:test {:extra-paths []
:extra-deps {}}
:uberjar {:main se.example.user-api.core}}} ;; (4)
-
Add
user
component -
Add
user-api
base -
Add logging library (slacker lib does some logging that we’ll ignore)
-
Specify main for uberjar artifact
Phew, that should be it! Now, let’s test if it works.
From a separate terminal, launch the user-service
:
cd projects/user-service/target
java -jar user-service.jar
You should see the following output:
server started: http://127.0.0.1:2104
Tip
|
Cursive users:
Now that you have a running service, you can test if you can call it from the REPL.
You activated the remote profile in your IDE earlier, which made the Note that this only instructs the IDE to treat …but it doesn’t automatically load its source code into the REPL! You can verify this by adding this code to (ns dev.lisa
(:require [se.example.user.interface :as user]))
(user/hello "Lisa") …and if you execute the "Hello Lisa!!" Remember, you set your REPL configuration to include the Let’s create a REPL configuration that includes the remote profile: This REPL will use the But let’s continue with the REPL that is already running and see if we can switch to Open the If you execute the
|
Now, let’s continue with our example.
From another terminal (not the one from which you started the user-service
) from your example
workspace root dir:
cd projects/command-line/target
java -jar command-line.jar Lisa
You should see:
Hello Lisa - from the server!!
If your output matches, congratulations, you’ve successfully exercised poly
profiles!
Tip
|
You can find the complete tutorial code here. |
Now execute the info command (+
deactivates all profiles, and makes the default
profile visible):
cd ../../.. # (1)
poly info +
-
Navigate back to the workspace root dir
…and compare the info
output with our target design:
Great! Reality now matches our plan!
Notice that profile flags only include the st
flags and never the x
flag.
Whether or not to run tests is not tied to profiles.
Tip
|
This example was quite simple, but if your project is more complicated, you may want to manage state during development with a tool like Mount, Component, or Integrant.
You could also create your own helper functions in your development project namespace ( |
By default, the default
profile is active:
poly info
Notice:
-
default
is listed foractive profiles
-
the
dev
project column:-
includes the
user
brick (which is in thedefault
profile) -
doesn’t include the
user-remote
brick (which is in theremote
profile)
-
-
columns for the inactive
remote
profile are shown
Note
|
Profiles can also contain dependencies and paths to projects, but we’ve done no such thing in our example; therefore, you’ll see all profile flags as -- in the project section.
|
You can override the default profile by specifying a profile:
poly info +remote
Notice:
-
remote
is listed foractive profiles
-
that the
dev
project column:-
doesn’t include the
user
brick (which is in thedefault
profile) -
includes the
user-remote
brick (which is in theremote
profile)
-
-
columns for the inactive
default
profile are shown
You can specify more than one profile:
poly info +default +remote
Notice:
-
default
andremote
are listed asactive profiles
-
that the
dev
project column:-
includes the
user
brick (which is in thedefault
profile) -
includes the
user-remote
brick (which is in theremote
profile)
-
-
no inactive profile columns are shown
-
poly
tells us that it does not like that we included bothuser
anduser-remote
in thedevelopment
project
Let’s see how many lines of code we have by specifying the :loc
argument:
poly info :loc
Under bricks, each project column tallies the lines of code for its bricks src
code.
The loc
column counts the number of lines of codes for src
directories, while (t)
counts for the test
directories.
Our tutorial example
is small, but your real-world systems will likely reach thousands of lines of code.
When that happens, you may want to reconfigure the thousand delimiter, which is ,
by default.