{"slug": "i-tried-to-obfuscate-my-java-code-before-sending-it-to-ai-here-s-what-broke", "title": "I tried to obfuscate my Java code before sending it to AI — here's what broke", "summary": "A developer spent over a month trying to obfuscate Java source code before sending it to AI assistants, only to discover that Spring Data's repository method names and @Query annotations are semantically significant and cannot be renamed without breaking the application. The project required preserving Spring Data method naming conventions and JPQL queries, turning a simple string-replace task into a framework-aware obfuscation problem.", "body_md": "When a client added a *\"no AI assistants on our codebase\"* clause to my contract last year, I did what any developer would do: I assumed I could solve it with regex.\n\nThe plan was simple. Before sending Java source to Claude, I'd rename every identifier — `InvoiceService`\n\nbecomes `Cls_a1b2c3d4`\n\n, `customerName`\n\nbecomes `fld_e5f6a7b8`\n\n. The AI works on the obfuscated version, I rename everything back, the AI never sees my actual domain. Five hundred lines of code, two days max. I was sure of it.\n\nIt took two weeks before I had something that even compiled, and another month before the tests passed. This article is a tour of the things that broke and why each one is a different shape of the same lesson: **Java code obfuscation for AI is a framework problem, not a string-replace problem.**\n\nIf you've never tried this, the obvious approach feels obvious for a reason. You parse the Java file, collect the identifiers (classes, methods, fields, packages), generate a deterministic hash for each one, replace every occurrence. Roughly:\n\n```\n// Before\npublic class InvoiceService {\n    public Invoice calculateDiscount(Invoice invoice) {\n        return invoice.applyDiscount(0.10);\n    }\n}\n\n// After\npublic class Cls_a1b2c3d4 {\n    public Cls_e5f6a7b8 mtd_9c8d7e6f(Cls_e5f6a7b8 fld_3a4b5c6d) {\n        return fld_3a4b5c6d.mtd_2c1b0a9f(0.10);\n    }\n}\n```\n\nFine, right? The AI sees something structurally identical — same control flow, same types, same method calls — but with no business meaning to leak. JavaParser handles the AST, you walk it once, you're done.\n\nYou're not done. This works on a hello-world program. The first time you run it on a real Spring Boot project, your application context fails to start. And the failure has nothing to do with the AI yet — it's the framework loudly telling you that the names you renamed *meant something to it*.\n\nThis was my first proper \"oh.\" moment. Consider:\n\n```\npublic interface InvoiceRepository extends JpaRepository<Invoice, Long> {\n    List<Invoice> findByActiveTrue();\n    long countByPaidFalse();\n}\n```\n\nAfter my naive pass, `findByActiveTrue`\n\nbecame `mtd_a3f9b2c1`\n\n. The class compiled, the application *seemed* fine — and then at the first request, Spring threw:\n\n```\norg.springframework.data.mapping.PropertyReferenceException:\nNo property mtd_a3f9b2c1 found for type Invoice!\n```\n\nI read that error five times before I understood. Spring Data doesn't generate the SQL for `findByActiveTrue`\n\nfrom any *implementation* — there is no implementation. It generates the SQL from the *method name itself* at bean-creation time. `findBy`\n\n+ `Active`\n\n+ `True`\n\nbecomes `WHERE active = true`\n\n. Rename the method to `mtd_a3f9b2c1`\n\nand Spring tries to find a property called `mtd_a3f9b2c1`\n\non the `Invoice`\n\nentity. There isn't one. Crash.\n\nThe lesson: in Spring Data, **the method name is the query.** It is not a label that can be renamed independently of its meaning. Obfuscate it and you destroy its semantic content by definition.\n\nThe fix was to scan the project for interfaces extending `Repository`\n\n, `CrudRepository`\n\n, `JpaRepository`\n\n, etc., and for each one, mark every method matching the Spring Data prefix patterns as preserved:\n\n`findBy*`\n\n, `readBy*`\n\n, `getBy*`\n\n, `queryBy*`\n\n, `searchBy*`\n\n, `streamBy*`\n\n`existsBy*`\n\n, `countBy*`\n\n`deleteBy*`\n\n, `removeBy*`\n\n`And`\n\n, `Or`\n\n, `OrderBy`\n\n, `IgnoreCase`\n\n, `Distinct`\n\n, `First`\n\n, `Top10`\n\n...That set goes into an exclusion list applied as the obfuscation pass runs. Classes get renamed, fields get renamed, but Spring Data's contract methods are preserved exactly.\n\nThe first time the obfuscated project compiled and started cleanly, I cried a little.\n\n`@Query`\n\nannotations\nI thought I was through the worst. Then I tried it on a project with custom JPQL:\n\n```\n@Query(\"SELECT i FROM Invoice i WHERE i.customer.id = :customerId\")\nList<Invoice> findUnpaidByCustomer(@Param(\"customerId\") Long customerId);\n```\n\nAfter obfuscation, the `Invoice`\n\n*class* became `Cls_e5f6a7b8`\n\n. Hibernate happily compiled. At runtime, the query failed:\n\n```\norg.hibernate.query.SemanticException: Could not resolve entity 'Invoice'\n```\n\nJPQL is not SQL. It references entity *class names*, not table names. The string `\"SELECT i FROM Invoice i ...\"`\n\nsays *\"select from the entity class called Invoice.\"* If the class is now called `Cls_e5f6a7b8`\n\n, the string has to say `\"SELECT i FROM Cls_e5f6a7b8 i ...\"`\n\n.\n\nTwo follow-up traps:\n\n**Field accessors inside the JPQL.** `i.customer.id`\n\nis also a path through entity field names. If `customer`\n\nwas renamed to `fld_2c3d4e5f`\n\n, the string has to become `i.fld_2c3d4e5f.id`\n\n.\n\n**Native queries are different.** `@Query(value = \"SELECT * FROM invoices WHERE total > ?1\", nativeQuery = true)`\n\nreferences the database *table* and *column* names — those are part of the schema, not the Java identifier space. They must NOT be renamed.\n\nSo I added a post-processing pass: scan every `@Query`\n\nvalue, identify whether it's JPQL or native (the annotation argument tells you), and for JPQL, replace entity-name and field-name tokens in sync with the rest of the obfuscation. The native ones get left alone.\n\nThis one almost made me give up. Lombok generates code at compile time. Consider:\n\n```\n@Data\npublic class InvoiceDto {\n    private Long id;\n    private String customerName;\n    private boolean paid;\n}\n```\n\nLombok generates `getId()`\n\n, `setId(Long)`\n\n, `getCustomerName()`\n\n, `setCustomerName(String)`\n\n, `isPaid()`\n\n, `setPaid(boolean)`\n\n. The accessor names are *mechanically derived from the field names* at compile time.\n\nIf I rename `customerName`\n\nto `fld_8c9d0e1f`\n\n, Lombok will run on my obfuscated source and generate `getFld_8c9d0e1f()`\n\n. Meanwhile, every callsite in the rest of the codebase still calls `dto.getCustomerName()`\n\n. The build fails:\n\n```\ncannot find symbol\n  symbol:   method getCustomerName()\n  location: variable dto of type Cls_e5f6a7b8\n```\n\nThe fix is two-step. First, detect Lombok-instrumented classes (`@Data`\n\n, `@Getter`\n\n, `@Setter`\n\n, `@Value`\n\n, `@Builder`\n\n, `@RequiredArgsConstructor`\n\n, etc.). Second, when renaming a field on such a class, *also* rename the implicit accessors at every callsite in the project:\n\n`customerName`\n\n→ `fld_8c9d0e1f`\n\n`getCustomerName()`\n\ncallsites → `getFld_8c9d0e1f()`\n\n`setCustomerName(...)`\n\ncallsites → `setFld_8c9d0e1f(...)`\n\n`isPaid()`\n\n→ `isFld_0c1d2e3f()`\n\n(boolean variant uses `is`\n\n, not `get`\n\n)`.customerName(...)`\n\n→ `.fld_8c9d0e1f(...)`\n\nIt's a coordinated rename, not a per-identifier rename. Once you see this, you start seeing it everywhere — Jackson does the same kind of generation, Bean Validation references field names from constraints, OpenAPI generates schemas from field names.\n\nI'll spare you the long version. In quick succession I found:\n\n`@JsonProperty`\n\nand DTO classes`dto/`\n\n, `model/`\n\n, or `request/response/`\n\npackage gets its fields preserved.`@NotBlank private String email;`\n\n— the constraint message templates and `{validatedValue}`\n\ninterpolation reference the field name.`@ConfigurationProperties`\n\n`@ConfigurationProperties(prefix = \"app\")`\n\nbinds YAML keys to field names. Rename the field, the `application.yml`\n\nno longer maps.`Class.forName(\"com.acme.InvoiceService\")`\n\nis a string that has to be updated in lockstep with the class rename. Same for `getMethod(\"calculateTotal\")`\n\n.`@Schema`\n\nannotations document field names that end up in your API documentation. Rename and the contract documentation drifts.Each of these gets its own detector. Eight detectors total in the version that finally worked.\n\nThe mental model that finally clicked, after the third or fourth rewrite:\n\n```\nPass 0:  Scan the project for framework annotations and conventions.\n         For each detected framework, generate exclusion rules.\n\nPass 1:  Collect identifiers (classes, methods, fields, packages, enums, records)\n         using a JavaParser AST. Filter against the exclusion list.\n\nPass 2:  Replace identifiers in source files. Word-boundary regex,\n         longest-match-first ordering.\n\nPass 3:  Post-process strings in @Query, @ComponentScan, Class.forName(),\n         getMethod() — apply identifier replacement inside specific contexts.\n\nPass 4:  Compile. If it fails, parse compiler errors, reverse-map the broken\n         identifiers, add them to the exclusion list, re-run from Pass 1.\n         Repeat until green or max iterations.\n```\n\nThe detector-per-framework architecture means you can't think of it as one big \"obfuscate Java\" function. You think of it as a coordination problem between renaming and the conventions each framework uses. Once you have the detector for Spring Data, adding the detector for Spring Config or OpenAPI is mechanical.\n\nNo matter how careful the detectors are, real Java projects have edge cases. Some annotation processor you've never heard of generates code based on a field name. Some library uses reflection in a way the detectors don't catch. Some method name collides with a JDK method after obfuscation.\n\nFor everything detection misses, the auto-fix loop is the safety net:\n\n`mvn test-compile`\n\n(or `mvn test`\n\nif you trust your tests).In practice the loop converges in 2–4 iterations on a fresh project. The exclusion list grows to a few hundred entries on a 50-class project, then stabilizes. Subsequent runs just use the saved exclusions and don't need iteration.\n\nLooking back, my original mistake was thinking of the AI privacy problem as primarily a *text* problem (replace strings before sending to API). It is not. It is a *framework* problem (preserve the conventions that the frameworks rely on while renaming everything else).\n\nThe frameworks I depend on every day — Spring, JPA, Lombok, Jackson, Bean Validation, OpenAPI — encode meaning in identifier names *intentionally*. They were designed to reduce boilerplate by deriving behavior from naming conventions. That same design choice makes them incompatible with naive obfuscation. To obfuscate Java code safely for AI, you have to know each framework's contract and respect it.\n\nThere is no shortcut. The two-day project became a six-month project and more. But once the detector-per-framework architecture is in place, adding new framework support is incremental, the auto-fix loop handles the long tail, and the result is a Java codebase that the AI sees in obfuscated form, works on, and produces changes that re-apply cleanly to the real source — with the build still green.\n\nIf you want to see the detectors in action on real Java, the framework-by-framework before/after examples are in [gitlab.com/gbreton7/promptcape-docs](https://gitlab.com/gbreton7/promptcape-docs). I built this as ** PromptCape** — a Java-first obfuscation proxy for Claude Code, Cursor, and other AI coding assistants — but the design lessons here apply to any code obfuscation pipeline that has to coexist with framework conventions. MRs welcome on the docs repo if I'm missing your favorite framework.", "url": "https://wpnews.pro/news/i-tried-to-obfuscate-my-java-code-before-sending-it-to-ai-here-s-what-broke", "canonical_source": "https://dev.to/genevieve_breton_cb795f52/i-tried-to-obfuscate-my-java-code-before-sending-it-to-ai-heres-what-broke-5615", "published_at": "2026-07-08 17:22:49+00:00", "updated_at": "2026-07-08 18:11:14.839420+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models"], "entities": ["Spring Data", "Spring Boot", "Claude", "JavaParser", "JpaRepository", "CrudRepository", "InvoiceService"], "alternates": {"html": "https://wpnews.pro/news/i-tried-to-obfuscate-my-java-code-before-sending-it-to-ai-here-s-what-broke", "markdown": "https://wpnews.pro/news/i-tried-to-obfuscate-my-java-code-before-sending-it-to-ai-here-s-what-broke.md", "text": "https://wpnews.pro/news/i-tried-to-obfuscate-my-java-code-before-sending-it-to-ai-here-s-what-broke.txt", "jsonld": "https://wpnews.pro/news/i-tried-to-obfuscate-my-java-code-before-sending-it-to-ai-here-s-what-broke.jsonld"}}