Building Spring MVC with Maven

This article assumes that your workspace is C:\www.

Generating archetype

C:\ Command Prompt
C:\www>mvn archetype:generate -Dfilter=maven-archetype-webapp

Choose archetype:
1: remote -> com.haoxuer.maven.archetype:maven-archetype-webapp
2: remote -> com.lodsve:lodsve-maven-archetype-webapp
3: remote -> org.apache.maven.archetypes:maven-archetype-webapp
4: remote -> org.bytesizebook.com.guide.boot:maven-archetype-webapp
Choose a number or apply filter: : 3
Choose org.apache.maven.archetypes:maven-archetype-webapp version:
1: 1.0-alpha-1
2: 1.0-alpha-2
3: 1.0-alpha-3
4: 1.0-alpha-4
5: 1.0
6: 1.3
7: 1.4
Choose a number: 7: ↵
Define value for property 'groupId': net.java_school
Define value for property 'artifactId': spring-bbs
Define value for property 'version' 1.0-SNAPSHOT: : ↵
Define value for property 'package' net.java_school: : ↵
Confirm properties configuration:
groupId: net.java_school
artifactId: spring-bbs
version: 1.0-SNAPSHOT
package: net.java_school
 Y: : ↵

Maven creates the spring-bbs folder in C:\www through the build process. C:\www\spring-bbs is the root directory of your project. The src/main/webapp folder, just above WEB-INF, is the document base.

Create a Tomcat context file as follows.

ROOT.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context
  docBase="C:/www/spring-bbs/src/main/webapp"
  reloadable="true">
</Context>

Copy the ROOT.xml file to the CATALINA_HOME/conf/Catalina/localhost folder. After restart Tomcat, visit http://localhost:8080 to see the ROOT application is working.

Spring MVC Test

Modify pom.xml like below.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>net.java_school</groupId>
  <artifactId>spring-bbs</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>spring-bbs Maven Webapp</name>
  <url>http://localhhost:8080</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <spring.version>5.3.33</spring.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Servlet JSP -->
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>javax.servlet.jsp-api</artifactId>
      <version>2.3.3</version>
      <scope>provided</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>spring-bbs</finalName>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
          <configuration>
            <filesets>
               <fileset>
                  <directory>src/main/webapp/WEB-INF/classes</directory>
               </fileset>
               <fileset>
                  <directory>src/main/webapp/WEB-INF/lib</directory>
               </fileset>
            </filesets>
          </configuration>
        </plugin>
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

Modify web.xml like below.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0">
  
  <display-name>Spring BBS</display-name>

  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>spring-bbs</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring-bbs</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <session-config>
    <session-timeout>-1</session-timeout>
  </session-config>

  <error-page>
    <error-code>404</error-code>
    <location>/WEB-INF/views/404.jsp</location>
  </error-page>

  <error-page>
    <error-code>500</error-code>
    <location>/WEB-INF/views/500.jsp</location>
  </error-page>

</web-app>

Of the filters above, the one that executes the setCharacterEncoding("UTF-8") code on every request is essential for non-English websites. You should declare it before any other filters.

Since we set Spring MVC DispatcherServlet configuration file as /WEB-INF/spring/mvc.xml in web.xml, we need to create a spring configuration file named mvc.xml in /WEB-INF/spring/ folder.

mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<mvc:annotation-driven />

<bean id="internalResourceViewResolver"
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/views/" />
  <property name="suffix" value=".jsp" />
</bean>

</beans>

Design the homepage

src/main/webapp/WEB-INF/views/index.jsp

index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Spring MVC Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="Keywords" content="Spring MVC Test" />
<meta name="Description" content="This is web app for Spring MVC Test" />
<style>
html, body {
  margin: 0;
  padding: 0;
  background-color: #FFF;
  font-family: "Liberation Sans", Helvetica, sans-serif;
}
#wordcard {
  margin: 7px auto;
  padding: 1em;
  border: 3px solid grey;
  width: 600px;
  text-align: center;
}
</style>
</head>
<body>

<div id="wordcard">
<h1>Vocabulary</h1>
<p>
total number of words which (with rules for combining them) make up a language
</p>
</div>

</body>
</html>

The screen shows words and their meanings.

When the design is complete, it is common to start with database design, then implement JavaBeans, DAO, service, and controller in that order.
To experience Spring MVC more quickly, let's omit most of these and create an example that works only with controllers and views.

Controller
src/main/java/net/java_school/english/HomeController.java

The src/main/java folder is the default maven folder.

package net.java_school.english;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

  @GetMapping("/")
  public String index() {
    return "index";
  }
}

@Controller and @GetMapping are Spring annotations used to pass information to Spring. @Controller placed above the class declaration tells Spring that the Java bean is a controller. @GetMapping("/") located above the controller method indicates that this method handles the GET "/" request. If it is the root application, that "/" will be http://localhost:8080/.

For the homepage ("/") to be served through the controller, you should not declare any other servlets mapped to the homepage. However, the archetype has the index.jsp in the src/main/webapp folder. This file will respond to homepage requests. Delete the JSP file created by Maven when creating the archetype.

Register the controller

<bean id="homeController" class="net.java_school.english.HomeController" />

Build and deploy
Execute the following in the maven root directory:
mvn compile war:inplace
The war:inplace option creates bytecode in src/main/WEB-INF/classes and dependent libraries in src/main/WEB-INF/lib.

Test
Visit http://localhost:8080
Spring MVC

We figured out how Spring MVC works.
Next, practice how the controller delivers data to a view.
We won't use a database to keep the example simple.

Java Beans
src/main/java/net/java_school/english/WordCard.java

WordCard.java
package net.java_school.english;

public class WordCard {
  private String word;
  private String definitions;

  public WordCard() {}

  public WordCard(String word, String definitions) {
    this.word = word;
    this.definitions = definitions;
  }

  public String getWord() {
    return word;
  }

  public void setWord(String word) {
    this.word = word;
  }

  public String getDefinitions() {
    return definitions;
  }

  public void setDefinitions(String definitions) {
    this.definitions = definitions;
  }
}

DAO
src/main/java/net/java_school/english/WordCardDao.java

WordCardDao.java
package net.java_school.english;

public class WordCardDao {

  private final String[][] words = {
    {"illegible",
    	"difficult or impossible to read"}, 
    {"vulnerable",
    	"that is liable to be damaged; not protected against attack"}, 
    {"abate",
    	"(of winds, storms, floods, pain, etc) make or become less"}, 
    {"clandestine",
    	"secret; done secretly; kept secret"}, 
    {"sojourn",
    	"(make a) stay (with sb, at or in) for a time"}, 
    {"defiance",
    	"open disobedience or resistance; refusal to recognize authority; defyling"}, 
    {"infraction",
    	"breaking of a rule, law, etc; instance of this"}
  }; 
	
  public WordCard selectOne() {
    int no = (int)(Math.random() * 7) + 1;
    WordCard wordCard = new WordCard();
    wordCard.setWord(words[no - 1][0]);
    wordCard.setDefinitions(words[no - 1][1]);
    return wordCard;
  }
}
<bean id="wordCardDao" class="net.java_school.english.WordCardDao" />

Service
src/main/java/net/java_school/english/WordCardService.java

WordCardService.java
package net.java_school.english;

public class WordCardService {
  private WordCardDao wordCardDao;

  public WordCardService(WordCardDao wordCardDao) {
    this.wordCardDao = wordCardDao;
  }

  public WordCard getWordCard() {
    return wordCardDao.selectOne();
  }
}
<bean id="wordCardService" class="net.java_school.english.WordCardService">
  <constructor-arg ref="wordCardDao" />
</bean>

Controller

package net.java_school.english;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.GetMapping;

public class HomeController {

  private WordCardService wordCardService;

  public HomeController(WordCardService wordCardService) {
    this.wordCardService = wordCardService;
  }

  @GetMapping("/")
  public String index(Model model) {
    WordCard wordCard = wordCardService.getWordCard();
    model.addAttribute("wordCard", wordCard);
    return "index";
  }
}
<bean id="homeController" class="net.java_school.english.HomeController">
  <constructor-arg ref="wordCardService" />
</bean>

View
src/main/webapp/WEB-INF/views/index.jsp

Modify the content of the body element as below.

<div id="wordcard">
<h1>${wordCard.word }</h1>
<p>
${wordCard.definitions }
</p>
</div>

Test
mvn compile war:inplace
http://localhost:8080
Test 2

MyBatis-Spring Example

We haven't covered the database yet.
Model 2 bulletin board uses JDBC as it is. But code that uses JDBC as-is is unproductive due to repetitive code.

Using the persistence framework--it uses JDBC as an internal source--reduces the amount of code you have to write.

MyBatis is a SQL mapping persistence framework.
MyBatis-Spring is an interlocking module for conveniently using MyBatis in Spring.
See https://mybatis.org/spring/ko/getting-started.html

Connect to your JAVA account and create the following table and sequence.

create table photo (
no number,
content varchar2(4000) not null,
constraint pk_photo primary key(no),
constraint uq_photo unique(content)
);

create sequence seq_photo
increment by 1
start with 1;

Add Spring JDBC, Oracle JDBC driver, Apache DBCP, and MyBatis with mybatis-spring.

pom.xml
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc6 -->
<dependency>
  <groupId>com.oracle.database.jdbc</groupId>
  <artifactId>ojdbc6</artifactId>
  <version>11.2.0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-dbcp/commons-dbcp -->
<dependency>
  <groupId>commons-dbcp</groupId>
  <artifactId>commons-dbcp</artifactId>
  <version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.1.0</version>
</dependency>

commons-dbcp is a database connection pool provided by Apache.
Since ojdbc6.jar is a JDBC 4 driver, we need the DBCP 1.4 version.
See https://dlcdn.apache.org//commons/dbcp/

Add datasource and SqlSessionFactoryBean to the spring configuration.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">

  <mvc:annotation-driven />

  <bean id="internalResourceViewResolver" 
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    <property name="prefix" value="/WEB-INF/views/" />
    <property name="suffix" value=".jsp" />
  </bean>

  <bean id="wordCardDao" class="net.java_school.english.WordCardDao" />

  <bean id="wordCardService" class="net.java_school.english.WordCardService">
    <constructor-arg ref="wordCardDao" />
  </bean>

  <bean id="homeController" class="net.java_school.english.HomeController">
    <constructor-arg ref="wordCardService" />
  </bean>

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
      destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
    <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" />
    <property name="username" value="java" />
    <property name="password" value="school" />
    <property name="maxActive" value="100" />
    <property name="maxWait" value="1000" />
    <property name="poolPreparedStatements" value="true" />
    <property name="defaultAutoCommit" value="true" />
    <property name="validationQuery" value=" SELECT 1 FROM DUAL" />
  </bean>

  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
  </bean>
</beans>

If you are using commons-dbcp2, you need to change the maxActive and maxWait parameters to maxTotal and maxWaitMillis.

Java Beans
src/main/java/net/java_school/photostudio/Photo.java

Photo.java
package net.java_school.photostudio;

public class Photo {
  private int no;
  private String content;

  public Photo() {}

  public Photo(int no, String content) {
    this.no = no;
    this.content = content;
  }

  public int getNo() {
    return no;
  }

  public void setNo(int no) {
    this.no = no;
  }

  public String getContent() {
    return content;
  }

  public void setContent(String content) {
    this.content = content;
  }
}

Mapper Interface
It is better to put the mapper interface in a separate package from other Spring components.

src/main/java/net/java_school/mybatis/PhotoMapper.java

PhotoMapper.java
package net.java_school.mybatis;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Insert;

public interface PhotoMapper {
  @Insert("INSERT INTO photo VALUES (seq_photo.nextval, #{content})")
  public void insert(@Param("content") String content);
}

Add the mapper interface to the spring configuration using MapperFactoryBean.

<bean id="photoMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="net.java_school.mybatis.PhotoMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

Service

src/main/java/net/java_school/photostudio/PhotoService.java

PhotoService.java
package net.java_school.photostudio;

public interface PhotoService {
  public void add(String content);
}

src/main/java/net/java_school/photostudio/PhotoServiceImpl.java

PhotoServiceImpl.java
package net.java_school.photostudio;

import net.java_school.mybatis.PhotoMapper;

import org.springframework.stereotype.Service;

@Service
public class PhotoServiceImpl implements PhotoService {

  private PhotoMapper photoMapper;

  public PhotoServiceImpl(PhotoMapper photoMapper) {
    this.photoMapper = photoMapper;
  }

  @Override
  public void add(String content) {
    photoMapper.insert(content);
  }
}
<bean id="photoService" class="net.java_school.photostudio.PhotoServiceImpl">
  <constructor-arg ref="photoMapper" />
</bean>

Controller
Add @RequestMapping("photo") annotation on the class declaration to create a controller that handles requests with "photo" appended to the ContextPath.

src/main/java/net/java_school/photostudio/PhotoController.java

PhotoController.java
package net.java_school.photostudio;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
@RequestMapping("photo")
public class PhotoController {

  private PhotoService photoService;

  public PhotoController(PhotoService photoService) {
    this.photoService = photoService;
  }
  
  @GetMapping
  public String index() {
    return "photo/index";
  }

  @PostMapping
  public String add(String content) {
    photoService.add(content);	
    return "redirect:/photo/?page=1";
  }
}
<bean id="photoController" class="net.java_school.photostudio.PhotoController">
  <constructor-arg ref="photoService" />
</bean>

View
src/main/webapp/WEB-INF/views/photo/index.jsp

index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>mybatis-spring Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="Keywords" content="MyBatis Spring Test" />
<meta name="Description" content="This is web app for mybatis-spring Test" />
<style>
html, body {
  margin: 0;
  padding: 0;
  background-color: #FFF;
  font-family: "Liberation Sans", Helvetica, sans-serif;
}
</style>
</head>
<body>
  <form id="addForm" method="post">
    <input type="text" name="content" />
    <input id="submit" type="submit" value="Send" /> 
  </form>
</body>
</html>

Test

mvn compile war:inplace
http://localhost:8080/photo
Paste the web image link into the text field and click the send button. Connect to SQL*PLUS and check if there are any records in the table.

Logging
See https://mybatis.org/mybatis-3/ko/logging.html

Add the apache commons-logging and log4j2 libraries to dependencies in pom.xml.

<!-- Logging -->                
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-api</artifactId>
  <version>2.19.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.19.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-jcl -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-jcl</artifactId>
  <version>2.19.0</version>
</dependency>

Create a log4j2 configuration file with the name log4j2.xml in the src/main/resources/ folder. That is the default maven folder.

log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<Configuration>
  <Appenders>
    <File name="A1" fileName="A1.log" append="false">
      <PatternLayout pattern="%t %-5p %c{2} - %m%n" />
    </File>
    <Console name="STDOUT" target="SYSTEM_OUT">
      <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
    </Console>
  </Appenders>
  <Loggers>
    <Logger name="net.java_school" level="DEBUG">
      <AppenderRef ref="A1" />
    </Logger>
    <Root level="INFO">
      <AppenderRef ref="STDOUT" />
    </Root>
  </Loggers>
</Configuration>

Run mvn compile war:inplace to compile and check the log message.

http-nio-8080-exec-14 DEBUG PhotoMapper.insert - ==>  Preparing: INSERT INTO photo VALUES (seq_photo.nextval, ?
http-nio-8080-exec-14 DEBUG PhotoMapper.insert - ==> Parameters: https://cdn.pixabay.com/house_720.jpg(String)

The right parenthesis is missing in the insert statement.
Add parentheses and compile again.
Check the log message.

http-nio-8080-exec-26 DEBUG PhotoMapper.insert - ==>  Preparing: INSERT INTO photo VALUES (seq_photo.nextval, ?)
http-nio-8080-exec-26 DEBUG PhotoMapper.insert - ==> Parameters: https://cdn.pixabay.com/house_720.jpg(String)
http-nio-8080-exec-26 DEBUG PhotoMapper.insert - <==    Updates: 1

XML Mapper
MyBatis provides a way to separate queries into XML files. This file is the XML mapper file. 이 파일이 XML 매퍼 파일이다.

So far, we have not created a MyBatis configuration file.
If you put the XML mapper file and the mapper interface on the same classpath, you don't need a MyBatis configuration file.
See https://mybatis.org/spring/ko/mappers.html#register

Create a PhotoMapper.xml mapper file in the src/main/resources/net/java_school/mybatis folder.

src/main/resources/net/java_school/mybatis/PhotoMapper.xml

PhotoMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="net.java_school.mybatis.PhotoMapper">

  <insert id="insert">
    INSERT INTO photo VALUES (seq_photo.nextval, #{content})
  </insert>
	
</mapper>

Remove the query from the PhotoMapper interface.

PhotoMapper.java
package net.java_school.mybatis;

import org.apache.ibatis.annotations.Param;

public interface PhotoMapper {
  public void insert(@Param("content") String content);
}

Show saved images
Let's try to show four images per page.

Modify the contents of the body element of the index.jsp file as follows.

index.jsp
<div id="photos">
  <img width="640" alt="p_1" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
  <img width="640" alt="p_2" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
  <img width="640" alt="p_3" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
  <img width="640" alt="p_4" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
</div>

<div id="paging">
  <a href="?page=10" title="10">◁ back</a>
  <a href="?page=1" title="1">1</a>
  <a href="?page=10" title="10">...</a>
  <strong>11</strong>
  <a href="?page=12" title="12">12</a>
  <a href="?page=13" title="3">3</a>
  <a href="?page=14" title="4">4</a>
  <a href="?page=15" title="5">5</a>
  <a href="?page=16" title="6">6</a>
  <a href="?page=17" title="7">7</a>
  <a href="?page=18" title="8">8</a>
  <a href="?page=19" title="9">9</a>
  <a href="?page=20" title="10">10</a>
  <a href="?page=21" title="11">...</a>
  <a href="?page=407" title="407">407</a>
  <a href="?page=12" title="12">next▷ </a>

  <form id="addForm" method="post">
    <input type="text" name="content" width="500" />
    <input id="submit" type="submit" value="Send" /> 
  </form>

</div>

Add the following to the style element of the file.

#paging {
  width: 640px;
  float: left;
  font-size: 1em;
}
a:link {
  color: #2C80D0;
  text-decoration: none;
}
a:visited {
  color: #2C80D0;
  text-decoration: none;
}
a:active {
  color: #2C80D0;
  text-decoration: none;
}
a:hover {
  color: #2C80D0;
  text-decoration: underline;
}

Add methods to the PhotoMapper interface to get the total number of records and items to display on the page.

PhotoMapper.java
package net.java_school.mybatis;

import net.java_school.photostudio.Photo;

import java.util.HashMap;
import java.util.List;

import org.apache.ibatis.annotations.Param;

public interface PhotoMapper {
  
  public void insert(@Param("content") String content);

  public int selectCountOfPhotos();//Total Record Count

  public List<Photo> selectPhotos(HashMap<String, String> hashmap);//Itemsfor view
}
PhotoMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="net.java_school.mybatis.PhotoMapper">

  <insert id="insert">
    NSERT INTO photo VALUES (seq_photo.nextval, #{content})
  </insert>

  <select id="selectCountOfPhotos" resultType="int">
    SELECT count(*) FROM photo
  </select>

  <select id="selectPhotos" parameterType="hashmap" resultType="net.java_school.photostudio.Photo">
    SELECT no,content
    FROM (
      SELECT rownum R,A.*
      FROM (
        SELECT 
          no,content
        FROM 
          photo
        ORDER BY no DESC
      ) A
    )
    WHERE R BETWEEN #{start} AND #{end}
  </select>
	
</mapper>

Using the typeAlias element of the MyBatis configuration file can shorten resultType="net.java_school.photostudio.Photo" to resultType="Photo" in the XML mapper file.
See https://mybatis.org/mybatis-3/ko/sqlmap-xml.html

<-- MyBatis XML Configuration -->
<typeAlias type="net.java_school.photostudio.Photo" alias="Photo"/>

Proceed without creating a MyBatis configuration file since only one Java Bean.

PhotoService.java
package net.java_school.photostudio;

import java.util.List;

public interface PhotoService {

  public void add(String content);

  public int getTotalRecordCount();	
  
  public List<Photo> getPhotos(Integer startRecord, Integer endRecord);
}
PhotoServiceImpl.java
package net.java_school.photostudio;

import java.util.List;
import java.util.HashMap;

import net.java_school.mybatis.PhotoMapper;

public class PhotoServiceImpl implements PhotoService {

  private PhotoMapper photoMapper;

  public PhotoServiceImpl(PhotoMapper photoMapper) {
    this.photoMapper = photoMapper;
  }

  @Override
  public void add(String content) {
    photoMapper.insert(content);
  }

  @Override
  public int getTotalRecordCount() {
	return photoMapper.selectCountOfPhotos();
  }

  @Override
  public List<Photo> getPhotos(Integer startRecord, Integer endRecord) {
    HashMap<String, String> hashmap = new HashMap<String, String>();
    hashmap.put("start", startRecord.toString());
    hashmap.put("end", endRecord.toString());
    
    return photoMapper.selectPhotos(hashmap);
  }
}
PhotoController.java
package net.java_school.photostudio;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;
import java.util.Map;
import java.util.HashMap;

@Controller
@RequestMapping("/photo")
public class PhotoController {

  private PhotoService photoService;

  public PhotoController(PhotoService photoService) {
    this.photoService = photoService;
  }

  @PostMapping
  public String add(String content) {
    photoService.add(content);	
    return "redirect:/photo/?page=1";
  }

  private Map<String, Integer> getNumbersForPaging(int totalRecordCount, int page, int recordsPerPage, int pagesPerBlock) {

    Map<String, Integer> map = new HashMap<String, Integer>();

    int totalPageCount = totalRecordCount / recordsPerPage;
    if (totalRecordCount % recordsPerPage != 0) totalPageCount++;

    int totalBlockCount = totalPageCount / pagesPerBlock;
    if (totalPageCount % pagesPerBlock != 0) totalBlockCount++;

    int block = page / pagesPerBlock;
    if (page % pagesPerBlock != 0) block++;

    int firstPage = (block - 1) * pagesPerBlock + 1;
    int lastPage = block * pagesPerBlock;

    int prevBlock = 0;//previus block's last page
    if (block > 1) prevBlock = firstPage - 1;

    int nextBlock = 0;//next blcok's first page
    if (block < totalBlockCount) nextBlock = lastPage + 1;
    if (block >= totalBlockCount) lastPage = totalPageCount;
		
    int listItemNo = totalRecordCount - (page - 1) * recordsPerPage;
    int startRecord = (page - 1) * recordsPerPage + 1;
    int endRecord = page * recordsPerPage;

    map.put("finalPage", totalPageCount);
    map.put("firstPage", firstPage);
    map.put("lastPage", lastPage);
    map.put("prevBlock", prevBlock);
    map.put("nextBlock", nextBlock);
    map.put("startRecord", startRecord);
    map.put("endRecord", endRecord);

    return map;
  }

  @GetMapping
  public String index(Integer page, Model model) {
    if (page == null) return "redirect:/photo/?page=1";

    int recordsPerPage = 4;
    int pagesPerBlock = 10;

    int totalRecordCount = photoService.getTotalRecordCount();

    Map<String, Integer> map = getNumbersForPaging(totalRecordCount, page, recordsPerPage, pagesPerBlock);
    Integer startRecord = map.get("startRecord");
    Integer endRecord = map.get("endRecord");

    List<Photo> photos = photoService.getPhotos(startRecord, endRecord);

    Integer prevBlock = map.get("prevBlock");
    Integer nextBlock = map.get("nextBlock");
    Integer firstPage = map.get("firstPage");
    Integer lastPage = map.get("lastPage");
    Integer finalPage = map.get("finalPage");

    model.addAttribute("photos", photos);
    model.addAttribute("prevBlock", prevBlock);
    model.addAttribute("nextBlock", nextBlock);
    model.addAttribute("firstPage", firstPage);
    model.addAttribute("lastPage", lastPage);
    model.addAttribute("finalPage", finalPage);

    return "photo/index";
  }
}

View
To use JSTL, add the following to the index.jsp file.

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

Use the JSTL code to set the passed data in the view.

index.jsp
<div id="photos">
  <c:forEach var="photo" items="${photos }" varStatus="status">
  <img width="640" alt="p_${photo.no }" src="${photo.content }" />
  </c:forEach>
</div>

<div id="paging">
  <c:if test="${param.page > 1 }">
  <a href="?page=${param.page - 1 }" title="${param.page - 1}">◁ Back</a>
  </c:if>

  <c:if test="${prevBlock > 0}">
  <a href="?page=1" title="1">1</a>
  <a href="?page=${prevBlock }" title="${prevBlock }">...</a>
  </c:if>

  <c:forEach var="i" begin="${firstPage }" end="${lastPage }" varStatus="status">
  <c:choose>
    <c:when test="${param.page == i}">
    <strong>${i }</strong>
    </c:when>
    <c:otherwise>
    <a href="?page=${i }" title="${i }">${i }</a>
    </c:otherwise>
  </c:choose>
  </c:forEach>

  <c:if test="${nextBlock > 0 }">
  <a href="?page=${nextBlock }" title="${nextBlock }">...</a>
  <a href="?page=${finalPage }" title="${finalPage }">${finalPage }</a>
  </c:if>

  <c:if test="${param.page < finalPage }">
  <a href="?page=${param.page + 1 }" title="${param.page + 1 }">Next▷ </a>
  </c:if>

  <form id="addForm" method="post">
    <input type="text" name="content" width="500" />
    <input id="submit" type="submit" value="Send" /> 
  </form>

</div>

Spring Auto Scan

You can use Spring's auto-scan to reduce configuration file content.
<context:component-scan /> scans Spring's components like Controllers, services, and Repositories.
<mybatis:scan /> scans mapper instances.

mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://mybatis.org/schema/mybatis-spring
    http://mybatis.org/schema/mybatis-spring.xsd">

  <mvc:annotation-driven />

  <context:component-scan base-package="net.java_school.english,
      net.java_school.photostudio" />

  <mybatis:scan base-package="net.java_school.mybatis" />

  <bean id="internalResourceViewResolver" 
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    <property name="prefix" value="/WEB-INF/views/" />
    <property name="suffix" value=".jsp" />
  </bean>

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
      destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
    <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" />
    <property name="username" value="java" />
    <property name="password" value="school" />
    <property name="maxActive" value="100" />
    <property name="maxWait" value="1000" />
    <property name="poolPreparedStatements" value="true" />
    <property name="defaultAutoCommit" value="true" />
    <property name="validationQuery" value=" SELECT 1 FROM DUAL" />
  </bean>

  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
  </bean>
</beans>

<context:component-scan /> lets you to use @Autowired annotation.

PhotoController.java
import org.springframework.beans.factory.annotation.Autowired;

@Controller
public class PhotoController {

  @Autowired
  private PhotoService photoService;

  /* Remove the Constructor.
  public PhotoController(PhotoService photoService) {
    this.photoService = photoService;
  }
  */

  //..omit..
}
PhotoServiceImpl.java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;

@Service
public class PhotoServiceImpl implements PhotoService {

  @Autowired
  private PhotoMapper photoMapper;

  /* Remove a Constructor
  public PhotoServiceImpl(PhotoMapper photoMapper) {
    this.photoMapper = photoMapper;
  }
  */

  //..omit..
}

Add annotations to WordCardDao, WordCardService, and HomeController so that <context:component-scan /> can scan them.

src/main/java/net/java_school/english/WordCardDao.java

WordCardDao.java
import org.springframework.stereotype.Repository;

@Repository
public class WordCardDao {

  //..omit..
}

src/main/java/net/java_school/english/WordCardService.java

WordCardService.java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;

@Service
public class WordCardService {

  @Autowired
  private WordCardDao wordCardDao;

  //..omit..
}

src/main/java/net/java_school/english/HomeController.java

HomeController.java
import org.springframework.beans.factory.annotation.Autowired;

@Controller
public class HomeController {

  @Autowired
  private WordCardService wordCardService;
  
  //..omit..
}

Set up the project in eclipse

Start Eclipse and select your workspace as C:\www. In the Project Explorer view, use the right mouse button to display context menus. Import the spring-bbs project into Eclipse.

Context menu Import

Maven Project Import

If the pom.xml file changes, you need to synchronize pom.xml with Eclipse.

eclipse and pom.xml synchronized

References