انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-055-L02

الاتصال: المشغّل وURL وبيانات الاعتماد من متغيرات البيئة

  • الدرس 2 من 7
  • 10 دقيقة قراءة
  • مبتدئ
في هذا الدرس 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 Connection with DriverManager.getConnection(url, user, password).
  • Read credentials with System.getenv and 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:

text
jdbc:postgresql://host:port/database
PartMeaningExample
jdbc:postgresql:Protocol prefix; selects the PostgreSQL driverfixed
hostMachine running PostgreSQLlocalhost
portTCP port; the default is 54325432
databaseWhich database to openlibrary

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.getenv returns null if 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

java
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:

java
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 for DB_USER and DB_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:

CauseWhat we sawSQLState
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 passwordFATAL: password authentication failed for user "lib"28P01
Database name does not existFATAL: database "nolib" does not exist3D000

Read the message first. It nearly always names the problem.

Examples

Example 1: connect and print (simple)

java
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:

text
Connected: true
Product: PostgreSQL
Valid: true

isValid(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:

text
Missing environment variable: DB_URL

Then with the wrong password (DB_PASSWORD=nope):

text
SQLState 28P01: FATAL: password authentication failed for user "lib"

Then with a database name that does not exist (.../nolib):

text
SQLState 3D000: FATAL: database "nolib" does not exist

Each 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"):

text
$ 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 100644

The 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

java
DriverManager.getConnection(url, "lib", "secret123"); // never

It 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

  1. Write the JDBC URL for a PostgreSQL server on the same machine, default port, database library.
  2. What does System.getenv("DB_PASSWORD") return when the variable is not set?
  3. Why is getConnection placed in a try-with-resources header?

Level B: Application

  1. Set the three variables in your terminal, add the Db helper, and run ConnectDemo. Copy your output.
  2. Break each variable in turn (wrong port, wrong password, wrong database). Record the SQLState and one phrase from each message.
  3. 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

  1. Extend Db so that a missing DB_PORT is not an error: use 5432 as the default, and build the URL from DB_HOST, DB_PORT, and DB_NAME instead of DB_URL. Compare the two designs: which is easier for a teammate to configure?
  2. 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 defaults localhost and 5432.
  • DriverManager.getConnection(url, user, password) returns a Connection or throws SQLException.
  • 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 Connection in 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

  1. What are the four parts of jdbc:postgresql://localhost:5432/library?
  2. Which three environment variable names does this module use?
  3. What does SQLState 28P01 tell you in our run?
  4. Why is deleting a committed password from the file not enough?
  5. Why must a Connection be closed?
أظهر الإجابات
  1. Protocol prefix jdbc:postgresql:, host localhost, port 5432, database library.
  2. DB_URL, DB_USER, DB_PASSWORD.
  3. Password authentication failed for the user.
  4. Git history still contains the old commit, so the secret stays readable; the password must be changed.
  5. 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 (localhost and port 5432), and passing user and password to getConnection.
  • Oracle, System (Java SE 21 API). It states that getenv returns null for an undefined variable and that name case-sensitivity depends on the operating system.
  • Git, gitignore documentation. It supports the statement that .gitignore does not affect files already tracked.
  • Git, git-log documentation. The page only mentions the -S pickaxe option in passing, so the description of -S here 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.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.