axim-rest-framework

Build Spring Boot REST APIs with Axim REST Framework. Use when creating entities, repositories, services, controllers, error handling, or pagination with the axim-rest-framework (Spring Boot + MyBatis). Covers @XEntity, @XRepository, IXRepository, query derivation, save/modify/upsert, XPagination, XPage, error codes, i18n exceptions, and declarative REST client.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "axim-rest-framework" with this command: npx skills add axim-one/rest-framework/axim-one-rest-framework-axim-rest-framework

Axim REST Framework

Spring Boot + MyBatis lightweight REST framework. Annotation-based entity mapping and repository proxy pattern that minimizes boilerplate while keeping MyBatis SQL control.

Version: 1.1.0 Requirements: Java 17+, Spring Boot 3.3+, MySQL 5.7+/8.0+, MyBatis 3.0+ Repository: https://github.com/Axim-one/rest-framework

Critical Rules

  • SECURITY: axim.rest.session.secret-key MUST be set in production. Without it, tokens have NO signature — anyone can forge a session token.
  • SECURITY: Set spring.profiles.active=prod in production. Non-prod profiles log full request bodies including passwords.
  • @XColumn is only needed for: primary keys, custom column names, or insert/update control. Regular fields auto-map via camelCase → snake_case — do NOT add @XColumn to every field.
  • @XDefaultValue(value="X") alone does NOT work — isDBDefaultUsed defaults to true, so the value is ignored. Must set isDBDefaultUsed=false for literal values.
  • @XRestServiceScan is required on the application class when using @XRestService declarative REST clients.
  • XWebClient beans can be registered via axim.web-client.services.{name}={url} in properties, then injected with @Qualifier.
  • Session token format is NOT JWT — it uses custom Base64(payload).HmacSHA256(signature). Do not use JWT libraries.
  • JSON date format is yyyy-MM-dd HH:mm:ss, not ISO 8601.
  • XSessionResolver auto-detects SessionData subclass parameters — no annotation required on the controller parameter.
  • @XPaginationDefault defaults: page=1, size=10, direction=DESC. Sort without direction defaults to ASC.
  • MANDATORY: Every member variable (Entity, DTO, Request, Response, VO) and every enum item MUST have a detailed Javadoc comment including purpose, example values, format rules, constraints, and allowed values.

Installation

Gradle

repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.Axim-one.rest-framework:core:1.2.1'
    implementation 'com.github.Axim-one.rest-framework:rest-api:1.2.1'
    implementation 'com.github.Axim-one.rest-framework:mybatis:1.2.1'
}

Maven

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.github.Axim-one.rest-framework</groupId>
        <artifactId>core</artifactId>
        <version>1.2.1</version>
    </dependency>
    <dependency>
        <groupId>com.github.Axim-one.rest-framework</groupId>
        <artifactId>rest-api</artifactId>
        <version>1.2.1</version>
    </dependency>
    <dependency>
        <groupId>com.github.Axim-one.rest-framework</groupId>
        <artifactId>mybatis</artifactId>
        <version>1.2.1</version>
    </dependency>
</dependencies>

Application Setup

CRITICAL: All annotations below are required. Add @XRestServiceScan if using @XRestService REST clients.

@ComponentScan({"one.axim.framework.rest", "one.axim.framework.mybatis", "com.myapp"})
@SpringBootApplication
@XRepositoryScan("com.myapp.repository")
@MapperScan({"one.axim.framework.mybatis.mapper", "com.myapp.mapper"})
@XRestServiceScan("com.myapp.client")  // Only if using @XRestService REST clients
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
AnnotationRequiredPurpose
@ComponentScanYesMust include one.axim.framework.rest, one.axim.framework.mybatis, and app packages
@XRepositoryScanYesScans for @XRepository interfaces
@MapperScanYesMust include one.axim.framework.mybatis.mapper + app mapper packages
@XRestServiceScanIf using REST clientScans for @XRestService interfaces, creates JDK proxy beans

application.properties — Complete Reference

# ── DataSource ──
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.config-location=classpath:mybatis-config.xml

# ── Framework: HTTP Client ──
axim.rest.client.pool-size=200                    # Max connection pool (default: 200)
axim.rest.client.connection-request-timeout=30    # seconds (default: 30)
axim.rest.client.response-timeout=30              # seconds (default: 30)
axim.rest.debug=false                             # REST client logging (default: false)

# ── Framework: Gateway ──
axim.rest.gateway.host=http://api-gateway:8080    # Enables gateway mode for @XRestService

# ── Framework: XWebClient Beans ──
axim.web-client.services.userClient=http://user-service:8080    # Named XWebClient bean
axim.web-client.services.orderClient=http://order-service:8080  # Named XWebClient bean

# ── Framework: Session / Token ──
axim.rest.session.secret-key=your-hmac-secret-key # HMAC-SHA256 signing (omit = unsigned)
axim.rest.session.token-expire-days=90            # Token lifetime (default: 90)

# ── Framework: i18n ──
axim.rest.message.default-language=ko-KR          # Default locale (default: ko-KR)
axim.rest.message.language-header=Accept-Language  # Language header (default: Accept-Language)
spring.messages.basename=messages                  # App message files (default: messages)
spring.messages.encoding=UTF-8

mybatis-config.xml

All three elements (objectFactory, plugins, mappers) are required.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="useGeneratedKeys" value="true"/>
        <setting name="defaultExecutorType" value="SIMPLE"/>
        <setting name="defaultStatementTimeout" value="10"/>
        <setting name="callSettersOnNulls" value="true"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <!-- REQUIRED: Entity instantiation -->
    <objectFactory type="one.axim.framework.mybatis.plugin.XObjectFactory"/>
    <!-- REQUIRED: Pagination + result mapping -->
    <plugins>
        <plugin interceptor="one.axim.framework.mybatis.plugin.XResultInterceptor"/>
    </plugins>
    <!-- REQUIRED: Framework internal CRUD mapper -->
    <mappers>
        <mapper class="one.axim.framework.mybatis.mapper.CommonMapper"/>
    </mappers>
</configuration>

Entity Definition

Use @XEntity to map a class to a database table. Fields auto-map using camelCase → snake_case conversion.

@Data
@XEntity("users")
public class User {

    @XColumn(isPrimaryKey = true, isAutoIncrement = true)
    private Long id;

    private String email;
    private String name;

    @XDefaultValue(value = "NOW()", isDBValue = true)
    private LocalDateTime createdAt;

    @XDefaultValue(updateValue = "NOW()", isDBValue = true)
    private LocalDateTime updatedAt;

    @XColumn(insert = false, update = false)
    private String readOnlyField;

    @XIgnoreColumn
    private String transientField;
}

Entity with Schema

@XEntity(value = "orders", schema = "shop")
public class Order { ... }

Entity Inheritance

Parent class fields are automatically included:

public class BaseEntity {
    @XColumn(isPrimaryKey = true, isAutoIncrement = true)
    private Long id;

    @XDefaultValue(value = "NOW()", isDBValue = true)
    private LocalDateTime createdAt;

    @XDefaultValue(updateValue = "NOW()", isDBValue = true)
    private LocalDateTime updatedAt;
}

@Data
@XEntity("partners")
public class Partner extends BaseEntity {
    private String name;
    private String status;
}

@XDefaultValue Patterns

// Pattern 1: Use DB DEFAULT (column omitted from INSERT)
@XDefaultValue(isDBDefaultUsed = true)
private String region;

// Pattern 2: Literal string value on INSERT
@XDefaultValue(value = "ACTIVE", isDBDefaultUsed = false)
private String status;

// Pattern 3: DB expression on INSERT
@XDefaultValue(value = "NOW()", isDBValue = true, isDBDefaultUsed = false)
private LocalDateTime createdAt;

// Pattern 4: Auto-set value on UPDATE
@XDefaultValue(updateValue = "NOW()", isDBValue = true)
private LocalDateTime updatedAt;

Annotations Reference

AnnotationTargetDescription
@XEntity(value, schema)ClassMaps class to database table
@XColumn(value, isPrimaryKey, isAutoIncrement, insert, update)FieldColumn mapping with options
@XDefaultValue(value, updateValue, isDBDefaultUsed, isDBValue)FieldDefault values for INSERT/UPDATE
@XIgnoreColumnFieldExcludes field from DB mapping
@XRepositoryInterfaceMarks repository for proxy generation
@XRepositoryScan(basePackages)ClassScans for @XRepository interfaces

Repository

Extend IXRepository<K, T> and annotate with @XRepository:

@XRepository
public interface UserRepository extends IXRepository<Long, User> {
    User findByEmail(String email);
    List<User> findByStatus(String status);
    boolean existsByEmail(String email);
    long countByStatus(String status);
    int deleteByStatusAndName(String status, String name);
}

Repository API

MethodReturnDescription
save(entity)KPK null → INSERT, PK present → Upsert (Composite: all PKs set → upsert)
insert(entity)KPlain INSERT with auto-generated ID (Composite: returns key class)
saveAll(List)KBatch INSERT IGNORE
update(entity)intFull UPDATE (all columns including nulls)
modify(entity)intSelective UPDATE (non-null fields only)
findOne(key)TFind by primary key
findAll()List<T>Find all rows
findAll(pagination)XPage<T>Paginated find all
findWhere(Map)List<T>Find by conditions
findWhere(pagination, Map)XPage<T>Paginated find by conditions
findOneWhere(Map)TFind one by conditions
exists(key)booleanCheck existence by PK
count() / count(Map)longTotal / conditional count
deleteById(key)intDelete by primary key
deleteWhere(Map)intDelete by conditions

CRUD Examples

// save() - Upsert
User user = new User();
user.setName("Alice");
userRepository.save(user);        // INSERT, auto-increment ID set on entity
user.setId(1L);
userRepository.save(user);        // INSERT ... ON DUPLICATE KEY UPDATE

// insert() - Plain INSERT
userRepository.insert(user);

// update() vs modify()
userRepository.update(user);      // SET name='Alice', email=NULL, status=NULL
userRepository.modify(user);      // SET name='Alice' (null fields skipped)

// saveAll() - Batch
userRepository.saveAll(List.of(user1, user2, user3));

// Find
User found = userRepository.findOne(1L);
List<User> active = userRepository.findWhere(Map.of("status", "ACTIVE"));
boolean exists = userRepository.exists(1L);
long count = userRepository.count(Map.of("status", "ACTIVE"));

// Delete
userRepository.deleteById(1L);
userRepository.deleteWhere(Map.of("status", "INACTIVE"));

Composite Primary Key

Entities with multiple primary keys use a key class for IXRepository<K, T>.

// Key class — field names must match entity PK field names
@Data
public class OrderItemKey {
    private Long orderId;
    private Long itemId;
}

// Entity — multiple @XColumn(isPrimaryKey = true)
@Data
@XEntity("order_items")
public class OrderItem {
    @XColumn(isPrimaryKey = true)
    private Long orderId;
    @XColumn(isPrimaryKey = true)
    private Long itemId;
    private int quantity;
    private BigDecimal price;
}

// Repository
@XRepository
public interface OrderItemRepository extends IXRepository<OrderItemKey, OrderItem> {}
// Usage
OrderItemKey key = new OrderItemKey();
key.setOrderId(1L);
key.setItemId(100L);

repository.findOne(key);       // WHERE order_id = ? AND item_id = ?
repository.delete(key);        // WHERE order_id = ? AND item_id = ?
repository.save(orderItem);    // All PKs set → upsert, any null → insert
repository.insert(orderItem);  // Returns OrderItemKey with both PK values

Query Derivation

Declare methods and SQL is auto-generated from the method name.

Supported Prefixes: findBy, findAllBy, countBy, existsBy, deleteBy Condition Combinator: And

@XRepository
public interface OrderRepository extends IXRepository<Long, Order> {
    Order findByOrderNo(String orderNo);                           // WHERE order_no = ?
    List<Order> findByUserIdAndStatus(Long userId, String status); // WHERE user_id = ? AND status = ?
    long countByStatus(String status);                             // SELECT COUNT(*) WHERE status = ?
    boolean existsByOrderNo(String orderNo);                       // EXISTS check
    int deleteByUserIdAndStatus(Long userId, String status);       // DELETE WHERE ...
}

Pagination

IMPORTANT: Always use XPagination and XPage for pagination. NEVER create custom pagination classes (e.g., PageRequest, PageResponse, PaginationDTO). The framework handles COUNT, ORDER BY, and LIMIT automatically.

XPagination pagination = new XPagination();
pagination.setPage(1);       // 1-based
pagination.setSize(20);
pagination.addOrder(new XOrder("createdAt", XDirection.DESC));

XPage<User> result = userRepository.findAll(pagination);
result.getTotalCount();   // total rows
result.getPage();         // current page
result.getPageRows();     // rows in this page
result.getHasNext();      // more pages?

// Controller with auto-binding
@GetMapping
public XPage<User> searchUsers(@XPaginationDefault XPagination pagination) {
    return userRepository.findAll(pagination);
}
// Accepts: ?page=1&size=10&sort=email,asc

Argument Resolvers

Two resolvers are auto-registered via XWebMvcConfiguration:

XPaginationResolver — @XPaginationDefault

Resolves XPagination from query parameters. Annotation defaults:

AttributeDefaultDescription
page1Page number (1-based)
size10Rows per page
offset0Row offset (alternative to page)
column"" (none)Default sort column (camelCase)
directionDESCDefault sort direction

Sort parsing:

?sort=createdAt,DESC          → XOrder("createdAt", DESC)
?sort=name                    → XOrder("name", ASC)   ← omitted direction defaults to ASC
?sort=createdAt,DESC&sort=name,ASC  → multi-sort

Priority: ?page= present → page-based; ?offset= only → offset-based. Query params override annotation defaults. "undefined" and "null" strings are treated as absent.

@GetMapping
public XPage<User> listUsers(
        @XPaginationDefault(size = 20, column = "createdAt", direction = XDirection.DESC)
        XPagination pagination) {
    return userRepository.findAll(pagination);
}

XSessionResolver — SessionData Subclass

Resolves any SessionData subclass from Access-Token HTTP header. No annotation required — auto-detected by parameter type.

// UserSession extends SessionData → auto-resolved from Access-Token header
@GetMapping("/me")
public UserProfile getMyProfile(UserSession session) {
    return userService.getProfile(session.getUserId());
}
  • Requires XAccessTokenParseHandler bean (auto-configured or custom @Component)
  • If XAccessTokenParseHandler not registered → returns null (no error)
  • If token missing → 401 (NOT_FOUND_ACCESS_TOKEN)
  • If token invalid → 401 (INVALID_ACCESS_TOKEN)
  • If token expired → 401 (EXPIRE_ACCESS_TOKEN)

Query Strategy: Repository vs Custom Mapper

The framework provides two query approaches. Choosing the right one is critical:

Use @XRepository (auto-generated SQL) when:

  • Exact-match WHERE conditions: findByStatus("ACTIVE")
  • Single-table CRUD operations
  • Simple AND conditions: findByUserIdAndStatus(id, status)

Use @Mapper (custom SQL) when:

  • LIKE / partial match: WHERE name LIKE '%keyword%'
  • BETWEEN / range: WHERE created_at BETWEEN ? AND ?
  • JOIN: Any query involving multiple tables
  • Subqueries: WHERE id IN (SELECT ...)
  • Aggregation: GROUP BY, HAVING, SUM(), COUNT() per group
  • OR conditions: WHERE status = ? OR role = ?
  • Complex sorting: Sorting by computed/joined columns
  • UNION: Combining result sets

CRITICAL: Query derivation only supports exact-match = with And combinator. It does NOT support LIKE, BETWEEN, OR, IN, >, <, JOIN, or any other SQL operator. When these are needed, immediately create a @Mapper interface — do not attempt to work around Repository limitations.

Custom Mapper with Pagination (XPagination)

Custom @Mapper methods integrate with XPagination seamlessly. The framework's XResultInterceptor automatically intercepts the query to handle COUNT, ORDER BY, and LIMIT — you only write the base SELECT.

Rules for custom mapper pagination:

  1. Include XPagination as a parameter
  2. Return XPage<T> as the return type (entity type is inferred from the generic parameter)
  3. Write only the base SELECT — do NOT add ORDER BY or LIMIT in your SQL
@Mapper
public interface UserMapper {

    // LIKE search with pagination
    @Select("SELECT * FROM users WHERE name LIKE CONCAT('%', #{keyword}, '%')")
    XPage<User> searchByName(XPagination pagination, @Param("keyword") String keyword);

    // BETWEEN with pagination
    @Select("SELECT * FROM users WHERE created_at BETWEEN #{from} AND #{to}")
    XPage<User> findByDateRange(XPagination pagination,
                                @Param("from") LocalDateTime from,
                                @Param("to") LocalDateTime to);

    // JOIN with pagination
    @Select("SELECT u.*, d.name AS department_name FROM users u " +
            "INNER JOIN departments d ON u.department_id = d.id " +
            "WHERE d.status = #{status}")
    XPage<UserWithDepartment> findUsersWithDepartment(XPagination pagination,
                                                      @Param("status") String status);

    // Multiple conditions (OR, IN)
    @Select("<script>" +
            "SELECT * FROM users WHERE status IN " +
            "<foreach item='s' collection='statuses' open='(' separator=',' close=')'>" +
            "#{s}" +
            "</foreach>" +
            "</script>")
    XPage<User> findByStatuses(XPagination pagination,
                               @Param("statuses") List<String> statuses);

    // Without pagination — just return List<T> (no XPagination needed)
    @Select("SELECT * FROM users WHERE email LIKE CONCAT('%', #{keyword}, '%')")
    List<User> searchByEmail(@Param("keyword") String keyword);

    // Aggregation (non-paginated, returns custom projection)
    @Select("SELECT department_id, COUNT(*) as user_count FROM users GROUP BY department_id")
    List<Map<String, Object>> countByDepartment();
}

Controller Pattern: Repository + Mapper Together

A typical controller uses Repository for simple operations and Mapper for complex queries:

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserController {

    private final UserRepository userRepository;  // simple CRUD
    private final UserMapper userMapper;           // complex queries

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userRepository.findOne(id);
    }

    @GetMapping
    public XPage<User> searchUsers(@XPaginationDefault XPagination pagination,
                                   @RequestParam(required = false) String keyword) {
        if (keyword != null) {
            return userMapper.searchByName(pagination, keyword);
        }
        return userRepository.findAll(pagination);
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        userRepository.save(user);
        return user;
    }
}

Error Code System

ErrorCode Record

public record ErrorCode(String code, String messageKey) {}

Built-in Exceptions

CodeExceptionHTTPDescription
1UnAuthorizedException401Authentication required
2UnAuthorizedException401Invalid credentials
3UnAuthorizedException401Token expired
11InvalidRequestParameterException400Invalid parameter
12InvalidRequestParameterException400Request body not found
13InvalidRequestParameterException400Method not supported
100NotFoundException404Not found
900UnavailableServerException504Server unavailable
999UnknownServerException500Unknown error

Custom Exception

public class UserException extends XRestException {
    public static final ErrorCode DUPLICATE_EMAIL = new ErrorCode("2001", "user.error.duplicate-email");
    public static final ErrorCode INACTIVE_ACCOUNT = new ErrorCode("2002", "user.error.inactive-account");

    public UserException(ErrorCode error) {
        super(HttpStatus.BAD_REQUEST, error);
    }

    public UserException(ErrorCode error, String description) {
        super(HttpStatus.BAD_REQUEST, error, description);
    }
}

// Usage
throw new UserException(UserException.DUPLICATE_EMAIL, "alice@example.com already exists");

i18n Messages

# messages.properties
user.error.duplicate-email=Email already exists.

# messages_ko.properties
user.error.duplicate-email=이미 존재하는 이메일입니다.

Error Response Format

{
    "code": "2001",
    "message": "Email already exists.",
    "description": "alice@example.com already exists",
    "data": null
}

Declarative REST Client

Requires @XRestServiceScan on application class.

Direct Mode vs Gateway Mode

// Direct Mode — host specified → URL: {host}{path}
@XRestService(value = "user-service", host = "${USER_SERVICE_HOST:http://localhost:8081}")
public interface UserServiceClient { ... }

// Gateway Mode — host omitted → URL: {gatewayHost}/{serviceName}/{version}{path}
@XRestService(value = "user-service", version = "v1")
public interface UserServiceClient { ... }

Parameter Annotations

@XRestService(value = "order-service", host = "${ORDER_SERVICE_HOST}")
public interface OrderServiceClient {

    @XRestAPI(value = "/orders/{id}", method = XHttpMethod.GET)
    Order getOrder(@PathVariable("id") Long id);

    @XRestAPI(value = "/orders", method = XHttpMethod.POST)
    Order createOrder(@RequestBody OrderCreateRequest request);

    @XRestAPI(value = "/orders", method = XHttpMethod.GET)
    List<Order> search(@RequestParam("status") String status,
                       @RequestParam("keyword") String keyword);

    @XRestAPI(value = "/orders", method = XHttpMethod.GET)
    List<Order> getOrders(@RequestHeader("X-Tenant-Id") String tenantId);

    // XPagination auto-converted → ?page=1&size=20&sort=createdAt,DESC
    @XRestAPI(value = "/orders", method = XHttpMethod.GET)
    XPage<Order> listOrders(XPagination pagination);

    @XRestAPI(value = "/orders/{id}", method = XHttpMethod.PUT)
    Order updateOrder(@PathVariable("id") Long id,
                      @RequestBody OrderUpdateRequest request,
                      @RequestHeader("Access-Token") String token);
}

Error Handling

try {
    Order order = orderClient.getOrder(id);
} catch (XRestException e) {
    e.getStatus();       // Original HTTP status (400, 404, 500, etc.)
    e.getCode();         // Error code from ApiError
    e.getMessage();      // Error message
    e.getDescription();  // Additional description
}

JSON Date Format

The framework ObjectMapper uses yyyy-MM-dd HH:mm:ss (NOT ISO 8601):

// ✓ "2024-01-15 14:30:00"
// ✗ "2024-01-15T14:30:00Z"

XWebClient (RestClient-based Alternative)

For programmatic HTTP calls (not declarative proxy). Two registration options:

Option 1: Declarative Bean Registration via Properties

# application.properties — each entry creates a named XWebClient bean
axim.web-client.services.userClient=http://user-service:8080
axim.web-client.services.orderClient=http://order-service:8080
@Service
@RequiredArgsConstructor
public class ExternalApiService {

    @Qualifier("userClient")
    private final XWebClient userClient;

    public User getUser(Long id) {
        return userClient.get("/users/{id}", User.class, id);
    }
}

Option 2: Programmatic via XWebClientFactory

@Service
@RequiredArgsConstructor
public class ExternalApiService {

    private final XWebClientFactory webClientFactory;

    public User getUser(Long id) {
        XWebClient client = webClientFactory.create("http://external-api.com");
        return client.get("/users/{id}", User.class, id);
    }
}

API Reference

// Simple API
client.get("/users/{id}", User.class, id);
client.post("/users", body, User.class);
client.put("/users/{id}", body, User.class, id);
client.delete("/users/{id}", Void.class, id);

// Generic types
client.get("/users", new ParameterizedTypeReference<List<User>>() {});

// Builder API
client.spec()
        .get("/users?keyword=" + keyword)
        .header("X-API-Key", "my-key")
        .body(requestBody)
        .retrieve(new ParameterizedTypeReference<List<User>>() {});
Feature@XRestServiceXWebClient
StyleInterface + annotationsDirect method calls
Bean creation@XRestServiceScanProperties or XWebClientFactory
Best forInternal microservice callsExternal API, dynamic URLs
PaginationAuto XPagination → query paramsManual query string

## Session / Token Authentication

The framework provides a built-in token system. **This is NOT JWT** — uses custom `Base64(payload).HmacSHA256(signature)` format.

### Custom Session Data

```java
@Data
public class UserSession extends SessionData {
    /** 사용자 고유 ID */
    private Long userId;
    /** 사용자 이름 */
    private String userName;
    /** 사용자 권한 목록 (예시: ["ADMIN", "USER"]) */
    private List<String> roles;
}

SessionData base fields (auto-managed): sessionId, createDate (format: yyyyMMddHHmmss)

Generating Tokens (Login)

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class AuthController {

    private final XAccessTokenParseHandler tokenHandler;

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest request) {
        // Authenticate user...
        UserSession session = new UserSession();
        session.setSessionId(UUID.randomUUID().toString());
        session.setUserId(authenticatedUser.getId());
        session.setUserName(authenticatedUser.getName());
        session.setRoles(authenticatedUser.getRoles());

        String token = tokenHandler.generateAccessToken(session);
        return Map.of("accessToken", token);
    }
}

Using Session in Controllers

Session auto-resolved from Access-Token HTTP header:

@GetMapping("/me")
public UserProfile getMyProfile(UserSession session) {
    // If token missing → 401 (NOT_FOUND_ACCESS_TOKEN)
    // If token invalid → 401 (INVALID_ACCESS_TOKEN)
    // If token expired → 401 (EXPIRE_ACCESS_TOKEN)
    return userService.getProfile(session.getUserId());
}

Session Configuration

# MUST be set in production — without it, tokens can be forged
axim.rest.session.secret-key=your-secret-key
axim.rest.session.token-expire-days=90         # Expiration in days (default: 90)

SECURITY WARNING: If secret-key is omitted, tokens have NO signature verification — anyone can forge a valid session token by crafting Base64-encoded JSON. ALWAYS set a strong secret key in production.

i18n Message Source

Hierarchical message resolution: Application messages override framework defaults.

messages.properties (your app)  →  overrides  →  framework-messages.properties (built-in)

Built-in framework messages:

server.http.error.invalid-parameter=Invalid request parameter.
server.http.error.required-auth=Authentication required.
server.http.error.invalid-auth=Invalid authentication credentials.
server.http.error.expire-auth=Authentication expired.
server.http.error.notfound-api=API not found.
server.http.error.server-error=Internal server error.

If key not found in any source, the key string itself is returned (no exception).

Security Warnings

1. Session Secret Key — Token Forgery Risk

Without axim.rest.session.secret-key, token payload is Base64-decoded without integrity check. Anyone can forge a valid session token.

# ✗ DANGEROUS — attacker creates Base64({"userId":1}) → valid token
# axim.rest.session.secret-key=

# ✓ REQUIRED for production
axim.rest.session.secret-key=a-strong-random-secret-key-at-least-32-chars

Rules: Always set in production. Minimum 32 chars. Never commit to source control — use environment variables.

2. Request Body Logging in Non-prod Profile

XRequestFilter logs full request bodies when profile is NOT prod. Passwords and sensitive fields are logged as-is (no field-level masking). HTTP headers like Authorization are masked, but request body fields are NOT.

# ✗ Non-prod → logs plaintext passwords from login endpoints
spring.profiles.active=dev

# ✓ Prod → disables body logging and stack traces in errors
spring.profiles.active=prod

3. Demo Credentials

Demo module contains hardcoded DB credentials for local development only. NEVER copy into production config.

Complete Service Layer Example

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    public User create(User user) {
        userRepository.save(user);
        return user;
    }

    public User partialUpdate(Long id, UserUpdateRequest req) {
        User user = new User();
        user.setId(id);
        user.setName(req.getName());
        userRepository.modify(user);    // selective UPDATE
        return userRepository.findOne(id);
    }

    public XPage<User> list(int page, int size) {
        XPagination pagination = new XPagination();
        pagination.setPage(page);
        pagination.setSize(size);
        pagination.addOrder(new XOrder("createdAt", XDirection.DESC));
        return userRepository.findAll(pagination);
    }
}

Coding Conventions

MANDATORY: Document All Member Variables and Enum Items

Every member variable in Entity, DTO, Request, Response, VO classes and every enum item MUST have a detailed Javadoc comment. Include: purpose, example values, format/pattern rules, constraints, and allowed values.

Entity Example

@Data
@XEntity("orders")
public class Order {

    /** 주문 고유 식별자 (Auto Increment) */
    @XColumn(isPrimaryKey = true, isAutoIncrement = true)
    private Long id;

    /**
     * 주문 번호
     * - 형식: "ORD-{yyyyMMdd}-{6자리 시퀀스}"
     * - 예시: "ORD-20240115-000001"
     * - UNIQUE 제약조건 적용
     */
    private String orderNo;

    /**
     * 주문 상태
     * - "PENDING": 결제 대기
     * - "PAID": 결제 완료
     * - "SHIPPED": 배송 중
     * - "DELIVERED": 배송 완료
     * - "CANCELLED": 주문 취소
     * @see OrderStatus
     */
    private String status;

    /**
     * 주문 총 금액 (단위: 원, KRW)
     * - 소수점 2자리까지 허용
     * - 음수 불가
     * - 예시: 15000.00
     */
    private BigDecimal totalAmount;

    /**
     * 주문자 ID (users 테이블 FK)
     * - NULL 불가
     */
    private Long userId;

    /** 주문 생성 일시 (INSERT 시 자동 설정) */
    @XDefaultValue(value = "NOW()", isDBValue = true)
    private LocalDateTime createdAt;

    /** 주문 수정 일시 (UPDATE 시 자동 갱신) */
    @XDefaultValue(updateValue = "NOW()", isDBValue = true)
    private LocalDateTime updatedAt;
}

DTO / Request Example

@Data
public class OrderCreateRequest {

    /**
     * 주문할 상품 ID 목록
     * - 최소 1개 이상 필수
     * - 예시: [1, 2, 3]
     */
    @NotEmpty
    private List<Long> productIds;

    /**
     * 배송지 주소
     * - 전체 도로명 주소 (우편번호 제외)
     * - 예시: "서울특별시 강남구 테헤란로 123 4층"
     * - 최대 200자
     */
    @NotBlank
    @Size(max = 200)
    private String shippingAddress;

    /**
     * 배송 메모 (선택사항)
     * - 예시: "부재 시 경비실에 맡겨주세요"
     * - 최대 500자, NULL 허용
     */
    @Size(max = 500)
    private String deliveryNote;

    /**
     * 결제 수단 코드
     * - "CARD": 신용/체크카드
     * - "BANK": 무통장입금
     * - "KAKAO": 카카오페이
     * - "NAVER": 네이버페이
     */
    @NotBlank
    private String paymentMethod;
}

Enum Example

public enum OrderStatus {

    /** 결제 대기 — 주문이 생성되었으나 결제가 완료되지 않은 상태 */
    PENDING,

    /** 결제 완료 — 결제가 확인되어 상품 준비 중인 상태 */
    PAID,

    /** 배송 중 — 택배사에 인계되어 배송이 진행 중인 상태 */
    SHIPPED,

    /** 배송 완료 — 수령인이 상품을 수령한 상태 */
    DELIVERED,

    /** 주문 취소 — 고객 요청 또는 시스템에 의해 취소된 상태. 환불 처리 필요 */
    CANCELLED
}

Comment Rules

RuleDescription
All member variablesMust have /** */ Javadoc comment describing purpose
Example valuesInclude concrete examples with 예시: or e.g. prefix
Format/PatternDocument format rules (e.g., "ORD-{yyyyMMdd}-{seq}")
Allowed valuesList all valid values for string-coded fields
ConstraintsNote NOT NULL, UNIQUE, max length, range limits
Enum itemsEach item must have a comment explaining the state/meaning
FK referencesNote the referenced table (e.g., "users 테이블 FK")
UnitsSpecify units for numeric fields (e.g., 원, KRW, %, 초)

@XColumn Usage Rules

@XColumn is NOT required on every field. The framework auto-maps fields using camelCase → snake_case. Only use @XColumn when you need to set specific options.

@Data
@XEntity("users")
public class User {

    @XColumn(isPrimaryKey = true, isAutoIncrement = true)
    private Long id;           // ✓ @XColumn needed — primary key

    private String email;      // ✓ auto-mapped to "email" — NO @XColumn needed
    private String userName;   // ✓ auto-mapped to "user_name" — NO @XColumn needed
    private Integer loginCount; // ✓ auto-mapped to "login_count" — NO @XColumn needed

    @XColumn("usr_email_addr")
    private String emailAddr;  // ✓ @XColumn needed — custom column name

    @XColumn(insert = false, update = false)
    private String readOnly;   // ✓ @XColumn needed — read-only field

    @XColumn(update = false)
    private String createdBy;  // ✓ @XColumn needed — immutable after creation
}
Situation@XColumnExample
Primary KeyRequired@XColumn(isPrimaryKey = true, isAutoIncrement = true)
Composite PK fieldRequired@XColumn(isPrimaryKey = true)
Regular field (camelCase→snake_case)Omitprivate String userName;user_name
Custom column nameRequired@XColumn("usr_nm")
Read-only / insert-only / update-onlyRequired@XColumn(insert = false, update = false)
Exclude from DB entirelyUse @XIgnoreColumn@XIgnoreColumn private String temp;

@XDefaultValue Pitfall: isDBDefaultUsed Defaults to true

CRITICAL: isDBDefaultUsed defaults to true. This means @XDefaultValue(value = "ACTIVE") will omit the column from INSERT and use DB DEFAULT — the value is silently ignored!

// ✗ WRONG — "ACTIVE" is IGNORED because isDBDefaultUsed defaults to true
@XDefaultValue(value = "ACTIVE")
private String status;

// ✓ CORRECT — must set isDBDefaultUsed = false for literal values
@XDefaultValue(value = "ACTIVE", isDBDefaultUsed = false)
private String status;

// ✓ CORRECT — DB expression
@XDefaultValue(value = "NOW()", isDBValue = true)
private LocalDateTime createdAt;

// ✓ CORRECT — intentionally use DB DEFAULT
@XDefaultValue(isDBDefaultUsed = true)
private String region;

// ✓ CORRECT — auto-set on UPDATE only
@XDefaultValue(updateValue = "NOW()", isDBValue = true)
private LocalDateTime updatedAt;

Common Pitfalls

1. Column Names vs Field Names

Query derivation and findWhere() use Java field names (camelCase), NOT column names (snake_case).

// ✗ WRONG
User findByUser_name(String name);
userRepository.findWhere(Map.of("user_name", "Alice"));

// ✓ CORRECT
User findByUserName(String name);
userRepository.findWhere(Map.of("userName", "Alice"));

2. findBy Return Type Determines Behavior

User findByEmail(String email);       // → LIMIT 1 (single result)
List<User> findByEmail(String email); // → no LIMIT (all matches)

3. update() Overwrites with NULL

User user = new User();
user.setId(1L);
user.setName("Alice");
// email and status are null

userRepository.update(user);  // ✗ Sets email=NULL, status=NULL in DB!
userRepository.modify(user);  // ✓ Only sets name='Alice', others preserved

4. ORDER BY / LIMIT in Custom Mapper SQL

XResultInterceptor handles pagination SQL automatically. Never add ORDER BY or LIMIT.

// ✗ WRONG
@Select("SELECT * FROM users WHERE status = #{status} ORDER BY created_at DESC LIMIT 20")
XPage<User> findByStatus(XPagination pagination, @Param("status") String status);

// ✓ CORRECT — only the base SELECT
@Select("SELECT * FROM users WHERE status = #{status}")
XPage<User> findByStatus(XPagination pagination, @Param("status") String status);

5. Never Create Custom Pagination Classes

// ✗ WRONG
public class PageRequest { int page; int size; }
public class PageResponse<T> { List<T> items; long total; }

// ✓ CORRECT — always use framework classes
XPagination pagination = new XPagination();
XPage<User> result = userRepository.findAll(pagination);

6. Empty Map in findWhere()

userRepository.findWhere(Map.of());                    // ✗ Throws exception
userRepository.findAll();                               // ✓ Use findAll() instead
userRepository.findWhere(Map.of("status", "ACTIVE"));  // ✓ Non-empty map

7. Query Derivation Method Name Parsing

And only splits when preceded by lowercase and followed by uppercase.

User findByBrandName(String brandName);                       // → single field "brandName"
List<User> findByStatusAndName(String status, String name);   // → two fields "status", "name"

// ✗ Parameter count must match parsed field count
List<User> findByStatusAndName(String status);  // WRONG — 2 fields but 1 param

Architecture

Application Code                    Framework Internals
────────────────                    ───────────────────
@XRepository                        XRepositoryBeanScanner
UserRepository                           ↓
  extends IXRepository<K, T>        XRepositoryProxyFactoryBean
       ↓                                 ↓
  (JDK Dynamic Proxy)              XRepositoryProxy (InvocationHandler)
       ↓                                 ↓
                                    CommonMapper (@Mapper)
                                         ↓
                                    CrudSqlProvider (SQL Generation + Cache)
                                         ↓
                                    XResultInterceptor (Pagination, Result Mapping)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

axim-restdoc-generator

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

openclaw-version-monitor

监控 OpenClaw GitHub 版本更新,获取最新版本发布说明,翻译成中文, 并推送到 Telegram 和 Feishu。用于:(1) 定时检查版本更新 (2) 推送版本更新通知 (3) 生成中文版发布说明

Archived SourceRecently Updated
Coding

ask-claude

Delegate a task to Claude Code CLI and immediately report the result back in chat. Supports persistent sessions with full context memory. Safe execution: no data exfiltration, no external calls, file operations confined to workspace. Use when the user asks to run Claude, delegate a coding task, continue a previous Claude session, or any task benefiting from Claude Code's tools (file editing, code analysis, bash, etc.).

Archived SourceRecently Updated
Coding

ai-dating

This skill enables dating and matchmaking workflows. Use it when a user asks to make friends, find a partner, run matchmaking, or provide dating preferences/profile updates. The skill should execute `dating-cli` commands to complete profile setup, task creation/update, match checking, contact reveal, and review.

Archived SourceRecently Updated