الاتصال: المشغّل وURL وبيانات الاعتماد من متغيرات البيئة
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Last lesson drew the map. Now you dial the library. To open a connection you need three things: the address (a JDBC URL), a user name, and a password. The first two are harmless. The password is a secret, and the single most common beginner mistake is to type it straight into the Java file and commit it to Git.
In this lesson you will open a real Connection, read all three values from environment variables, and read the errors that appear when something is wrong. You will also learn what to do if a password has already slipped into Git.
You need a running PostgreSQL with a library database and a user that owns it (BEG-051), and the Maven project with the driver from lesson 1.
Learning Objectives
By the end of this lesson you will be able to:
- Write a PostgreSQL JDBC URL and explain each part.
- Open a
ConnectionwithDriverManager.getConnection(url, user, password). - Read credentials with
System.getenvand fail clearly when one is missing. - Set environment variables for a run, from a terminal or an IDE run configuration.
- Recognise the three common connection failures from their messages.
- Check that a password is not in your code or your Git history.
Written Explanation
The JDBC URL
A JDBC URL tells DriverManager which driver to use and where the database lives. For PostgreSQL the documented form is:
jdbc:postgresql://host:port/database| Part | Meaning | Example |
|---|---|---|
jdbc:postgresql: | Protocol prefix; selects the PostgreSQL driver | fixed |
host | Machine running PostgreSQL | localhost |
port | TCP port; the default is 5432 | 5432 |
database | Which database to open | library |
The driver documentation says the host defaults to localhost and the port to 5432 if you leave them out. We will write them out, because explicit is easier to debug. A complete URL for this module: jdbc:postgresql://localhost:5432/library.
Why credentials do not belong in code
Anything in your source ends up in Git, and Git remembers every commit. A password committed once is visible to everyone who can read the repository, including after you delete the line, because the old commit still contains it. Source code is also copied to laptops, build servers, and code-review tools.
The fix is simple: the code never contains the secret. It reads it from outside, when it starts. The first outside place we use is environment variables: named values that the operating system hands to a process when it starts. The JDK reads one with System.getenv("NAME"). This is the same idea as keeping your house key out of the blueprint of the house.
Two details from the JDK documentation matter:
System.getenvreturnsnullif the variable is not set. Always check.- Whether the name is case-sensitive depends on the operating system (typically yes on Unix-like systems, typically no on Windows). Use the same exact upper-case name everywhere.
For this module we use three names, as the course specifies: DB_URL, DB_USER, and DB_PASSWORD. Environment variables are a good start, not a perfect vault: other processes of the same user may be able to see them. Production teams use secret managers; the principle (no secrets in the repository) is identical.
Connecting: one method call
Connection c = DriverManager.getConnection(url, user, password);It returns an open Connection, or throws SQLException. A Connection holds a real network session and a server-side process, so it is a resource to close: put it in a try-with-resources header (BEG-029).
A small helper that reads the environment
We will reuse one helper in every later lesson:
package lib;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public final class Db {
private Db() { }
private static String require(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException("Missing environment variable: " + name);
}
return value;
}
public static Connection open() throws SQLException {
return DriverManager.getConnection(
require("DB_URL"), require("DB_USER"), require("DB_PASSWORD"));
}
}require turns a silent null into a clear message. Failing early with the name of the missing variable saves a lot of guessing.
Setting the variables
How you set variables depends on your system, so treat these as typical examples and check your own tools:
- Bash-style terminal:
export DB_URL=jdbc:postgresql://localhost:5432/library, then the same forDB_USERandDB_PASSWORD. - PowerShell:
$env:DB_URL = "jdbc:postgresql://localhost:5432/library", and so on. - IDE: most IDEs have a run configuration with an "environment variables" field. Menu names differ between IDEs and versions.
Variables set this way last for that terminal session or run configuration. Keep the actual values out of the repository. If you keep a helper file such as env.sh for convenience, list it in .gitignore so Git never picks it up. The Git documentation notes that .gitignore does not affect files that are already tracked, so add the ignore rule before the first commit.
Reading the three common failures
When the connection fails you get an SQLException. Its getSQLState() gives a five-character code and getMessage() a description. These three cover most of what you will see:
| Cause | What we saw | SQLState |
|---|---|---|
| Server not reachable (down, wrong port) | Connection to localhost:55999 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections. | 08001 |
| Wrong password | FATAL: password authentication failed for user "lib" | 28P01 |
| Database name does not exist | FATAL: database "nolib" does not exist | 3D000 |
Read the message first. It nearly always names the problem.
Examples
Example 1: connect and print (simple)
package lib;
import java.sql.Connection;
import java.sql.SQLException;
public class ConnectDemo {
public static void main(String[] args) {
try (Connection c = Db.open()) {
System.out.println("Connected: " + !c.isClosed());
System.out.println("Product: " + c.getMetaData().getDatabaseProductName());
System.out.println("Valid: " + c.isValid(2));
} catch (SQLException e) {
System.out.println("SQLState " + e.getSQLState() + ": " + e.getMessage());
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}With the three variables set correctly:
Connected: true
Product: PostgreSQL
Valid: trueisValid(2) asks the server to confirm the connection works, waiting at most 2 seconds. After the try block ends, the connection is closed automatically.
Example 2: failing clearly (practical)
Run the same program three times with something deliberately wrong. First with DB_URL not set at all:
Missing environment variable: DB_URLThen with the wrong password (DB_PASSWORD=nope):
SQLState 28P01: FATAL: password authentication failed for user "lib"Then with a database name that does not exist (.../nolib):
SQLState 3D000: FATAL: database "nolib" does not existEach message points at exactly one thing to fix. (In a local test setup, PostgreSQL may be configured to trust local connections without checking the password; our scratch server first accepted nope until we switched it to password authentication. If a wrong password "works" for you, that setting is why, not a bug in your Java.)
Example 3: proving no secret is in Git (realistic)
You can search the current files and the whole history. This was run in a scratch repository whose first commit contained a hard-coded password and whose second commit replaced it with System.getenv("DB_PASSWORD"):
$ git grep -n lib_pw
(no output: the current files are clean)
$ git log -p -S lib_pw --oneline
9468442 two
-String p = "lib_pw";
+String p = System.getenv("DB_PASSWORD");
2f9802d one
new file mode 100644The first command found nothing, yet the history search (-S lists commits where the number of occurrences of that text changed) shows the secret in the old commit, and a deleted line is still readable by anyone who can read the history. The lesson: if a real password was ever committed, change the password on the database. Rewriting history is a separate, more advanced step and still does not help for copies others already hold.
Backend Insight
Backend services run in many places: your laptop, a test server, production. The URL and the password differ in each one, but the compiled code should be identical. Reading configuration from the environment is how one program runs everywhere without edits. You will see this again when Spring and containers read their settings, so the habit you build now carries straight over.
Also remember that a database connection is expensive and limited: each one uses memory on the server, and PostgreSQL has a maximum number of them. Opening a connection for every little step does not scale, which is why later modules introduce connection pools. For now, open a connection, do the work, close it.
Common Mistakes
Mistake 1: A password in the source file
DriverManager.getConnection(url, "lib", "secret123"); // neverIt is committed the moment you commit the file. Read it from the environment.
Mistake 2: Committing a "local config" file with the secret
Replacing a constant with a config.properties file helps only if that file is not committed. Add it to .gitignore first, and commit an example file with fake values instead.
Mistake 3: Printing the password while debugging
A line like System.out.println(url + user + password) puts the secret into logs. Log the URL and user if you must, never the password.
Mistake 4: Forgetting to close the connection
A Connection outside try-with-resources stays open if an exception happens. Repeat this in a loop and the server runs out of connections. Always open in the try header.
Exercises
Level A: Foundation
- Write the JDBC URL for a PostgreSQL server on the same machine, default port, database
library. - What does
System.getenv("DB_PASSWORD")return when the variable is not set? - Why is
getConnectionplaced in a try-with-resources header?
Level B: Application
- Set the three variables in your terminal, add the
Dbhelper, and runConnectDemo. Copy your output. - Break each variable in turn (wrong port, wrong password, wrong database). Record the SQLState and one phrase from each message.
- Create a scratch Git repository. Commit a file containing a fake password, replace it with an environment-variable read in a second commit, and then use the two search commands from Example 3 to show what each one finds.
Level C: Challenge
- Extend
Dbso that a missingDB_PORTis not an error: use5432as the default, and build the URL fromDB_HOST,DB_PORT, andDB_NAMEinstead ofDB_URL. Compare the two designs: which is easier for a teammate to configure? - Write a short checklist (five lines) a teammate can follow before every commit to avoid leaking credentials.
أظهر الحلول
A1. jdbc:postgresql://localhost:5432/library (the short form jdbc:postgresql:library also works, because the host and port have defaults).
A2. null. That is why the helper checks for null or blank values.
A3. The connection is a resource that must be closed even when an exception is thrown. Try-with-resources guarantees close() runs.
B1. Expected output: Connected: true, Product: PostgreSQL, Valid: true.
B2. Wrong port: 08001, "refused" and "postmaster". Wrong password: 28P01, "password authentication failed". Wrong database: 3D000, "does not exist". (If the wrong password is accepted, your server trusts local connections: see Example 2.)
B3. git grep -n <password> prints nothing, because the working files no longer contain it. git log -p -S<password> lists both commits, showing the line in the first commit and its removal in the second.
C1. Sketch: String host = System.getenv().getOrDefault("DB_HOST", "localhost");, likewise DB_PORT with "5432", and DB_NAME required; then String url = "jdbc:postgresql://" + host + ":" + port + "/" + name;. Building a URL from parts is fine here, because the pieces come from configuration you control and never from a user typing into your application; lesson 4 explains why that distinction matters for SQL text. A single DB_URL is simpler to set; separate parts are easier to override one at a time.
C2. Example: (1) No passwords or keys in changed files. (2) git status shows no env or .properties files with secrets. (3) Secret files are in .gitignore. (4) Example config files hold fake values. (5) If a secret was ever committed, change it at once.
Mini Challenge
Create a README section (as a file in your scratch project) titled "Running locally" that lists the three environment variables the Library application needs, with fake example values, and one command per operating system you use to set them. Then add your real-values file name to .gitignore.
Check your work: a classmate can run the program from your README alone, and git status never shows your real password.
Summary & Next Lesson
Key takeaways
- A PostgreSQL URL has the form
jdbc:postgresql://host:port/database, with defaultslocalhostand 5432. DriverManager.getConnection(url, user, password)returns aConnectionor throwsSQLException.- Read credentials from environment variables (
DB_URL,DB_USER,DB_PASSWORD) and fail clearly if one is missing. - Never commit a secret: Git history keeps it, so change any password that leaked.
- Open the
Connectionin try-with-resources so it is always closed.
Next lesson
The next lesson, "Querying and mapping rows to objects", uses this connection to run SELECT statements with PreparedStatement and turn each row into a Java object.
Self-check
- What are the four parts of
jdbc:postgresql://localhost:5432/library? - Which three environment variable names does this module use?
- What does
SQLState28P01tell you in our run? - Why is deleting a committed password from the file not enough?
- Why must a
Connectionbe closed?
أظهر الإجابات
- Protocol prefix
jdbc:postgresql:, hostlocalhost, port5432, databaselibrary. DB_URL,DB_USER,DB_PASSWORD.- Password authentication failed for the user.
- Git history still contains the old commit, so the secret stays readable; the password must be changed.
- It holds a network session and server resources; unclosed connections pile up and the server runs out of them.
References
- PostgreSQL JDBC Driver, Initializing the Driver. It supports the URL forms, the defaults (
localhostand port 5432), and passing user and password togetConnection. - Oracle, System (Java SE 21 API). It states that
getenvreturnsnullfor an undefined variable and that name case-sensitivity depends on the operating system. - Git, gitignore documentation. It supports the statement that
.gitignoredoes not affect files already tracked. - Git, git-log documentation. The page only mentions the
-Spickaxe option in passing, so the description of-Shere is from general Git knowledge, confirmed by running it in a scratch repository. - The error messages and SQLState codes in the table were copied from real runs against PostgreSQL 18 with driver 42.7.8. The remark that other processes of the same user may see environment variables is general knowledge that was not checked against a source.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.