<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nicholas</title>
    <description>The latest articles on DEV Community by nicholas (@nicholas1991).</description>
    <link>https://dev.to/nicholas1991</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1452816%2Fe30d965a-18d0-4441-9008-22528ab0ea68.jpg</url>
      <title>DEV Community: nicholas</title>
      <link>https://dev.to/nicholas1991</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nicholas1991"/>
    <language>en</language>
    <item>
      <title>Redis Memory Full and Keys Evicted: A Java Backend Checklist of 4 Spots</title>
      <dc:creator>nicholas</dc:creator>
      <pubDate>Tue, 03 Nov 2026 15:18:17 +0000</pubDate>
      <link>https://dev.to/nicholas1991/redis-memory-full-and-keys-evicted-a-java-backend-checklist-of-4-spots-2o5i</link>
      <guid>https://dev.to/nicholas1991/redis-memory-full-and-keys-evicted-a-java-backend-checklist-of-4-spots-2o5i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Wednesday afternoon, cache hit rate dropped from 98% to 62%, and database QPS quintupled. Ops checked around and said: Redis memory is maxed out, some keys were evicted, it's normal behavior. But the keys that got evicted happened to be login sessions, and users were collectively kicked offline. Memory full means eviction—sounds perfectly reasonable. The problem is &lt;em&gt;which&lt;/em&gt; keys get evicted and &lt;em&gt;why&lt;/em&gt; those. This article walks through Redis memory eviction from policy configuration down to kernel behavior, and throws in a set of monitoring code you can run directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;I. What Exactly Is This Problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Conclusion: Redis deleting keys after memory is maxed out is not a bug—it's the &lt;code&gt;maxmemory-policy&lt;/code&gt; setting doing its job by the rules. Pick the wrong rule, and it won't delete what you wanted deleted.&lt;/p&gt;

&lt;p&gt;Redis is an in-memory database—all data lives in memory. Give it a limit, and it won't eat memory indefinitely. That limit is called &lt;code&gt;maxmemory&lt;/code&gt;. Whether writes can still go through once you hit the limit, and who gets deleted when they can't, all depends on &lt;code&gt;maxmemory-policy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Many teams, to "keep things simple" at launch, just copy a chunk of config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;maxmemory&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="n"&gt;gb&lt;/span&gt;
&lt;span class="n"&gt;maxmemory&lt;/span&gt;-&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;allkeys&lt;/span&gt;-&lt;span class="n"&gt;lru&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;allkeys-lru&lt;/code&gt; means: when memory runs short, pick the least recently accessed key from &lt;strong&gt;all keys&lt;/strong&gt; and delete it. Note the word "all." Sessions, product cache, rate-limit counters, distributed locks, feature flags—all treated the same. As long as it's the least recently accessed, it gets killed first.&lt;/p&gt;

&lt;p&gt;Config-type data suffers the most. Take a feature flag stored in Redis: once written, nobody reads it again. An hour later memory gets tight, and it's the first to go. Then the business logic reads a null value, falls through to the default branch, and the feature quietly turns off—with no error whatsoever.&lt;/p&gt;

&lt;p&gt;Redis supports 8 eviction policies in total. The difference comes down to two things: &lt;strong&gt;which pool to pick from&lt;/strong&gt; and &lt;strong&gt;by what rule to pick&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PolicyCandidate ScopeSelection Rule&lt;/strong&gt;noevictionNo evictionWrites return an errorallkeys-lruAll keysLeast recently accessedallkeys-lfuAll keysLowest access frequencyallkeys-randomAll keysRandomvolatile-lruKeys with TTL setLeast recently accessedvolatile-lfuKeys with TTL setLowest access frequencyvolatile-randomKeys with TTL setRandomvolatile-ttlKeys with TTL setShortest remaining TTL&lt;/p&gt;

&lt;p&gt;The difference between &lt;code&gt;allkeys-*&lt;/code&gt; and &lt;code&gt;volatile-*&lt;/code&gt; is fatal. The former deletes your permanent config right along with everything else; the latter only touches data you explicitly marked as "expirable."&lt;/p&gt;

&lt;p&gt;There's one more easily overlooked point: &lt;strong&gt;the default policy is&lt;/strong&gt; &lt;code&gt;noeviction&lt;/code&gt;. Instances that never configured &lt;code&gt;maxmemory-policy&lt;/code&gt; won't delete anything when memory fills up—all write commands return &lt;code&gt;OOM command not allowed when used memory &amp;gt; 'maxmemory'&lt;/code&gt;. Reads work fine; writes all fail. Some people treat this as "Redis is stuck," when in reality it's explicitly rejecting you.&lt;/p&gt;

&lt;p&gt;To determine whether an instance has ever evicted anything, one metric is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli info stats | &lt;span class="nb"&gt;grep &lt;/span&gt;evicted_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This number is the cumulative count of keys evicted since process start—it only goes up. It's a cumulative value, not a current value. It resets to zero on restart, so don't use it for business analytics. What you actually need to watch is its &lt;strong&gt;rate of growth&lt;/strong&gt;—tens of thousands in half an hour means memory is under sustained pressure, not an occasional blip.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;II. What's Really Happening Under the Hood&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Conclusion: Redis's memory limit is "effective memory," not "physical memory"; eviction is a check performed before each command execution; LRU is sampled estimation, not exact ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Where Memory Actually Goes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There are a few key fields in &lt;code&gt;INFO memory&lt;/code&gt;, and many people only look at the first one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;used_memory&lt;/code&gt;: data memory as counted by Redis itself, including the data and the overhead of structures managing that data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;used_memory_rss&lt;/code&gt;: actual process usage as seen by the OS, including memory fragmentation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;used_memory_dataset&lt;/code&gt;: pure data portion, minus management overhead&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mem_fragmentation_ratio&lt;/code&gt;: &lt;code&gt;used_memory_rss / used_memory&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A concrete example. You store 1 million string keys, each key averaging 20 bytes and each value averaging 80 bytes—the data itself is roughly 100MB. But Redis attaches a &lt;code&gt;dictEntry&lt;/code&gt; and a &lt;code&gt;redisObject&lt;/code&gt; to every key, plus jemalloc's fixed-size-bin allocation leaves holes. Actual &lt;code&gt;used_memory&lt;/code&gt; often lands around 150MB—50% inflation is normal.&lt;/p&gt;

&lt;p&gt;So for the question "My data is only 100MB, why is 8G of memory full?"—the answer usually lies in four areas: management overhead, big keys, replication buffers, and client output buffers—not the data itself.&lt;/p&gt;

&lt;p&gt;Also watch the fragmentation ratio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mem_fragmentation_ratio&lt;/code&gt; between 1.0 and 1.5: normal&lt;/li&gt;
&lt;li&gt;Greater than 1.8: severe fragmentation, consider enabling &lt;code&gt;activedefrag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Less than 1: some memory has been swapped out, and performance will fall off a cliff&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Eviction Happens Before Command Execution&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Redis processes commands single-threaded (referring to the main command execution thread). For every command that might increase memory—&lt;code&gt;SET&lt;/code&gt;, &lt;code&gt;LPUSH&lt;/code&gt;, &lt;code&gt;HSET&lt;/code&gt;—before execution it checks once: has current &lt;code&gt;used_memory&lt;/code&gt; exceeded &lt;code&gt;maxmemory&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Once it exceeds the limit, it enters the eviction process, deleting keys in a loop until it drops below the limit or can't delete any more. This loop has a cap; it won't delete indefinitely, avoiding a single command getting stuck for too long.&lt;/p&gt;

&lt;p&gt;The key point is here: &lt;strong&gt;this check is triggered per command, not by a background scheduled job&lt;/strong&gt;. That is, if there are no writes for a while, memory can exceed the limit without being cleaned up. Conversely, a large batch-write script may repeatedly trigger eviction during execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;LRU is sampling, not sorting&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Textbook LRU maintains a full access linked list, which is costly in both memory and CPU. Redis doesn't do that.&lt;/p&gt;

&lt;p&gt;It records a 24-bit timestamp for each key (the &lt;code&gt;lru&lt;/code&gt; field, reusing space in &lt;code&gt;redisObject&lt;/code&gt;). When evicting, it randomly samples &lt;code&gt;maxmemory-samples&lt;/code&gt; keys, 5 by default, and deletes the least recently accessed among those 5.&lt;/p&gt;

&lt;p&gt;Sampling 5 is obviously not accurate. But Redis compensates in two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Maintain a 16-element candidate pool (pool), compare each sample against the pool, and keep the "older" ones&lt;/li&gt;
&lt;li&gt;Eviction is continuous; if one pass isn't accurate, a few more rounds will make it accurate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Set &lt;code&gt;maxmemory-samples&lt;/code&gt; to 10 and accuracy approaches true LRU, at the cost of a bit more CPU per eviction. In production, choose between 5 and 10; don't set it too high.&lt;/p&gt;

&lt;p&gt;LFU is a different mechanism. It gives each key an 8-bit counter; each access increments it probabilistically, while decaying keys that haven't been accessed for a long time (&lt;code&gt;lfu-decay-time&lt;/code&gt;, default 1 minute). The 8-bit max is 255, and it uses logarithmic growth, so the first few accesses increase quickly and later ones increase slowly.&lt;/p&gt;

&lt;p&gt;LFU suits scenarios where "hot spots are stable," such as homepage recommendation slots. LRU suits scenarios where "hot spots drift," such as breaking news. Choosing the wrong one won't crash anything, but hit rate will be noticeably worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The nastiest case with volatile policies&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Suppose you set the policy to &lt;code&gt;volatile-lru&lt;/code&gt;, and in your business code a batch of keys is missing an expiration time.&lt;/p&gt;

&lt;p&gt;When memory is full, Redis looks for "keys with a TTL" to evict. If it finds them, it evicts normally. What if it can't find any?&lt;/p&gt;

&lt;p&gt;The answer is: &lt;strong&gt;it treats that as nothing to evict, and write commands directly return an OOM error&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is exactly the root cause of many cases where "volatile-lru is set but writes still fail." The policy isn't wrong; the data doesn't meet the policy's precondition. The &lt;code&gt;volatile-*&lt;/code&gt; family has a hard precondition: &lt;strong&gt;every key that should be eligible for eviction must have a TTL&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Deleting a big key can block the main thread&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Evicting a key isn't just removing it from the dictionary; it also has to free the memory it occupies. If the key is a Hash with 5 million elements, freeing it can take tens of milliseconds.&lt;/p&gt;

&lt;p&gt;Redis 4.0 and later provides asynchronous freeing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;lazyfree&lt;/span&gt;-&lt;span class="n"&gt;lazy&lt;/span&gt;-&lt;span class="n"&gt;eviction&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once enabled, eviction only performs a logical delete, and the actual memory freeing is handed off to a background thread. The cost is that memory reclamation is delayed, and &lt;code&gt;used_memory&lt;/code&gt; will briefly be higher than the actual value.&lt;/p&gt;

&lt;p&gt;Large keys should be split up in the first place. A single value over 10KB is a warning sign, and over 1MB is basically a design problem. Use &lt;code&gt;MEMORY USAGE key&lt;/code&gt; to see the actual usage of a single key, and &lt;code&gt;redis-cli --bigkeys&lt;/code&gt; to scan the entire database.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Eviction under primary-replica architecture&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Only the primary node makes eviction decisions. After the primary deletes a key, it generates a &lt;code&gt;DEL&lt;/code&gt; command and syncs it to the replicas.&lt;/p&gt;

&lt;p&gt;Replicas do not evict on their own. So if a replica's &lt;code&gt;maxmemory&lt;/code&gt; is set the same as the primary's, the replica's memory may exceed the limit. This is also why replicas are usually recommended to be configured with &lt;code&gt;maxmemory-policy noeviction&lt;/code&gt;, so they are only responsible for copying the primary's delete actions and do not make decisions themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What should maxmemory be set to&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Do not set it to 100% of physical memory.&lt;/p&gt;

&lt;p&gt;There are several areas that must be left room for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replication buffer (&lt;code&gt;repl-backlog&lt;/code&gt;, 1MB by default, but it can spike during a full sync)&lt;/li&gt;
&lt;li&gt;AOF rewrite buffer&lt;/li&gt;
&lt;li&gt;Client output buffer (Redis 7 and later has &lt;code&gt;maxmemory-clients&lt;/code&gt; to limit it separately)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule of thumb: set &lt;code&gt;maxmemory&lt;/code&gt; to 60%–75% of physical memory. If the machine runs only Redis, 75%; if it is co-located with other services, 60% or even lower. The remaining space will also be consumed in no small part by the child process forked during BGSAVE doing copy-on-write.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Hands-on: build an observable cache step by step&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Conclusion: data such as sessions must go through &lt;code&gt;volatile-lru&lt;/code&gt; + enforced TTL, and there must be code-level eviction monitoring, so you do not have to wait until users are logged out to discover the problem.&lt;/p&gt;

&lt;p&gt;The project below is called &lt;code&gt;redis-memory-guard&lt;/code&gt; and contains three things: a session cache with TTL, a monitoring component that collects Redis memory metrics every 30 seconds, and an endpoint that exposes the metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Project structure&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redis-memory-guard
├── docker-compose.yml
├── pom.xml
└── src/main
    ├── java/com/example/redisguard
    │   ├── RedisMemoryGuardApplication.java
    │   ├── monitor
    │   │   ├── RedisMemoryMonitor.java
    │   │   ├── RedisMemorySnapshot.java
    │   │   └── RedisMonitorController.java
    │   └── session
    │       ├── SessionCacheService.java
    │       └── SessionController.java
    └── resources/application.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Start a deliberately misconfigured Redis&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;First, start an instance with the kind of wrong configuration you are preparing to troubleshoot: a 128MB limit, &lt;code&gt;allkeys-lru&lt;/code&gt;, to make it easy to reproduce the problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-eviction-demo&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis-server&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--maxmemory&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;128mb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--maxmemory-policy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;allkeys-lru&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;pom.xml&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;project&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0"&lt;/span&gt;
         &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
         &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelVersion&amp;gt;&lt;/span&gt;4.0.0&lt;span class="nt"&gt;&amp;lt;/modelVersion&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;parent&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-parent&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.1.2&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;relativePath/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/parent&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.example&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;redis-memory-guard&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;name&amp;gt;&lt;/span&gt;redis-memory-guard&lt;span class="nt"&gt;&amp;lt;/name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;Redis 内存淘汰监控示例&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;java.version&amp;gt;&lt;/span&gt;21&lt;span class="nt"&gt;&amp;lt;/java.version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.1.2&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
​
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-data-redis&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.1.2&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
​
        &lt;span class="c"&gt;&amp;lt;!-- Lettuce 连接池需要，不引这个 pool 配置不生效 --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.commons&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;commons-pool2&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.13.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
​
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-test&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.1.2&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;test&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.1.2&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;commons-pool2&lt;/code&gt; line. Many people write connection pool parameters in &lt;code&gt;application.yml&lt;/code&gt; and find they don't take effect—this dependency is missing. Lettuce uses its own connection pool implementation, and Spring Boot only enables pooling configuration when &lt;code&gt;commons-pool2&lt;/code&gt; is present on the classpath.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;application.yml&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="s"&gt;​&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-memory-guard&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;
      &lt;span class="na"&gt;lettuce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;max-active&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;
          &lt;span class="na"&gt;max-idle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;
          &lt;span class="na"&gt;min-idle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
          &lt;span class="na"&gt;max-wait&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;timeout: 2s&lt;/code&gt; is the command timeout, not the connection timeout. The default is no timeout, and a slow command can hang a request thread indefinitely, so always set it explicitly in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Main Class&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.redisguard&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.SpringApplication&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.autoconfigure.SpringBootApplication&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.scheduling.annotation.EnableScheduling&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="nd"&gt;@SpringBootApplication&lt;/span&gt;
&lt;span class="nd"&gt;@EnableScheduling&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedisMemoryGuardApplication&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisMemoryGuardApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@EnableScheduling&lt;/code&gt; is used by the monitoring component's scheduled task. If you miss it, the &lt;code&gt;@Scheduled&lt;/code&gt; method will never execute, not a single log line will appear, and you'll think the monitoring code is wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Session Cache Service&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.redisguard.session&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.data.redis.core.StringRedisTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.stereotype.Service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.time.Duration&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.concurrent.TimeUnit&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionCacheService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"session:"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt; &lt;span class="no"&gt;TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMinutes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;StringRedisTemplate&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SessionCacheService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StringRedisTemplate&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redisTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;nickname&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"userId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nickname"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nickname&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loginAt"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opsForHash&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;putAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// putAll 不会带过期时间，TTL 必须单独设置&lt;/span&gt;
        &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;TTL&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opsForHash&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="c1"&gt;// 每次读都续期，做成滑动过期&lt;/span&gt;
        &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;TTL&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;ttlSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getExpire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2L&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are worth mentioning.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;putAll&lt;/code&gt;, calling &lt;code&gt;expire&lt;/code&gt; separately is required. Redis's &lt;code&gt;HSET&lt;/code&gt; command itself does not include an expiration time, and Spring Data Redis does not fold TTL into the &lt;code&gt;putAll&lt;/code&gt; wrapper. If you miss this line, the key becomes permanent, and permanent keys are never evicted under &lt;code&gt;volatile-lru&lt;/code&gt;—the cache only grows and never shrinks.&lt;/p&gt;

&lt;p&gt;The renewal in &lt;code&gt;find&lt;/code&gt; is sliding expiration: each access resets the 30-minute window. It suits sessions where the user stays active. If what you want is "must re-authenticate 30 minutes after login," then don't renew; use absolute expiration.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Session Endpoint&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.redisguard.session&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.http.ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.DeleteMapping&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.GetMapping&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.PathVariable&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.PostMapping&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.RequestMapping&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.RequestParam&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.RestController&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/session"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;SessionCacheService&lt;/span&gt; &lt;span class="n"&gt;sessionCacheService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SessionController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SessionCacheService&lt;/span&gt; &lt;span class="n"&gt;sessionCacheService&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionCacheService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessionCacheService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{sessionId}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                      &lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                      &lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;nickname&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sessionCacheService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nickname&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"sessionId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"ttlSeconds"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionCacheService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ttlSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{sessionId}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessionCacheService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notFound&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="nd"&gt;@DeleteMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{sessionId}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sessionCacheService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"deleted"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Memory Snapshot&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.redisguard.monitor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;RedisMemorySnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;usedMemory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;usedMemoryRss&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;maxMemory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;evictedKeys&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;evictedDelta&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt; &lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RedisMemorySnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;usedRatio&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxMemory&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;usedMemory&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;maxMemory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;fragmentationRatio&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usedMemory&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;usedMemoryRss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;usedMemory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;usedMemoryHuman&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;human&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usedMemory&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;maxMemoryHuman&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;maxMemory&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"未设置"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;human&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxMemory&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;human&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1024L&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;kb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1024.0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1024.0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%.1fKB"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kb&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kb&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1024.0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1024.0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%.1fMB"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%.2fGB"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1024.0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a record with immutable fields, suitable for passing between the monitoring thread and HTTP threads. &lt;code&gt;evictedDelta&lt;/code&gt; is the "number of evictions added this round," which is much more useful than a cumulative value—a cumulative value only proves that "something was deleted," while a delta tells you that "deletions are happening now."&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Monitoring Collection&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.redisguard.monitor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.slf4j.Logger&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.slf4j.LoggerFactory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.data.redis.connection.RedisConnection&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.data.redis.core.RedisCallback&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.data.redis.core.StringRedisTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.scheduling.annotation.Scheduled&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.stereotype.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Properties&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedisMemoryMonitor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoggerFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisMemoryMonitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;StringRedisTemplate&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;volatile&lt;/span&gt; &lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;volatile&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;lastEvictedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;RedisMemoryMonitor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StringRedisTemplate&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redisTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="nd"&gt;@Scheduled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixedDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30_000L&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisCallback&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"读取 Redis INFO 失败，本次采集跳过"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;evictedDelta&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Redis 正在淘汰 key：本轮新增 {} 个，策略 {}，已用 {} / 上限 {}，碎片率 {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;evictedDelta&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;usedMemoryHuman&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxMemoryHuman&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%.2f"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fragmentationRatio&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisConnection&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverCommands&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"memory"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverCommands&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stats"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"used_memory"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"used_memory_rss"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"maxmemory"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;evicted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"evicted_keys"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"maxmemory_policy"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastEvictedKeys&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evicted&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lastEvictedKeys&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Redis 重启过，计数归零，这一轮整体当作新增&lt;/span&gt;
            &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evicted&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evicted&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastEvictedKeys&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;lastEvictedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evicted&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RedisMemorySnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rss&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evicted&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NumberFormatException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt; &lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this code does is simple: every 30 seconds it sends two &lt;code&gt;INFO&lt;/code&gt; commands over the Redis protocol and parses the text results into a struct. Using &lt;code&gt;RedisCallback&lt;/code&gt; instead of creating a new Jedis connection is to reuse Spring's connection pool and timeout configuration, and to route issues through unified exception handling.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;serverCommands().info("memory")&lt;/code&gt; returns &lt;code&gt;Properties&lt;/code&gt;, and the key names are exactly the field names in the &lt;code&gt;INFO&lt;/code&gt; output, all lowercase with underscores. &lt;code&gt;evicted_keys&lt;/code&gt; is only in &lt;code&gt;INFO stats&lt;/code&gt;, so don't look for it in the &lt;code&gt;memory&lt;/code&gt; section.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Metrics Endpoint&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.redisguard.monitor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.GetMapping&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.RequestMapping&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.RestController&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin/redis"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedisMonitorController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;RedisMemoryMonitor&lt;/span&gt; &lt;span class="n"&gt;redisMemoryMonitor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;RedisMonitorController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisMemoryMonitor&lt;/span&gt; &lt;span class="n"&gt;redisMemoryMonitor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redisMemoryMonitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisMemoryMonitor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/memory"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RedisMemorySnapshot&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisMemoryMonitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"usedMemory"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;usedMemoryHuman&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="s"&gt;"maxMemory"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxMemoryHuman&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="s"&gt;"usedRatio"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%.2f%%"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;usedRatio&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                &lt;span class="s"&gt;"fragmentationRatio"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%.2f"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fragmentationRatio&lt;/span&gt;&lt;span class="o"&gt;()),&lt;/span&gt;
                &lt;span class="s"&gt;"policy"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="s"&gt;"evictedKeysTotal"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;evictedKeys&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="s"&gt;"evictedKeysLastRound"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;evictedDelta&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Run and Verify&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn spring-boot:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write a session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8080/session/s-1001?userId=1001&amp;amp;nickname=lvdongbin"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"s-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ttlSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1799&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:8080/session/s-1001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"nickname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"lvdongbin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"loginAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1792934400000"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check monitoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:8080/admin/redis/memory"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"usedMemory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1.2MB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"maxMemory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"128.0MB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"usedRatio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"0.94%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fragmentationRatio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1.08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"allkeys-lru"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"evictedKeysTotal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"evictedKeysLastRound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To trigger eviction, just flood Redis with data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;redis-eviction-demo redis-benchmark &lt;span class="nt"&gt;-n&lt;/span&gt; 200000 &lt;span class="nt"&gt;-r&lt;/span&gt; 200000 &lt;span class="nt"&gt;-d&lt;/span&gt; 2048 &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command uses Redis's built-in benchmarking tool to write 200,000 random keys, each value around 2KB, which can easily fill up 128MB. Once you push past the limit, check &lt;code&gt;/admin/redis/memory&lt;/code&gt; again; &lt;code&gt;evictedKeysLastRound&lt;/code&gt; will no longer be 0, and warnings about &lt;code&gt;Redis 正在淘汰 key&lt;/code&gt; will also appear in the logs.&lt;/p&gt;

&lt;p&gt;The value of this code is not in the monitoring itself, but in &lt;strong&gt;turning "eviction is happening" into a number you can receive a signal from&lt;/strong&gt;. A drop in cache hit rate is lagging; the eviction count is real-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Pitfalls and Best Practices&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Conclusion: 80% of Redis memory problems come from two things: "keys without TTL" and "maxmemory set too high."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 1:&lt;/strong&gt; &lt;code&gt;HSET&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;code&gt;LPUSH&lt;/code&gt; &lt;strong&gt;and then forgetting&lt;/strong&gt; &lt;code&gt;EXPIRE&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Redis expiration is attached to the key, not to fields. If you keep adding fields to a Hash, the key's own TTL will not be reset by writes, nor will it be created automatically.&lt;/p&gt;

&lt;p&gt;A real case: a project's user profile cache used &lt;code&gt;opsForHash().putAll(key, map)&lt;/code&gt; in the code and never set an expiration. Three months after launch, this Hash had grown to more than 20 million keys and filled the entire instance. When the memory alarm went off, the first things killed by &lt;code&gt;allkeys-lru&lt;/code&gt; were login sessions.&lt;/p&gt;

&lt;p&gt;The fix is not to add memory, but to add one line, &lt;code&gt;expire&lt;/code&gt;, where the write happens. For every method that writes to cache, TTL must be a required parameter and must not have a default value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 2:&lt;/strong&gt; &lt;code&gt;maxmemory&lt;/code&gt; &lt;strong&gt;set to 100% of physical memory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when it fills up? &lt;code&gt;used_memory&lt;/code&gt; indeed won't exceed the limit, but &lt;code&gt;used_memory_rss&lt;/code&gt; will. The child process forked during BGSAVE performs copy-on-write and needs extra physical memory. Linux's OOM Killer will kill the Redis process, leaving a &lt;code&gt;Killed&lt;/code&gt; in the logs, with no Redis error message of its own.&lt;/p&gt;

&lt;p&gt;More insidious is swap. When physical memory is insufficient, the system swaps Redis pages to disk, and &lt;code&gt;mem_fragmentation_ratio&lt;/code&gt; will drop below 1. At this point Redis can still run, but every access to cold data is disk I/O, and latency goes from 0.2 ms to 20 ms. Monitoring shows no errors, and it just feels like "Redis has gotten slower."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 3: Looking only at&lt;/strong&gt; &lt;code&gt;used_memory&lt;/code&gt;&lt;strong&gt;, not at&lt;/strong&gt; &lt;code&gt;used_memory_rss&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Redis freed 1GB of data, and &lt;code&gt;used_memory&lt;/code&gt; drops immediately, but &lt;code&gt;used_memory_rss&lt;/code&gt; does not necessarily drop. jemalloc does not always return memory to the operating system.&lt;/p&gt;

&lt;p&gt;This does not count as a leak. As long as the fragmentation rate stays within 1.5, there is no need to worry about it. But if &lt;code&gt;used_memory_rss&lt;/code&gt; keeps rising while &lt;code&gt;used_memory&lt;/code&gt; does not increase, then it is real fragmentation. Enable &lt;code&gt;activedefrag yes&lt;/code&gt;, or find an off-peak period and restart once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 4:&lt;/strong&gt;&lt;code&gt;volatile-lru&lt;/code&gt; &lt;strong&gt;configured with a bunch of keys without TTL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We covered the mechanism earlier; now let's look at the symptom. It goes like this: memory isn't full, yet write commands return &lt;code&gt;OOM command not allowed&lt;/code&gt;. Ops takes one look at &lt;code&gt;maxmemory&lt;/code&gt;, sees there's still headroom, and is completely baffled.&lt;/p&gt;

&lt;p&gt;One command to troubleshoot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli info keyspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output has two key columns per db: the total count &lt;code&gt;keys&lt;/code&gt; and the number of keys with a TTL &lt;code&gt;expires&lt;/code&gt;. If &lt;code&gt;expires&lt;/code&gt; is 0 or far smaller than &lt;code&gt;keys&lt;/code&gt;, then the &lt;code&gt;volatile-*&lt;/code&gt; policy is basically equivalent to &lt;code&gt;noeviction&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Best practices checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always use &lt;code&gt;allkeys-lru&lt;/code&gt; for cache data, and give &lt;strong&gt;every&lt;/strong&gt; key a TTL — belt and suspenders&lt;/li&gt;
&lt;li&gt;When co-locating cache with "data that must not be lost," use &lt;code&gt;volatile-lru&lt;/code&gt;, and also run a script periodically to scan for keys without a TTL&lt;/li&gt;
&lt;li&gt;Put sessions, verification codes, and rate-limit counters in a separate instance or a separate db — don't let them compete with business cache for memory&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;maxmemory&lt;/code&gt;, leave at least 25% of physical memory for fork, replication buffers, and client buffers&lt;/li&gt;
&lt;li&gt;Enable &lt;code&gt;lazyfree-lazy-eviction yes&lt;/code&gt; to avoid blocking the main thread when deleting large keys&lt;/li&gt;
&lt;li&gt;Wire the growth rate of &lt;code&gt;evicted_keys&lt;/code&gt; into alerting, with thresholds set per workload — for example, alert if more than 1000 are added within 5 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most critical item on the list is this one. The vast majority of Redis memory incidents are not due to a lack of monitoring, but because monitoring only looks at memory usage and not eviction rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;V. Performance Comparison and Technology Selection&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Conclusion: The CPU overhead differences between eviction policies are very small. The key to selection is "whether the data can be lost," not "which one is faster."&lt;/p&gt;

&lt;p&gt;Comparison of eviction overhead across different policies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PolicyExtra overhead per evictionHit rate performanceUse case&lt;/strong&gt;noevictionNone (directly rejects writes)Not applicableStoring data that cannot be lost, such as queues and configurationsallkeys-lruSamples 5 keys and compares timestampsGood for hotspot drift scenariosPure cache instancesallkeys-lfuSampling + counter comparisonBetter for stable hotspot scenariosRecommendation slots, leaderboardsallkeys-randomAlmost zeroWorst, not recommendedAlmost never usedvolatile-lruSame as allkeys-lruDepends on TTL coverageMixed deployment of cache and persistent datavolatile-ttlCompares remaining TTLModerateScenarios with clear time-based eviction&lt;/p&gt;

&lt;p&gt;A few empirically measured rules of thumb:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;maxmemory-samples&lt;/code&gt; Adjusting from 5 to 10 improves the hit rate by about 1~3 percentage points, while CPU overhead increases by less than 1%. It is worth tuning.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lazyfree-lazy-eviction&lt;/code&gt; Once enabled, the latency of evicting large keys drops from tens of milliseconds to the microsecond level, at the cost of delayed memory reclamation by a few seconds. It is recommended to enable it in high-concurrency write scenarios.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;activedefrag&lt;/code&gt; Continuously consumes CPU (by default occupying one background thread). Do not enable it when the fragmentation rate is below 1.5.&lt;/p&gt;

&lt;p&gt;For the decision order in technology selection, I suggest the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Would losing this data cause a business failure? If yes → &lt;code&gt;noeviction&lt;/code&gt;, use a separate instance.&lt;/li&gt;
&lt;li&gt;If not, but it is mixed with other data in the same instance? → &lt;code&gt;volatile-lru&lt;/code&gt;, force all cache keys to have a TTL.&lt;/li&gt;
&lt;li&gt;Is the entire instance used as cache? → &lt;code&gt;allkeys-lru&lt;/code&gt;, simple and direct.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not choose LFU just to be "more precise." The counter decay parameter in LFU is very difficult to tune. If it is not tuned well, "new hotspots can never get in" because the counters of old keys are too high. Unless you have a clear judgment that hotspots are stable, LRU is more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;VI. Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Deleting keys after Redis memory is full is designed behavior, not a failure. The real questions to ask are three: which batch of keys is being deleted, why them, and will critical data be deleted next time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;maxmemory-policy&lt;/code&gt; determines the candidate pool. &lt;code&gt;allkeys-*&lt;/code&gt; deletes permanent data along with everything else, while &lt;code&gt;volatile-*&lt;/code&gt; only touches keys that have a TTL. Choosing &lt;code&gt;volatile-*&lt;/code&gt; without setting a TTL on your keys turns the instance into &lt;code&gt;noeviction&lt;/code&gt;, and every write request returns an error.&lt;/p&gt;

&lt;p&gt;Don't push &lt;code&gt;maxmemory&lt;/code&gt; all the way to your physical memory limit. Leave at least 25% for fork, replication buffers, and client buffers; otherwise the OOM killer will make a much harsher decision for you at some ungodly hour.&lt;/p&gt;

&lt;p&gt;For monitoring, &lt;code&gt;used_memory&lt;/code&gt; only tells you "almost full," while the growth rate of &lt;code&gt;evicted_keys&lt;/code&gt; tells you "eviction has already started." Wiring the latter into your alerts is far more useful than staring at memory usage.&lt;/p&gt;

&lt;p&gt;At the code level, a TTL should be a required field when writing to the cache, not an optional one. Only if you hold this line does &lt;code&gt;volatile-lru&lt;/code&gt; mean anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;References&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://redis.io/docs/latest/develop/reference/eviction/" rel="noopener noreferrer"&gt;Key eviction - Redis Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://redis.io/docs/latest/commands/info/" rel="noopener noreferrer"&gt;INFO - Redis Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://redis.io/docs/latest/commands/memory-usage/" rel="noopener noreferrer"&gt;MEMORY USAGE - Redis Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/config/" rel="noopener noreferrer"&gt;Redis configuration - Redis Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-data/redis/reference/" rel="noopener noreferrer"&gt;Spring Data Redis Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>redis</category>
      <category>java</category>
      <category>caching</category>
      <category>performance</category>
    </item>
    <item>
      <title>LLM JSON Parsing Always Fails? Java Structured Output in Practice</title>
      <dc:creator>nicholas</dc:creator>
      <pubDate>Tue, 03 Nov 2026 15:15:43 +0000</pubDate>
      <link>https://dev.to/nicholas1991/llm-json-parsing-always-fails-java-structured-output-in-practice-2pab</link>
      <guid>https://dev.to/nicholas1991/llm-json-parsing-always-fails-java-structured-output-in-practice-2pab</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A week after the customer service ticket extraction feature launched, the logs were full of JsonParseException. Sometimes the model wraps JSON in a Markdown code block; sometimes it prepends a line like "Sure, here are the results"; and the field name can even change from orderNo to order_no. Adding regexes, truncating strings, and tweaking prompts holds up for a couple of days before it breaks again. This article explains LangChain4j's structured output: how to push format constraints down to the protocol layer, and how to add business validation that the schema cannot handle. The code is a complete project; copy it and it runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. What This Problem Actually Is: Why Does JSON Returned by LLMs Always Break&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JSON parsing failures are not because the model is "disobedient"; they happen because you only ask it with prompts and do not constrain it at the protocol layer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The five most common ways of asking a model to return JSON go wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrapped in a code block: it returns a json block wrapped in three backticks, and Jackson immediately reports "Unexpected character '`'".&lt;/li&gt;
&lt;li&gt;Prefixes and suffixes: "Sure, I've organized it for you as follows:" followed by JSON. You slice from &lt;code&gt;{&lt;/code&gt; to &lt;code&gt;}&lt;/code&gt;, and it breaks again when curly braces appear inside string values.&lt;/li&gt;
&lt;li&gt;Field drift: this time &lt;code&gt;orderNo&lt;/code&gt;, next time &lt;code&gt;order_no&lt;/code&gt;, and the time after that &lt;code&gt;order_number&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Type drift: the amount should be a number, but the model gives you &lt;code&gt;"399 元"&lt;/code&gt;, and parsing &lt;code&gt;BigDecimal&lt;/code&gt; blows up immediately.&lt;/li&gt;
&lt;li&gt;Enums improvising: you define &lt;code&gt;REFUND&lt;/code&gt; and &lt;code&gt;LOGISTICS&lt;/code&gt;, and it returns &lt;code&gt;"退款"&lt;/code&gt; and &lt;code&gt;"refund_request"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many people's first reaction is to change the prompt: add "must return pure JSON", add "do not explain", add few-shot examples. It works in the short term, then breaks again a couple of days later. The reason is no mystery:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prompts are soft constraints. When generating each token, the model can still choose any word.&lt;/li&gt;
&lt;li&gt;As context grows longer, earlier format requirements get diluted. Ticket conversations are often thousands of words long, so the model is more likely to "casually" add an explanation.&lt;/li&gt;
&lt;li&gt;Different models understand "JSON" differently. Some think code blocks count as JSON; some write numbers as strings.&lt;/li&gt;
&lt;li&gt;Even low temperature is not zero probability. As long as one token goes off track, the whole response is ruined.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some people also think that setting temperature to 0 makes things stable. Temperature only affects the sampling distribution; it does not guarantee that the same input always produces the same output: server-side batching, model version canary releases, and changes in context length can all make results drift. What is more troublesome is "half-correct" output—four out of five fields are correct, parsing succeeds, and bad data is silently written to the database. A total failure at least triggers an alert; a half-correct result quietly pollutes reports.&lt;/p&gt;

&lt;p&gt;Hand-written cleanup code is equally unreliable. Regexes can only cover the failure modes you have seen; &lt;code&gt;substring(indexOf("{"), lastIndexOf("}"))&lt;/code&gt; truncates the JSON when &lt;code&gt;}&lt;/code&gt; appears inside a string value; worse, swallowing parse exceptions and returning &lt;code&gt;null&lt;/code&gt; pushes the null pointer to the business layer and doubles the debugging cost.&lt;/p&gt;

&lt;p&gt;Cleanup code also has a hidden cost: it mixes "format problems" and "business problems" together. When parsing fails, you add a regex; when a field is wrong, you add &lt;code&gt;if&lt;/code&gt;; six months later nobody dares to touch that code, because nobody knows which line is compensating for which model's which quirk. Structured output pulls this back into the type system: if something breaks, it is a compile error or a parse exception, and the boundary is clear.&lt;/p&gt;

&lt;p&gt;Here the problem needs to be split into two layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LayerSpecific problemWho can solve it&lt;/strong&gt;ShapeIs it valid JSON, are the field names correct, are the types correct, are the enum values validJSON SchemaSemanticsIs the amount a refund amount or an order amount, is the classification correct, is the order number the one for this orderBusiness validation&lt;/p&gt;

&lt;p&gt;The schema can guarantee that &lt;code&gt;amount&lt;/code&gt; is a number, but it cannot guarantee that the number comes from "refund 399 yuan" rather than "original price 599 yuan". The schema can guarantee that &lt;code&gt;category&lt;/code&gt; is one of the four enums, but it cannot guarantee that the model did not classify a logistics issue as a refund.&lt;/p&gt;

&lt;p&gt;In one sentence: &lt;strong&gt;leave format to the schema, facts to validation, and failures to bounded retries.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. How the Underlying Mechanism Actually Works: How JSON Schema Constrains LLM Output&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Structured output compiles Java types into a JSON Schema, sends it to the model with the request, constrains the generation phase with the schema, and then deserializes the returned text back into an object.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The whole chain has three steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 1: Translate Java Classes into a Schema&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;LangChain4j uses reflection to read your return type and generates the schema field by field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;String&lt;/code&gt; → &lt;code&gt;"type": "string"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;int&lt;/code&gt; / &lt;code&gt;Integer&lt;/code&gt; → &lt;code&gt;"type": "integer"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BigDecimal&lt;/code&gt; / &lt;code&gt;double&lt;/code&gt; → &lt;code&gt;"type": "number"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enum&lt;/code&gt; → &lt;code&gt;"type": "string"&lt;/code&gt; plus the &lt;code&gt;enum&lt;/code&gt; whitelist&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; → &lt;code&gt;"type": "array"&lt;/code&gt; plus &lt;code&gt;items&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Nested objects → &lt;code&gt;"type": "object"&lt;/code&gt;; recursive structures are closed off with &lt;code&gt;$defs&lt;/code&gt; and &lt;code&gt;$ref&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;@Description&lt;/code&gt; on a field gets written into the schema's &lt;code&gt;description&lt;/code&gt;. Don't underestimate this one line of text — it's mainly what the model relies on to decide "what should go in this field." The &lt;code&gt;@JsonProperty(required = true)&lt;/code&gt; on a field goes into the &lt;code&gt;required&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;This translation is recursive. &lt;code&gt;List&amp;lt;TicketItem&amp;gt;&lt;/code&gt; produces an array, and the element objects get expanded in turn; self-referencing types don't recurse infinitely — once LangChain4j detects a cycle, it switches to &lt;code&gt;$ref&lt;/code&gt; pointing at &lt;code&gt;$defs&lt;/code&gt;. So you can put tree structures in your DTO and still get a valid schema, it's just that the deeper the nesting, the higher the odds the model fills something in wrong.&lt;/p&gt;

&lt;p&gt;A schema generated from a ticket class looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderNo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"订单号，格式为 ORD- 加 8 位数字"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"REFUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LOGISTICS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INVOICE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OTHER"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"涉及金额，单位元；对话里没提金额就填 0"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;required&lt;/code&gt; here is empty. Only fields marked &lt;code&gt;@JsonProperty(required = true)&lt;/code&gt; make it in. Before sending to OpenAI, strict mode does another rewrite pass: it stuffs all the keys into &lt;code&gt;required&lt;/code&gt; and turns the types of fields not marked required into nullable forms, such as &lt;code&gt;["string", "null"]&lt;/code&gt; and &lt;code&gt;["number", "null"]&lt;/code&gt;. So "required" under strict mode only guarantees the key is present, not that the value is non-null. I'll dig into this trap later.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 2: Put the Schema into the Request&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The request carries the schema via &lt;code&gt;ResponseFormat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ResponseFormat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResponseFormatType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jsonSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OpenAI adapter layer translates it into &lt;code&gt;response_format: {"type": "json_schema", ...}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;strict&lt;/code&gt; is enabled, the server enforces the constraint at the decoding stage: for every token generated, candidates that violate the schema get masked out outright. The model isn't "trying its best to get it right" — it "can't write it wrong." That's a whole different thing from asking it nicely in a prompt.&lt;/p&gt;

&lt;p&gt;Capabilities vary widely across vendors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI's Structured Outputs supports a common subset of JSON Schema, with hard constraints in strict mode.&lt;/li&gt;
&lt;li&gt;Inference frameworks like vLLM use guided decoding and can also mask tokens according to the schema.&lt;/li&gt;
&lt;li&gt;Plenty of OpenAI-compatible services only support &lt;code&gt;json_object&lt;/code&gt;. It only guarantees "the whole thing is valid JSON" — field names, types, and enums are all ignored.&lt;/li&gt;
&lt;li&gt;Some services just ignore &lt;code&gt;response_format&lt;/code&gt; entirely — whatever you pass, they act like they never saw it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;json_object&lt;/code&gt; and &lt;code&gt;json_schema&lt;/code&gt; often get conflated. The former only guarantees syntax; the latter governs field names and types as well. A concrete example: you ask it to extract &lt;code&gt;{"amount": 399}&lt;/code&gt;, and json_object mode might return &lt;code&gt;{"退款金额": "399元"}&lt;/code&gt; — perfectly valid syntax, and your DTO still can't handle it. Schema mode won't even let the key name &lt;code&gt;amount&lt;/code&gt; be changed.&lt;/p&gt;

&lt;p&gt;LangChain4j uses capability flags to decide which path to take. Only if the model's &lt;code&gt;supportedCapabilities()&lt;/code&gt; includes &lt;code&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/code&gt; does it go with native schema; otherwise it appends the format instructions to the user message. The fallback mode isn't completely useless — it pastes the schema and field descriptions into the user message, so the structure the model sees is accurate, and most of the time it gets it right. The difference is between "most of the time" and "always."&lt;/p&gt;

&lt;p&gt;There's a well-hidden trap here: &lt;code&gt;OpenAiChatModel&lt;/code&gt; &lt;strong&gt;doesn't declare this capability by default.&lt;/strong&gt; You have to explicitly declare &lt;code&gt;.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)&lt;/code&gt; for it to turn the capability flag on. Without that line, &lt;code&gt;AiServices&lt;/code&gt; will still convert the result into an object, but under the hood it's "pasting a format requirement into the prompt," and stability drops off immediately. A lot of people say "I'm using AiServices, so why does it still fall over?" — and the answer is usually right here.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 3: Turn the Returned Text Back into an Object&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;AiServices&lt;/code&gt; gets the response, it hands it to &lt;code&gt;PojoOutputParser&lt;/code&gt;. The parser doesn't just &lt;code&gt;readValue&lt;/code&gt; right away — it first tries the whole string; if that fails, it scans backwards for the last &lt;code&gt;}&lt;/code&gt; or &lt;code&gt;]&lt;/code&gt;, then matches braces going backwards, skipping over braces that sit inside strings. So code fences and surrounding chatter all get rescued.&lt;/p&gt;

&lt;p&gt;This leniency is a safety net, not a design goal. If field names or types are wrong, it can't save you either — it just throws &lt;code&gt;OutputParsingException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's another case it can't do anything about: the response gets truncated by max_tokens. The JSON is missing a closing brace, brace matching fails, and it throws an exception outright. You can catch this kind of failure early by checking &lt;code&gt;finishReason&lt;/code&gt; — don't wait for Jackson to blow up. Setting max_tokens comfortably above your normal output size is a cheap and effective fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Three Things Schema Can't Cover&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Semantics.&lt;/strong&gt; Schema governs shape. Whose amount it is, whether the category is right, whether the order number belongs to this order — it doesn't concern itself with any of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown values.&lt;/strong&gt; The OpenAI adapter's strict mode puts all properties into &lt;code&gt;required&lt;/code&gt; and also changes the types of fields not marked required into nullable form. The key must be present, and the value can be &lt;code&gt;null&lt;/code&gt;. When the model encounters "the amount isn't mentioned in the conversation," it is more likely to give you &lt;code&gt;null&lt;/code&gt; than to make up a number. So in the DTO, use wrapper types to receive it. If you use &lt;code&gt;int&lt;/code&gt;, Jackson will silently turn &lt;code&gt;null&lt;/code&gt; into 0, and validation sees fake data; if the business does not accept nulls, check it in validation, or add &lt;code&gt;UNKNOWN&lt;/code&gt; to the enum and agree on 0 for the amount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-field rules.&lt;/strong&gt; A constraint like "the refund amount cannot be greater than the order amount" can be written with conditional expressions in JSON Schema, but support varies widely among model vendors. Don't pin business rules on the schema; just write Java validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Hands-On: Writing LangChain4j Structured Output Code Step by Step&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A minimal working project consists of four parts: dependencies, DTOs, the AI Service interface, and validation plus retry. The three examples below go from simple to complete, and each one is a full class.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Dependencies&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;project&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0"&lt;/span&gt;
         &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
         &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelVersion&amp;gt;&lt;/span&gt;4.0.0&lt;span class="nt"&gt;&amp;lt;/modelVersion&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.example&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;structured-output-demo&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;maven.compiler.release&amp;gt;&lt;/span&gt;21&lt;span class="nt"&gt;&amp;lt;/maven.compiler.release&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;project.build.sourceEncoding&amp;gt;&lt;/span&gt;UTF-8&lt;span class="nt"&gt;&amp;lt;/project.build.sourceEncoding&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.langchain4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;langchain4j&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.22.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.langchain4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;langchain4j-open-ai&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.22.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.fasterxml.jackson.core&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jackson-databind&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.22.4&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.slf4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;slf4j-simple&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.0.20&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
​
    &lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.maven.plugins&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;maven-compiler-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.16.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Example 1: AiServices Automatically Generates the Schema&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This is the least effort approach. Define an interface, set the return type to a DTO, and LangChain4j handles schema generation, request sending, and result parsing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.structured&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.ChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.openai.OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.output.structured.Description&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.service.AiServices&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.service.UserMessage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.math.BigDecimal&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;langchain4j&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Capability&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TicketExtractionDemo&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="no"&gt;REFUND&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;LOGISTICS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;INVOICE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;OTHER&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"订单号，格式为 ORD- 加 8 位数字"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;orderNo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"问题分类：REFUND 退款、LOGISTICS 物流、INVOICE 发票、OTHER 其他"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Category&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"涉及金额，单位元；对话里没提金额就填 0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"优先级，1 最低，5 最高；催得急或涉及投诉填 4 以上"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"用户诉求摘要，不超过 30 个字"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;TicketAssistant&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@UserMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
                从下面的客服对话中抽取工单信息。
                只抽对话里明确出现的内容，不要推测。
​
                对话：
                {{it}}
                """&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dialogue&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// 这两行决定走原生 JSON Schema，而不是在 prompt 里贴格式说明&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supportedCapabilities&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;strictJsonSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"模型能力: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supportedCapabilities&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;TicketAssistant&lt;/span&gt; &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AiServices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TicketAssistant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
                用户：我上周买的耳机，订单 ORD-20261012，到现在还没发货。
                客服：我帮您查一下物流。
                用户：不用查了，直接退款，399 元，太慢了。
                """&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"订单号: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderNo&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"分类: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"金额: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"优先级: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"摘要: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down the key behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{{it}}&lt;/code&gt; in &lt;code&gt;@UserMessage&lt;/code&gt; is a placeholder corresponding to the method's only parameter. You don't need to manually concatenate strings.&lt;/li&gt;
&lt;li&gt;The return type is &lt;code&gt;Ticket&lt;/code&gt;, not &lt;code&gt;String&lt;/code&gt;. This single signature tells LangChain4j to generate a schema and deserialize the result.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Description&lt;/code&gt; is not a comment; it goes into the schema. The more abstract the field name, the clearer the description must be. For example, if &lt;code&gt;priority&lt;/code&gt; doesn't say '1 to 5', the model will happily give you 10.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;temperature(0.0)&lt;/code&gt; reduces randomness, but don't expect it to replace the schema.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;System.out.println(model.supportedCapabilities())&lt;/code&gt; is for troubleshooting. If the output lacks &lt;code&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/code&gt;, you're in prompt fallback mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Example 2: Hand-Write the Schema Without Going Through the Interface Proxy&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Some scenarios are not suitable for &lt;code&gt;AiServices&lt;/code&gt;: the schema is assembled at runtime, you need to reuse a &lt;code&gt;ChatModel&lt;/code&gt;, or you want to see exactly what request was sent. In these cases, construct &lt;code&gt;ChatRequest&lt;/code&gt; directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.structured&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.data.message.SystemMessage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.data.message.UserMessage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.ChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.ChatRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.ResponseFormat&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.ResponseFormatType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.json.JsonObjectSchema&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.json.JsonSchema&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.response.ChatResponse&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.openai.OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.List&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;langchain4j&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Capability&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RawSchemaDemo&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supportedCapabilities&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;strictJsonSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;JsonSchema&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JsonSchema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ticket"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rootElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonObjectSchema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addStringProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderNo"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"订单号，格式为 ORD- 加 8 位数字"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addEnumProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"category"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"REFUND"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"LOGISTICS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"INVOICE"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OTHER"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addNumberProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"涉及金额，单位元，没有就填 0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addIntegerProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"priority"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"优先级，1 到 5"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addStringProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"summary"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"用户诉求摘要，不超过 30 个字"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderNo"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"category"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"priority"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"summary"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;ChatRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="nc"&gt;SystemMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"你是客服工单抽取器，只输出 JSON。"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                        &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"订单 ORD-20261012 一直没发货，我要退款 399 元。"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;responseFormat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResponseFormat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResponseFormatType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jsonSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;ChatResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;aiMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JsonObjectSchema.builder()&lt;/code&gt; provides a series of convenience methods; &lt;code&gt;addStringProperty&lt;/code&gt;, &lt;code&gt;addEnumProperty&lt;/code&gt;, and &lt;code&gt;addNumberProperty&lt;/code&gt; all take two parameters: "name + description." &lt;code&gt;required&lt;/code&gt; specifies which fields must be present, and &lt;code&gt;additionalProperties(false)&lt;/code&gt; prevents the model from adding extras.&lt;/p&gt;

&lt;p&gt;The advantage of hand-writing the schema is control: field names, enum values, descriptions, and required fields are all in your hands. The downside is that it easily drifts out of sync with the DTO. Change one side and forget the other, and parsing will still break. My advice: for scenarios the interface proxy can cover, prefer Example 1; only hand-write when the schema needs to be generated dynamically.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Example 3: Business Validation + Limited Retry&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The first two examples solve shape; this one solves semantics. Core idea: after parsing succeeds, don't return immediately—run the rules first. If there's a problem, feed "which rule failed" back to the model and have it rewrite. Retry at most twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.structured&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.fasterxml.jackson.databind.ObjectMapper&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.data.message.SystemMessage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.data.message.UserMessage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.ChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.ChatRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.ResponseFormat&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.request.ResponseFormatType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.chat.response.ChatResponse&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.openai.OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.model.output.structured.Description&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dev.langchain4j.service.output.JsonSchemas&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.math.BigDecimal&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.ArrayList&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.List&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.regex.Pattern&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;langchain4j&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Capability&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ValidatedExtractor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt; &lt;span class="no"&gt;ORDER_NO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"^ORD-\\d{8}$"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt; &lt;span class="no"&gt;MAPPER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="no"&gt;MAX_ATTEMPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ValidatedExtractor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"订单号，格式为 ORD- 加 8 位数字"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;orderNo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"问题分类：REFUND 退款、LOGISTICS 物流、INVOICE 发票、OTHER 其他"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"退款或涉及金额，单位元；没有就填 0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"优先级，1 最低，5 最高"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"用户诉求摘要，不超过 30 个字"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ResponseFormat&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ResponseFormat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResponseFormatType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jsonSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonSchemas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jsonSchemaFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;MAX_ATTEMPTS&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;userText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
                        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\n\n上一次输出有以下问题，请逐条修正后重新输出：\n- "&lt;/span&gt;
                        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n- "&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
            &lt;span class="nc"&gt;ChatRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                            &lt;span class="nc"&gt;SystemMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"只做信息抽取。不确定的字段按默认值填，不要编造。"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                            &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userText&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;responseFormat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
            &lt;span class="nc"&gt;ChatResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;aiMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;MAPPER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"第 "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" 次校验失败: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"输出不是合法 JSON："&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"第 "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" 次解析失败，原始输出："&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"连续 "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;MAX_ATTEMPTS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" 次没有拿到合格结果，最后的问题："&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderNo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="no"&gt;ORDER_NO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderNo&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderNo 必须是 ORD- 加 8 位数字，当前是 "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderNo&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;category&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"REFUND"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"LOGISTICS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"INVOICE"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OTHER"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"category 只能是 REFUND、LOGISTICS、INVOICE、OTHER"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signum&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compareTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"100000"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amount 必须在 0 到 100000 之间"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"priority 必须在 1 到 5 之间"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supportedCapabilities&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;strictJsonSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;ValidatedExtractor&lt;/span&gt; &lt;span class="n"&gt;extractor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValidatedExtractor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extractor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"订单 ORD-20261012 一直没发货，我要退款 399 元。"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MAPPER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writerWithDefaultPrettyPrinter&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several design points in this code worth explaining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The schema only declares the shape; validation rules are written in Java.&lt;/strong&gt; &lt;code&gt;category&lt;/code&gt; in &lt;code&gt;Ticket&lt;/code&gt; deliberately uses &lt;code&gt;String&lt;/code&gt; instead of an enum. In native schema mode, it is an enum whitelist; when third-party compatible services ignore the schema, it is just a free-form string. Validation catches both cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feed the failure reason back.&lt;/strong&gt; Just saying "regenerate" is useless; the model doesn't know what went wrong. Tell it "orderNo must be ORD- followed by 8 digits, and the current value is 20261012," and the second attempt will basically get it right. This is what separates effective retries from ineffective ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cap the retries.&lt;/strong&gt; If it still fails after three attempts, the input itself is the problem, and continuing to burn tokens is pointless. Throw an exception directly and hand it off to a human or a dead-letter queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The raw output must be logged.&lt;/strong&gt; When parsing fails, &lt;code&gt;raw&lt;/code&gt; is the only clue. In production, wiring this log into alerting is far more useful than guessing afterward why the model went off the rails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One retry is one full call.&lt;/strong&gt; Every retry resends the system prompt, the user's original text, and the schema, so the cost is very real. Therefore, list all validation rules at once; don't drip-feed them one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Pitfalls and Best Practices: The 8 Places Where Structured Output Most Easily Goes Wrong&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Eighty percent of structured output pitfalls aren't in schema syntax; they're in "assuming it works as soon as you configure it."&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Just writing "return JSON".&lt;/strong&gt; That's not a constraint. Either use &lt;code&gt;ResponseFormat&lt;/code&gt; + &lt;code&gt;JsonSchema&lt;/code&gt;, or accept that it'll add code blocks whenever it feels like it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming&lt;/strong&gt; &lt;code&gt;AiServices&lt;/code&gt; &lt;strong&gt;will always use the native schema.&lt;/strong&gt; If the model doesn't declare &lt;code&gt;RESPONSE_FORMAT_JSON_SCHEMA&lt;/code&gt;, the framework falls back to prompt mode. For OpenAI models, remember to add &lt;code&gt;.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)&lt;/code&gt;, and print &lt;code&gt;supportedCapabilities()&lt;/code&gt; once before going live to confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You also want optional fields in strict mode.&lt;/strong&gt; OpenAI’s strict mode puts every property into &lt;code&gt;required&lt;/code&gt;, while fields not marked as required allow &lt;code&gt;null&lt;/code&gt;. The keys are always there; the values may be empty. On the Java side, use wrapper types (&lt;code&gt;Integer&lt;/code&gt;, &lt;code&gt;BigDecimal&lt;/code&gt;). If you use &lt;code&gt;int&lt;/code&gt;, Jackson will silently convert &lt;code&gt;null&lt;/code&gt; to 0, so validation ends up with fake data—harder to track down than an error. If you really don’t want nulls, add &lt;code&gt;UNKNOWN&lt;/code&gt; to enums, agree on 0 for amounts, and handle it in validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For amounts, use&lt;/strong&gt; &lt;code&gt;double&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; Floating-point errors in monetary amounts are a disaster. Use &lt;code&gt;BigDecimal&lt;/code&gt; in the DTO and &lt;code&gt;number&lt;/code&gt; in the schema, and Jackson can convert them correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use date fields directly as&lt;/strong&gt; &lt;code&gt;LocalDate&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; When generating the schema, &lt;code&gt;LocalDate&lt;/code&gt; isn't treated as &lt;code&gt;string&lt;/code&gt;; instead, it gets translated into an object with no fields, leaving the model to fill it in blindly. The ObjectMapper on the parsing side also isn't guaranteed to have JavaTimeModule registered. Accept &lt;code&gt;String&lt;/code&gt; in the DTO, then convert it to &lt;code&gt;LocalDate&lt;/code&gt; in the business layer. Two extra lines save you an afternoon of debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The prompt and the schema each say something different.&lt;/strong&gt; The prompt pastes the field structure again, so the model may output according to the old version in the prompt. The prompt should only cover the business rules (what counts as a refund, how priority is determined), and leave the structure to the schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The DTO is both wide and deep.&lt;/strong&gt; Twenty fields and three levels of nesting — the schema alone takes up several hundred tokens, and the model also tends to miss fields. Split it into two extractions: first extract the order number and category, then extract the details as needed. Both accuracy and cost improve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries with no cap and no idempotency.&lt;/strong&gt; Retrying endlessly on validation failure means that with input that never passes, the bill can go up tenfold. Cap it at two or three attempts, log the raw output and issue list for each attempt, and make writes idempotent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few more that are less common but potentially fatal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field names are too casually chosen.&lt;/strong&gt; With names like &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, and &lt;code&gt;tmp&lt;/code&gt;, the model can only guess. Use semantic English names such as &lt;code&gt;orderNo&lt;/code&gt; and &lt;code&gt;refundAmount&lt;/code&gt;, and add &lt;code&gt;@Description&lt;/code&gt; to fill in the details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgot&lt;/strong&gt; &lt;code&gt;additionalProperties(false)&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; The model can add arbitrary fields to the object. These fields cannot enter the DTO, waste output tokens for nothing, and also show that it did not strictly follow your structure. Turn it off when writing the schema by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming output paired with a POJO return value.&lt;/strong&gt; The streaming method in &lt;code&gt;AiServices&lt;/code&gt; returns &lt;code&gt;TokenStream&lt;/code&gt; and cannot directly return a DTO. If you want structured results, do not enable streaming, or parse the result yourself after streaming is complete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party compatible services silently ignore&lt;/strong&gt; &lt;code&gt;response_format&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; It does not report an error and still returns a passage of prose. So validation and parsing fallbacks are not optional; they are mandatory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After launch, monitor three metrics: parse success rate, validation failure rate, and average retry count. If the parse success rate is low, check the model's capability flags and max_tokens first; if the validation failure rate is high, the business rules in the prompt aren't clearly defined; if retry counts rise, it's usually because the model version changed or the input distribution shifted. These three metrics are far more useful than "it feels less accurate lately."&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Performance Comparison and Technology Selection: JSON Mode, JSON Schema, or Function Calling&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For extraction tasks in production, prefer JSON Schema; use Function Calling only when you want the model to decide whether to call a tool.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What it guarantees&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Extra overhead&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Suitable scenarios&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain text + custom parsing&lt;/td&gt;
&lt;td&gt;Guarantees nothing&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;One-off scripts, toy demos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON mode&lt;/td&gt;
&lt;td&gt;The output as a whole is valid JSON&lt;/td&gt;
&lt;td&gt;Very small&lt;/td&gt;
&lt;td&gt;Older models; compatible services that only support json_object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON Schema&lt;/td&gt;
&lt;td&gt;Field names, types, enums, required fields&lt;/td&gt;
&lt;td&gt;The schema consumes input tokens; decoding is slightly slower&lt;/td&gt;
&lt;td&gt;Production extraction; the preferred choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function Calling&lt;/td&gt;
&lt;td&gt;Tool parameter structure, and the model decides whether to call a tool&lt;/td&gt;
&lt;td&gt;Tool definitions consume tokens&lt;/td&gt;
&lt;td&gt;Agents, where the model needs to choose actions autonomously&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bulk of the latency is always output tokens and network round trips. The overhead from constrained decoding is barely perceptible on outputs of a few hundred tokens, like ticket extraction. What really sends costs out of control is retries: one failed retry means double the tokens and double the latency. So get the schema right the first time, and list every validation rule up front.&lt;/p&gt;

&lt;p&gt;Schemas aren't free. They ride along with every request — the more fields and the deeper the nesting, the higher your input token count. Keeping it to ten fields per DTO and no more than two levels of nesting is a comfortable range. Beyond that, consider splitting the call rather than cramming everything in.&lt;/p&gt;

&lt;p&gt;Choosing is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal systems doing information extraction, classification, or scoring: JSON Schema + Java validation, no hesitation.&lt;/li&gt;
&lt;li&gt;Hooking up to a third-party OpenAI-compatible service: first probe whether it supports &lt;code&gt;response_format&lt;/code&gt;; if not, fall back to JSON mode — and not a single line of validation logic can be skipped.&lt;/li&gt;
&lt;li&gt;The model needs to decide which tool to call based on user intent: Function Calling. At heart it's also structured output, except what gets structured is "actions."&lt;/li&gt;
&lt;li&gt;You just want nicer-looking logs: don't bother — regex extraction of key fields is enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;6. Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Structured output comes down to three sentences:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shape goes to the schema.&lt;/strong&gt; Use &lt;code&gt;ResponseFormat&lt;/code&gt; with &lt;code&gt;JsonSchema&lt;/code&gt; to push the constraints down to the protocol layer; returning a POJO from &lt;code&gt;AiServices&lt;/code&gt; auto-generates the schema, but make sure the model actually declares the capability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Facts go to validation.&lt;/strong&gt; The schema guarantees &lt;code&gt;amount&lt;/code&gt; is a number; it can't guarantee it's a refund amount. Order number format, amount ranges, enum whitelists, cross-field relationships — hard-code all of it in Java.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failures go to bounded retries.&lt;/strong&gt; Feed back to the model exactly which rule failed, at most two or three times, and log the raw output. Past that, give up — don't burn tokens out of spite.&lt;/p&gt;

&lt;p&gt;The implementation checklist is six items: narrow the DTO, turn on the schema, think through strict mode, always write validation, cap the retries, keep the raw logs complete.&lt;/p&gt;

&lt;p&gt;When should you not use structured output? Open-ended copywriting, long-form summarization, brainstorming. These tasks have no stable fields; forcing a schema on them just ties the model's hands and leaves the output flat. Structured output is good for "moving information around," not for "creating content."&lt;/p&gt;

&lt;p&gt;One more note for anyone about to ship: build a regression set from twenty real tickets and run it every time you swap models, tweak prompts, or touch the schema. Models aren't compilers — an upgrade won't tell you what's incompatible.&lt;/p&gt;

&lt;p&gt;The model writes the answers; the code grades the paper. If the grading rules aren't clear, no model — however large — will save you.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;References&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.langchain4j.dev/tutorials/structured-outputs" rel="noopener noreferrer"&gt;LangChain4j docs: Structured Outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.langchain4j.dev/tutorials/ai-services" rel="noopener noreferrer"&gt;LangChain4j docs: AI Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://json-schema.org/understanding-json-schema/" rel="noopener noreferrer"&gt;JSON Schema official guide: Understanding JSON Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://central.sonatype.com/artifact/dev.langchain4j/langchain4j" rel="noopener noreferrer"&gt;Maven Central: dev.langchain4j:langchain4j&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>llm</category>
      <category>langchain4j</category>
      <category>json</category>
    </item>
  </channel>
</rss>
