Skip to main content
Java remains one of the most widely deployed backend languages in the industry, backed by a mature ecosystem and a battle-tested JVM. This page distills the core knowledge you need for building production-grade Java services: Spring Boot’s auto-configuration model, JVM memory regions and class loading, multithreading primitives, Java 8+ language features, and data-access patterns with MyBatis-Plus.

Spring Boot Essentials

Spring Boot eliminates the boilerplate of traditional Spring applications. It follows a “convention over configuration” philosophy: a handful of starter dependencies gives you an embedded Tomcat server, JSON serialization, database connectivity, and more—all wired together automatically.

Auto-configuration and starters

Adding spring-boot-starter-web to your pom.xml pulls in Spring MVC, an embedded Tomcat, and Jackson without any XML configuration. Spring Boot scans your classpath and conditionally activates beans based on what it finds.
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Enable hot reload during development with the devtools starter:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>
Then configure it in application.properties:
spring.devtools.restart.enabled=true
spring.devtools.restart.additional-paths=src/main/java
spring.devtools.restart.exclude=static/**

Dependency injection

Spring Boot manages beans through its IoC container. Annotate classes with @Service, @Repository, or @Component to register them, and use @Autowired (or constructor injection) to receive dependencies:
@Service
public class OrderService {
    private final OrderRepository repo;

    public OrderService(OrderRepository repo) { // constructor injection
        this.repo = repo;
    }
}

REST controllers

Use @RestController for APIs that return data (JSON by default). @Controller is for server-rendered pages. In a front-end/back-end separated architecture you will almost always use @RestController.
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable int id) {
        return userService.findById(id);
    }

    @PostMapping("/users")
    public String createUser(@RequestBody User user) {
        userService.save(user);
        return "created";
    }
}
Spring Boot also supports request mapping shortcuts: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping. Route paths should contain only nouns, not verbs (/users, not /getUser).

Interceptors and filters

Interceptors handle Spring-managed resources; filters run before the Spring context and intercept every request including static files. Execution order: Filter → Interceptor → Controller.
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req,
                             HttpServletResponse res,
                             Object handler) {
        // return false to abort the request
        return true;
    }
}
Register the interceptor in a WebMvcConfigurer:
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**");
    }
}

JVM Internals

The JVM manages memory in several distinct regions. Understanding them helps you tune heap sizes, diagnose OutOfMemoryError, and reason about object lifecycles.

Memory regions

RegionThread scopePurpose
Program Counter RegisterPer-threadTracks the current bytecode instruction
JVM Stack (virtual machine stack)Per-threadStores stack frames with local variables and operand stacks
Native Method StackPer-threadServices native (C/C++) calls via JNI
HeapSharedStores almost all object instances; managed by the GC
Method AreaSharedStores class metadata, constants, static variables, JIT-compiled code
Runtime Constant PoolShared (part of Method Area)Literal values and symbolic references resolved at load time
The heap is the largest region and the primary GC target. You control its bounds with -Xms (initial size) and -Xmx (maximum size).

Object lifecycle

When the JVM executes new Foo():
  1. Class load check — verifies that Foo is already loaded, linked, and initialized; triggers class loading if not.
  2. Memory allocation — carves out a region of the heap. Uses CAS-based retry or per-thread TLAB (Thread-Local Allocation Buffer) to keep allocations thread-safe.
  3. Zero initialization — sets all instance fields to their zero values so code can read fields before explicitly assigning them.
  4. Object header setup — records the class pointer, identity hash code, GC generation age, and lock state in the header.
  5. Constructor — runs <init>() to set user-defined initial values.

Class loading

Class loading proceeds through five stages:
  1. Loading — reads the .class bytecode and creates a Class object in the method area.
  2. Verification — confirms the bytecode conforms to the JVM specification (no memory corruption, no type violations).
  3. Preparation — allocates memory for static variables and sets them to zero (not their declared values yet).
  4. Resolution — replaces symbolic references in the constant pool with direct memory pointers.
  5. Initialization — executes the <clinit>() method, running static initializer blocks and assigning declared static values.
A class is unloaded only when its Class object is GC’d, which requires all instances to be collected and the class loader itself to be collected. JVM built-in class loaders never unload their classes.

JIT compilation

The JVM interprets bytecode initially. The JIT compiler identifies hot methods and compiles them to native machine code at runtime. This means long-running Java processes typically outperform freshly started ones because the JIT has had time to optimize hot paths.

Multithreading

Thread and Runnable

There are two basic ways to define work for a thread. Extending Thread is simpler but wastes Java’s single-inheritance slot:
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("running in thread");
    }
}
new MyThread().start();
Implementing Runnable is preferred because it keeps your class free to extend another class and makes it easy to share one Runnable across multiple threads:
public class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("task running");
    }
}
new Thread(new MyTask()).start();

ExecutorService and thread pools

Creating and destroying threads on demand is expensive. Use ExecutorService to maintain a pool of worker threads:
ExecutorService pool = Executors.newFixedThreadPool(10);

// submit a Runnable
pool.execute(new MyTask());

// submit a Callable (returns a Future)
Future<Integer> future = pool.submit(() -> {
    return 42;
});
System.out.println(future.get()); // blocks until done

pool.shutdown();
Thread pool parameters to know:
  • corePoolSize — threads kept alive even when idle.
  • maximumPoolSize — upper bound on thread count.
  • keepAliveTime — how long idle threads above corePoolSize survive before being terminated.

synchronized vs ReentrantLock

synchronized is an implicit lock tied to an object monitor. It releases automatically when the block exits, even on exception:
public synchronized void increment() {
    count++;
}

// or a block lock on an explicit object
synchronized (sharedList) {
    sharedList.add(item);
}
ReentrantLock is explicit and offers more control: try-lock, timed lock, and fairness settings. Always release in a finally block:
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}
Priority ordering for choosing a lock: ReentrantLock > synchronized block > synchronized method.

CompletableFuture

CompletableFuture enables non-blocking async pipelines without explicit thread management:
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchData())        // runs on ForkJoinPool
    .thenApply(data -> transform(data))    // chained transformation
    .exceptionally(ex -> "fallback");      // error handling

String result = future.join(); // block until complete

Producer-consumer with wait / notifyAll

The classic bounded-buffer pattern uses wait and notifyAll on a shared monitor to coordinate producers and consumers:
class BoundedBuffer {
    private final Object[] items = new Object[20];
    private int count = 0;

    public synchronized void put(Object item) throws InterruptedException {
        while (count == items.length) wait(); // buffer full
        items[count++] = item;
        notifyAll();
    }

    public synchronized Object take() throws InterruptedException {
        while (count == 0) wait(); // buffer empty
        Object item = items[--count];
        notifyAll();
        return item;
    }
}

Java 8+ Features

Optional

Optional<T> makes the possibility of a missing value explicit in the type system, eliminating entire classes of NullPointerException:
Optional<User> user = userRepo.findById(id);
String name = user.map(User::getName)
                  .orElse("anonymous");

Stream API

Streams provide a declarative, functional-style pipeline for processing collections:
List<String> names = users.stream()
    .filter(u -> u.getAge() > 18)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .collect(Collectors.toList());

Functional interfaces and lambdas

Any interface with exactly one abstract method is a functional interface and can be expressed as a lambda:
// Built-in functional interfaces
Function<Integer, Integer> square = x -> x * x;
Predicate<String>          isEmpty = s -> s.isEmpty();
Consumer<String>           printer = System.out::println;
Supplier<List<String>>     factory = ArrayList::new;

System.out.println(square.apply(5)); // 25
Lambda expressions eliminate the need for anonymous inner classes and integrate naturally with the Stream API.

LocalDateTime

java.time.LocalDateTime replaces the problematic Date and Calendar APIs. It is immutable and thread-safe:
LocalDateTime now   = LocalDateTime.now();
LocalDateTime later = now.plusHours(2).plusDays(1);
String formatted    = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime parsed = LocalDateTime.parse("2026-01-15T10:30:00");

MyBatis-Plus

MyBatis-Plus enhances MyBatis with generic CRUD operations, a fluent query wrapper, and pagination—without requiring you to write SQL for common cases.

Setup

Add the dependencies in pom.xml:
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>
Configure your datasource in application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false
spring.datasource.username=root
spring.datasource.password=secret
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
Add @MapperScan to your main class:
@SpringBootApplication
@MapperScan("com.example.mapper")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

Basic CRUD

Extend BaseMapper<T> to inherit full CRUD without writing SQL:
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // selectById, insert, updateById, deleteById, selectList, etc.
    // are all inherited from BaseMapper
}
For custom queries, use annotations directly on the interface:
@Mapper
public interface UserMapper extends BaseMapper<User> {
    @Select("SELECT * FROM user WHERE name = #{name}")
    User findByName(String name);

    @Insert("INSERT INTO user VALUES (#{id}, #{name}, #{age})")
    int insert(User user);

    @Update("UPDATE user SET name=#{name}, age=#{age} WHERE id=#{id}")
    int update(User user);

    @Delete("DELETE FROM user WHERE id=#{id}")
    int delete(int id);
}

Conditional queries

The QueryWrapper lets you build type-safe WHERE clauses without string concatenation:
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "alice")
       .gt("age", 18)
       .orderByAsc("age");
List<User> users = userMapper.selectList(wrapper);

Pagination

Register the pagination interceptor once:
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor paginationInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(
            new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
Then call selectPage:
@GetMapping("/users/page")
public IPage<User> page(int pageNum, int pageSize) {
    Page<User> config = new Page<>(pageNum, pageSize);
    return userMapper.selectPage(config, null);
}

Dynamic SQL with XML mappers

For complex queries, pair an XML mapper with the Java interface. Place the XML file in the same package as the mapper with the same name:
<!-- com/example/mapper/UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="searchList" resultType="com.example.entity.User">
        SELECT * FROM user
        <where>
            <if test="name != null">
                name LIKE CONCAT('%', #{name}, '%')
            </if>
            <if test="minAge != 0 and maxAge != 0">
                AND age BETWEEN #{minAge} AND #{maxAge}
            </if>
        </where>
        ORDER BY age DESC
    </select>
</mapper>
public List<User> searchList(@Param("name") String name,
                              @Param("minAge") int minAge,
                              @Param("maxAge") int maxAge);