Package Java Classes Into a JAR the Right Way

Package Java Classes Into a JAR the Right Way
TakeawayDetail
Run `jar` from the root of the compiled class hierarchyThe single most common fix for `ClassNotFoundException` is executing `jar cf myapp.jar com/` from the `bin` or `target` directory, not from inside the package folder.
Use `jar cfe` to set the entry point in one commandThe `e` flag eliminates manual manifest editing: `jar cfe app.jar com.example.Main com/` sets `Main-Class` automatically.
Leverage the `-C` flag to include files from outside your working directory`jar cf app.jar -C bin/ .` temporarily changes directory to `bin/`, preserving package structure without moving files.
Let Maven or Gradle handle package structure automaticallyBoth tools derive the JAR layout from your source directories (`target/classes` or `build/classes/java/main`), removing manual error.
Verify structure with `jar tf` before shippingRun `jar tf myapp.jar` to list all entries; confirm `com/example/MyClass.class` exists, not just `MyClass.class`.
Use a fat JAR (Maven Shade or Gradle Shadow) for single-file deploymentThese plugins bundle dependencies into one archive, avoiding classpath issues at runtime.
Place resources at the JAR root or mirror the class path`.properties` and `.xml` files in `src/main/resources` are copied to the JAR root automatically by Maven/Gradle.
For JPMS modules, put `module-info.class` at the JAR root (as of July 2026)Java 9+ module descriptors must sit outside any package directory, and the JAR must go on the module path, not the classpath.

The JAR file is a ZIP archive with a strict contract: the directory tree inside must exactly mirror your Java package declarations. Most tutorials skip this invariant, leading to the `ClassNotFoundException` that wastes hours on Stack Overflow. This guide builds from that rule: first the manual command-line workflow, then build tool automation, then executable entry points, dependency strategies, and verification.

Recent changes in the Java ecosystem—wider adoption of JPMS modules and the dominance of Maven/Gradle—have made manual JAR creation less common, but the underlying principle remains unchanged. Understanding the directory-to-package mapping is the skill that separates a working JAR from a runtime failure, regardless of whether you use `jar cf` or a build tool.

The Package Structure Trap

You wrote perfect Java code, compiled without errors, ran `jar cf myapp.jar *.class`, and got a `ClassNotFoundException` — welcome to the single most common packaging mistake that wastes hours on Stack Overflow, and it is entirely preventable. The error is not in your code. It is in your directory mapping. A JAR is just a ZIP file with a contract: the directory tree inside must mirror your package declarations exactly. If your source declares `package com.example.myapp;`, the JAR must contain a directory `com/example/myapp/` with your `.class` files inside it, not a flat list of files at the root. The JVM looks for the path, not the file.

Most tutorials show you how to make a JAR, but they never explain why your package structure breaks at runtime. The real skill is understanding the directory-to-package mapping, not just memorizing `jar cf` flags. When you run `jar cf myapp.jar *.class` from inside your `target/classes/com/example/myapp/` directory, you create a JAR with no package structure. The JVM cannot find `com.example.myapp.Main` because the directory `com/example/myapp/` doesn't exist in the archive. It sees only `Main.class` at the root. This works only if you have no package structure, which means you are writing code that will never scale past a single file.

According to Oracle's official tutorial, you must run the `jar` command from the root of the compiled class hierarchy (e.g., `target/classes/`), not from within the package folder itself. The correct command from the root is `jar cf myapp.jar com/`. This captures the entire `com/` directory tree, preserving every package subdirectory. Field reports from r/java confirm this trap is widespread: "I spent three hours debugging a JAR that worked in IntelliJ but failed on the server — turns out I was zipping from the wrong directory." The IDE hides the directory structure from you during execution, but the command line does not.

To verify your structure before distribution, use `jar tf myapp.jar`. If you see `com/example/myapp/Main.class` in the output, you are good. If you see just `Main.class`, you have already failed. This verification step is non-negotiable for any production artifact.

The `-C` option in the `jar` command temporarily changes directories during packaging, allowing you to include files from a different directory while preserving the desired package structure. This is the escape hatch for complex build layouts. Instead of moving files around, you can tell `jar` to change into a directory, include files, and change back. This keeps your source tree clean and your build reproducible.

Build tools like Maven and Gradle handle package structure automatically. Maven uses the `maven-jar-plugin` (configured in `pom.xml`) to automatically create a JAR from the `target/classes` directory, preserving the package structure defined by the source code. Gradle uses the `jar` task (part of the Java plugin) to create a JAR from the `build/classes/java/main` directory, with the package structure automatically derived from the source set. If you are using a build tool, stop using the `jar` command manually. Let the machine do it.

For resource files (e.g., `.properties`, `.xml`, images), place them in the same directory structure as the class files (e.g., `src/main/resources` in Maven) so they are copied to the root of the JAR. This ensures your application can find its configuration at runtime. The `jar` command uses the `c` flag to create a new archive and the `f` flag to specify the output file name. The `e` flag in the `jar` command (`jar cfe`) sets the application entry point (Main-Class) directly, without needing to edit the manifest file manually.

Command Directory Context Resulting JAR Structure Runtime Status
`jar cf app.jar *.class` Inside `com/example/` `Main.class` (flat) ClassNotFoundException
`jar cf app.jar com/` Root of `target/classes/` `com/example/Main.class` Success
`jar cfe app.jar com.example.Main com/` Root of `target/classes/` `com/example/Main.class` + Manifest Success (Executable)
`mvn package` Project Root `com/example/Main.class` Success (Automated)
`gradle jar` Project Root `com/example/Main.class` Success (Automated)

The lesson is simple: your package declaration is a contract with the JVM. Break the directory structure, and the contract is void. Always verify your JAR contents with `jar tf` before shipping. If you are using a build tool, trust its default configuration. If you are using the command line, navigate to the root of your class hierarchy first. This single habit will save you hours of debugging.

The `-C` Escape Hatch

ls. It is the escape hatch for developers who refuse to restructure their build output to match the command line. Most tutorials tell you to `cd` into your class directory before running the command. That works until your build system outputs classes in a deeply nested path like `target/classes/java/main`. You do not need to navigate there. You can tell the `jar` tool to change directories temporarily, include the files, and change back. This keeps your source tree clean and your build reproducible.

The syntax is `jar cf archive.jar -C directory .`. The `-C` flag changes the working directory for the subsequent arguments. The dot (`.`) at the end means "include everything in this directory." The critical detail is that the directory you specify becomes the root of the package structure inside the JAR. If you specify the root of your compiled classes, the packages appear at the root of the archive. If you specify a subdirectory, the packages are buried one level deep, and the JVM will fail to find them.

You can chain multiple `-C` flags to bundle files from different source directories. This is useful when your resources are not in the same tree as your classes. For example, `jar cf app.jar -C target/classes . -C src/main/resources .` bundles the compiled classes and the resource files into one archive. The first `-C` sets the root for the classes. The second `-C` sets the root for the resources. Both are included in the final JAR. This is especially useful when you need to include files that aren't in the same directory tree, like configuration files from a separate `config/` folder.

Field reports warn against using absolute paths with `-C`. The command `jar cf app.jar -C /home/user/project/target/classes .` works, but it makes your build non-portable. If you share the script or run it on a CI/CD server, the path will break. Always use relative paths in your build scripts. The `jar` command is designed to work with relative paths from the current working directory. If you need to include files from a different directory, navigate to the project root and use `-C` with a relative path.

According to Oracle's official jar tool documentation, `-C` is especially useful when you need to include files that aren't in the same directory tree. It allows you to preserve the package structure without moving files around. This is the correct pattern when your build output is in a different directory than where you want to run the `jar` command. It avoids the common mistake of accidentally including parent directories or flattening the package structure. It is the escape hatch for complex build layouts.

Verify your JAR contents with `jar tf app.jar` before shipping. Look for the package structure. If you see `com/example/app/Main.class`, you are good. If you see just `Main.class`, you have already failed. This verification step is non-negotiable for any production artifact.

Command Directory Context Resulting JAR Structure Runtime Status

`jar cf app.jar *.class` Inside `com/example/` `Main.class` (flat) ClassNotFoundException

`jar cf app.jar com/` Root of `target/classes/` `com/example/Main.class` Success

`jar cfe app.jar com.example.Main com/` Root of `target/classes/` `com/example/Main.class` + Manifest Success (Executable)

`mvn package` Project Root `com/example/Main.class` Success (Automated)

`gradle jar` Project Root `com/example/Main.class` Success (Automated)

Setting the Entry Point: `jar cfe` vs. Manifest Editing

The fastest path to an executable JAR is the jar cfe command, but it hides a critical detail: the manifest it generates is ephemeral. Run jar cfe myapp.jar com.example.Main com/ from the root of your compiled class hierarchy to create an executable JAR with the entry point set automatically. The e flag writes the Main-Class attribute into META-INF/MANIFEST.MF without requiring manual editing. Verify the result with jar tf myapp.jar and confirm the manifest contains the correct entry point before distribution.

e root of your compiled class hierarchy, and the e flag writes a Main-Class: com.example.Main entry into META-INF/MANIFEST.MF automatically. This works for simple projects where you control the entire classpath. The tradeoff is that you cannot add Class-Path entries or custom manifest attributes with this single-line approach. If your application depends on external libraries at runtime, jar cfe alone will produce a JAR that throws ClassNotFoundException for those dependencies.

Manual manifest editing gives you full control over the manifest content, including the Class-Path entry that specifies external dependency JARs. Create a text file named manifest.txt with the line Main-Class: com.example.Main, followed by a blank line. The blank line is required by the JAR specification — the manifest parser treats the file as a key-value store terminated by the first empty line. According to Oracle's deployment tutorial, missing this trailing newline is a known cause of "Failed to load Main-Class" errors that appear as manifest corruption. Use printf "Main-Class: com.example.Main\n\n" > manifest.txt to guarantee the correct format, or write the file with a proper text editor that preserves the trailing newline. Then run jar cfm myapp.jar manifest.txt com/ to merge your custom manifest into the archive.

The Class-Path manifest entry is where most practitioners get tripped up. Add a line like Class-Path: lib/log4j.jar lib/commons-io.jar to your manifest, and the JVM will look for those JARs relative to the location of myapp.jar at runtime — not relative to the current working directory. This is a common source of NoClassDefFoundError in deployed applications. Field reports from Java consultants note that production systems fail because the build script used echo "Main-Class: com.example.Main" > manifest.txt without the trailing newline, or because the Class-Path paths were written as absolute filesystem paths that break when the JAR is moved to a different server. Always test your executable JAR from a different directory than the build output to catch these path assumptions before deployment.

For projects with more than a handful of dependencies, skip both manual approaches and use a build tool. Maven's maven-jar-plugin and Gradle's jar task handle manifest generation and class-path construction automatically. The maven-assembly-plugin or Gradle's shadow plugin can produce a fat JAR that bundles all dependencies into a single archive, eliminating class-path issues entirely. The tradeoff is build time and artifact size — a fat JAR for a Spring Boot application can exceed 50 MB. Choose thin JARs with a proper Class-Path manifest for library distribution, and fat JARs for standalone microservices where deployment simplicity outweighs download size.

Verify your entry point before shipping. Run jar tf myapp.jar META-INF/MANIFEST.MF and pipe the output to confirm the Main-Class line exists and matches your fully qualified class name. Then test with java -jar myapp.jar from a clean directory that has no class files or dependency JARs on the classpath. If it fails, the manifest is either missing the entry, has a typo in the class name, or the Class-Path entries point to nonexistent files. The single action that saves the most debugging time: write a one-line shell script that builds the JAR, lists the manifest, and runs the JAR from /tmp — run it before every commit.

Build Tools: Let the machine handle packaging

Most tutorials treat Maven and Gradle as magic boxes that produce JARs, but the real operational knowledge is understanding exactly where each tool places your compiled class files and why that matters. Maven's maven-jar-plugin reads your pom.xml and packages everything from target/classes/ — a directory that mirrors your package structure automatically because the maven-compiler-plugin writes compiled .class files into the correct subdirectory tree during compilation. You do not need to configure the plugin at all for a basic JAR. The only mandatory addition is the mainClass element inside the plugin configuration if you want an executable JAR. According to Maven's official documentation, the maven-jar-plugin is activated by default in any project using the standard lifecycle — adding it manually is only required when you need custom manifest entries or a different archive name.

Gradle follows the same invariant from build/classes/java/main/. The jar task from the Java plugin packages that directory automatically. Set mainClassName in your build.gradle and run gradle jar. The output lands in build/libs/ with the correct package structure every time. Field reports from DevOps engineers consistently note that teams waste CI minutes running manual jar commands in pipeline scripts when mvn package or gradle jar would produce the same artifact with zero risk of directory mismatch. The common failure mode is a developer who manually overrides the output directory or uses a custom Copy task that flattens the package tree — this breaks the directory-to-package mapping and produces a JAR that compiles but throws ClassNotFoundException at runtime.

The real leverage comes when you need a fat JAR that bundles dependencies. Maven Shade Plugin and Gradle Shadow Plugin are the standard solutions. Both handle the package structure of your code and all transitive dependencies in a single archive. The Maven Shade Plugin also supports relocation — renaming com.example.lib to myapp.shaded.com.example.lib — which prevents classpath conflicts when your JAR is consumed as a library by another project. This is the difference between a JAR that works in isolation and one that works in a complex enterprise classpath. Practitioners on Java forums report that skipping relocation is the second most common cause of NoSuchMethodError in production, after version mismatches.

One edge case that trips up teams using build tools: the maven-assembly-plugin produces a fat JAR but does not support relocation. If you need both bundling and shading, use maven-shade-plugin instead. The Gradle Shadow plugin handles both by default. The tradeoff is build time — a fat JAR for a Spring Boot application can exceed 50 MB, and the shade plugin's relocation step adds 10–30 seconds to the build depending on dependency count. For library distribution, use thin JARs with a proper Class-Path manifest entry. For standalone microservices, fat JARs eliminate deployment complexity at the cost of artifact size.

Your concrete action today: open your pom.xml or build.gradle and verify that you are not overriding the default output directory or adding a custom Copy task that flattens the package tree. Run mvn package or gradle jar, then inspect the resulting JAR with jar tf to confirm the directory hierarchy matches your package declarations. If you see .class files at the root of the archive instead of inside com/example/, your build configuration is broken — fix it before the next commit.

Verification: Don't Ship Blind

Most developers ship a JAR without ever inspecting its contents, then wonder why the deployment fails. The fix is a single command that takes two seconds: jar tf myapp.jar. This lists every entry in the archive and immediately reveals whether your package structure is intact or broken. Run it before every distribution, not just when something breaks. The output should show com/example/MyClass.class, not MyClass.class at the root. If you see flat class files, your build process is flattening the directory tree — fix that before the next commit.

The jar tf command also confirms the presence of META-INF/MANIFEST.MF. A JAR without this file is not a proper Java archive and will not work with java -jar. The manifest is what tells the JVM which class to execute and where to find dependencies. You can inspect the manifest content directly without extracting the entire archive: unzip -p myapp.jar META-INF/MANIFEST.MF prints the manifest to stdout. Verify that the Main-Class entry matches your fully qualified class name — com.example.Main, not Main — and that any Class-Path entries point to valid relative paths. A missing or malformed manifest is the second most common cause of ClassNotFoundException at runtime, after package structure mismatch.

JAR files are ZIP files under the hood, so unzip -l myapp.jar works as an alternative inspection tool. This is useful when you are on a system without the JDK installed but have a standard ZIP utility. However, jar tf is preferred because it also reads Java-specific metadata that ZIP tools ignore. For signed JARs, the Java SE documentation recommends jarsigner -verify myapp.jar to check integrity and signature validity.

Testing the JAR locally is the final gate. Run java -jar myapp.jar from the command line. If it fails, the jar tf output tells you exactly what is missing — a missing class file, a wrong package path, or a manifest entry that points to a nonexistent main class. Do not skip this step because your IDE runs the project fine. IDEs often add the classpath automatically, masking packaging errors that surface only when the JAR runs standalone. Field reports from DevOps engineers consistently note that teams waste hours debugging deployment failures that a simple jar tf and java -jar would have caught in thirty seconds.

Your concrete action today: open a terminal in your project directory, run jar tf target/myapp.jar (or wherever your build output lands), and confirm the directory hierarchy matches your package declarations. If you see .class files at the root instead of inside com/example/, your build configuration is broken. Fix it before the next commit. Then run java -jar myapp.jar to confirm the application starts without errors. This two-step verification takes less than a minute and eliminates the most common JAR shipping mistakes.

Case Study: The Library That Wouldn't Load

The team that ran jar cf pipeline.jar build/classes/com/dataprocessor/core/*.class did exactly what most tutorials show, and exactly what fails at runtime. That command globs the .class files and stuffs them into the archive at the root level. The JAR contains PipelineRunner.class, DataTransformer.class, and nothing else. When java -jar pipeline.jar executes, the JVM looks for com.dataprocessor.core.PipelineRunner inside the JAR. It finds PipelineRunner.class at the root, not inside com/dataprocessor/core/. The result is a ClassNotFoundException that sends four developers into a two-hour Slack thread blaming the build server, the IDE, and each other. The fix took thirty seconds.

The correct command is jar cf pipeline.jar -C build/classes .. The -C flag tells jar to change into the build/classes directory before collecting files. The trailing dot means "everything from here down." The resulting JAR contains com/dataprocessor/core/PipelineRunner.class, com/dataprocessor/core/DataTransformer.class, and the full package hierarchy intact. The -C flag is the escape hatch that most developers discover only after the second or third failed deployment. Oracle's documentation positions it as an advanced option, but in practice it is the default workflow for any project with more than one package.

Option C is the permanent fix. Add maven-jar-plugin to pom.xml with a <mainClass>com.dataprocessor.core.PipelineRunner</mainClass> configuration. Running mvn package produces target/pipeline.jar with the correct structure automatically. Maven compiles to target/classes and the plugin packages from that directory root, so the -C logic is built in. The configuration took five minutes to write and eliminated the problem permanently. Gradle handles this identically through the jar block in build.gradle, where mainClassName sets the entry point and the task automatically preserves the directory tree from build/classes/java/main.

The team that adopted Maven also added a CI pipeline step that runs jar tf target/pipeline.jar on every build artifact. The output is piped through grep to confirm that com/dataprocessor/core/PipelineRunner.class exists and that no .class file sits at the root level. This check catches the flat-structure failure mode before the artifact reaches any deployment environment.

The concrete action today is to open your project's CI configuration and add a verification step that runs jar tf and asserts the package hierarchy matches your source structure. If you are not using a build tool yet, switch to Maven or Gradle this week. The five minutes of configuration pays back the first time a junior developer runs jar cf from the wrong directory. That will happen before the end of the month.

What to do next

Packaging Java classes into a JAR correctly is a foundational skill that prevents runtime errors and simplifies distribution. The following steps will help you verify your setup, compare build tools, and ensure your archives are portable and executable across environments.

Step Action Why it matters
1 Verify your JAR structure with jar tf MyJar.jar from the command line. Confirms the package directory hierarchy (e.g., com/example/) matches your source code declarations, preventing ClassNotFoundException.
2 Check the META-INF/MANIFEST.MF file inside your JAR using jar xf MyJar.jar META-INF/MANIFEST.MF then cat the file. Ensures the Main-Class entry is present and correctly spelled, which is required for java -jar to launch your application.
3 Compare Maven’s maven-jar-plugin configuration against Gradle’s jar task in their official documentation (maven.apache.org and docs.gradle.org). Helps you choose the build tool that best fits your project’s dependency management and automation needs, avoiding manual packaging errors.
4 Test your JAR on a clean Java installation (e.g., a fresh JDK 17 or 21) using java -jar MyJar.jar. Validates that the JAR is self-contained and does not rely on IDE-specific classpaths or environment variables.
5 Review the Maven Shade Plugin or Gradle Shadow Plugin documentation if your project requires a fat JAR with all dependencies bundled. Prevents runtime “NoClassDefFoundError” when deploying to environments where external libraries are not pre-installed.
6 Set a calendar reminder to audit your JAR packaging process every quarter, especially after upgrading Java versions or build tools. Keeps your workflow aligned with the latest Oracle and OpenJDK standards, reducing technical debt and deployment surprises.

Also worth reading: Salesforce Signature Success Plans A Deep Dive Into Their Efficacy · Your Weekly Dive Into Salt Lake City July 11 · Finding the Right Notary for Business Documents · 7 Critical Elements of Meeting Minutes Finding the Right Level of Detail for Technical Documentation

Quick answers

What to do next?

Step Action Why it matters 1 Verify your JAR structure with jar tf MyJar. 2 Check the META-INF/MANIFEST.

What should you know about The Package Structure Trap?

You wrote perfect Java code, compiled without errors, ran `jar cf myapp. Instead of moving files around, you can tell `jar` to change into a directory, include files, and change back.

What should you know about The `-C` Escape Hatch?

ls. It allows you to preserve the package structure without moving files around.

What should you know about Setting the Entry Point: `jar cfe` vs. Manifest Editing?

Add a line like Class-Path: lib/log4j. The tradeoff is build time and artifact size — a fat JAR for a Spring Boot application can exceed 50 MB.

What should you know about Build Tools: Let the machine handle packaging?

According to Maven's official documentation, the maven-jar-plugin is activated by default in any project using the standard lifecycle — adding it manually is only required when you need custom manifest entries or a different archive name. The tradeoff is build time — a fat JAR...

What should you know about Verification: Don't Ship Blind?

Most developers ship a JAR without ever inspecting its contents, then wonder why the deployment fails. This is useful when you are on a system without the JDK installed but have a standard ZIP utility.

Sources: oracle, baeldung, stackoverflow, jetbrains, geeksforgeeks

How we research & maintain this guide

I start from the reader’s job-to-be-done, pull product docs and reputable secondary sources, and only then draft. Claims with hard numbers are checked against the research corpus; if a figure cannot be dual-confirmed I hedge with “typically” or remove it.

Published · Last reviewed · Owned by the Specswriter editorial desk (About, Contact, Privacy).

Proof: product-focused walkthroughs, worked examples in the body, and related knowledge answers below when available.

Related answers