end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

spring boot for java + mybatis で sql動的生成

sql mapperな mybatis における sql動的生成は、 .xml内に記載する方法をよく見かけますが、 .java 内にも記載できるので、メモ。

以降で様々、記載していますが、ポイントは、 『UserMstRepository interfaceにおける @SelectProvider() + org.apache.ibatis.jdbc.SQL 』です。

その他、mybatisの document 等、以下が参考になります。

class 構成

┌@Controller class ┐
│MyController      │
└─┬───────┘
┌@Service class──┐
│UserMstService    │
└─┬───────┘
┌@Mapper interface ┐
│UserMstRepository │
└─┬───────┘
┌@Entity class ──┐┌複合pkey class  ─┐
│UserMstEntity     ├┤UserMstEntityPkey │
└─┬───────┘└─────────┘
┌mysql ──────┐
│DB TBL user_mst   │
└─────────┘

pom.xml

様々、記載していますが、mybatis-spring-boot-starter により 動的sqlも利用できます。

modelmapper-spring を、entity->dto変換用に記載していますが、 今回は使用しませんでした。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.end0tknr</groupId>
  <artifactId>MySpring4</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>MySpring4</name>
  <description>seasar2 to spring</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.4</version>
    </dependency>
    
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency> <!-- 【JSP & タグ利用】 -->
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
    <dependency>
      <groupId>org.modelmapper.extensions</groupId>
      <artifactId>modelmapper-spring</artifactId>
      <version>2.4.2</version>
    </dependency>
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

application.properties

「mybatis.configuration.map-underscore-to-camel-case=true」により mysqlのsnake caseなcolumn名称が、javaのentityで使用するcamel caseに変換。

# JSP利用の為
spring.mvc.view.prefix= /WEB-INF/view/
spring.mvc.view.suffix= .jsp

# DB接続
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.63.3:3306/bukkenkoutei
spring.datasource.username=xparcadm
spring.datasource.password=ないしょ

# MyBatis
mybatis.mapper-locations:classpath*:/mapper/mysql/*.xml
mybatis.configuration.map-underscore-to-camel-case=true

@Controller と jsp

debug?用に用意しています。

package jp.end0tknr;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyController {

    @Autowired
    UserMstService userMstService;

    @RequestMapping(value="/index1")
    public String index1(Model model) {
        System.out.println("START index1");
        return "index";
    }

    @RequestMapping(value="/index2")
    public String index2(Model model) {
        System.out.println("START index2");

        UserMstEntity userMstEntity = userMstService.findById(1,"end0tknr1");
        System.out.println(userMstEntity);
        return "index";
    }

    @RequestMapping(value="/index3")
    public String index3(Model model) {
        System.out.println("START index3");

        List<UserMstEntity> userMstEntities =
                userMstService.selectUserIdList("end0tknr", 1);
        System.out.println(userMstEntities);
        return "index";
    }
}
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>

<a href="/index1">INDEX 1</a><br/>
<a href="/index2">INDEX 2</a><br/>
<a href="/index3">INDEX 3</a><br/>

</body>
</html>

@Entity

package jp.end0tknr;

import java.io.Serializable;
import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import javax.persistence.Version;

import lombok.Data;

@Entity
@Table(name = "user_mst")
@IdClass(value=UserMstEntityPkey.class)
@Data
public class UserMstEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /** kyotenIdプロパティ */
    @Id
    @Column(precision = 10, nullable = false, unique = false)
    public Integer kyotenId;

    /** userXmileidプロパティ */
    @Id
    @Column(length = 50, nullable = false, unique = false)
    public String userXmileid;

    /** isDeleteプロパティ */
    @Column(precision = 10, nullable = false, unique = false)
    public Integer isDelete;

    /** versionプロパティ */
    @Version
    @Column(precision = 10, nullable = false, unique = false)
    public Integer version;

    /** userNameプロパティ */
    @Column(length = 400, nullable = false, unique = false)
    public String userName;

    // 以降、省略
}
package jp.end0tknr;

import java.io.Serializable;

import lombok.Data;

@Data
public class UserMstEntityPkey implements Serializable {
    public Integer kyotenId;
    public String userXmileid;
}

DB TBL user_mst

mysql> desc user_mst;
+------------------------------+--------------+------+-----+---------+-------+
| Field                        | Type         | Null | Key | Default | Extra |
+------------------------------+--------------+------+-----+---------+-------+
| kyoten_id                    | int          | NO   | PRI | NULL    |       |
| user_xmileid                 | varchar(50)  | NO   | PRI | NULL    |       |
| user_name                    | varchar(400) | NO   |     | NULL    |       |
| is_delete                    | int          | NO   |     | 0       |       |
| reg_xmileid                  | varchar(50)  | NO   |     | NULL    |       |
| reg_name                     | varchar(400) | NO   |     | NULL    |       |
| reg_date                     | datetime     | NO   |     | NULL    |       |
   <以降、省略>
+------------------------------+--------------+------+-----+---------+-------+

UserMstRepository

package jp.end0tknr;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.jdbc.SQL;

@Mapper
public interface UserMstRepository {

    @Select("SELECT * FROM user_mst WHERE kyoten_id=${kyotenId} and user_xmileid='${userXmileid}'")
    UserMstEntity findById(Integer kyotenId, String userXmileid);

    @SelectProvider(type=UserMstRepositorySqlProvider.class,
            method="selectUserIdList")
    List<UserMstEntity> selectUserIdList(String userId, int kyotenId);

    class UserMstRepositorySqlProvider{

        public String selectUserIdList(String userId, int kyotenId) {
            return new SQL() {{
                SELECT("*");
                FROM("user_mst");
                WHERE("user_xmileid LIKE '${userId}%'");
                WHERE("kyoten_id=${kyotenId}");
                WHERE("is_delete=0");
            }}.toString();


        }
    }
}

UserMstService

package jp.end0tknr;

import java.util.List;

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

@Service
public class UserMstService {

    @Autowired
    UserMstRepository userMstRepository;

    public UserMstEntity findById(Integer kyotenId, String userXmileid) {
        return userMstRepository.findById(kyotenId, userXmileid);
    }

    public List<UserMstEntity> selectUserIdList(String userXmileid, Integer kyotenId) {
        return userMstRepository.selectUserIdList(userXmileid, kyotenId);
    }
}

springboot for java で 複数Controller間共有sessionには、@SessionScope

以下の通り。

以前は、@Scope と指定していましたが、最近では @SessionScope のようです。

参考url

dir構成

PATH NOTE
pom.xml 素朴な内容で、@SessionScope 用のdependencyはありません
MySessionBean.java @Component と @SessionScope がポイント
Index?Action.java MySessionBeanによるセッション変数test用です
templates/index?.html

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.end0tknr</groupId>
  <artifactId>MySpring3</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>MySpring3</name>
  <description>seasar2 to spring</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

MySessionBean.java

package jp.end0tknr;

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;
import lombok.Data;

@Data
@Component
@SessionScope
public class MySessionBean {
    private String userId     = "USER's NAME";
    private String firstName  = "USER's First Name";
    private String lastName   = "USER's Last  Name";
    private String tmpMessage = "TEMP MESSAGE";
}

Index?Action.java

Index1Action.java

package jp.end0tknr;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class Index1Action {

    @Autowired
    MySessionBean mySessionBean;

    @RequestMapping(value = "/index1")
    public String index(Model model) {

        mySessionBean.setTmpMessage("VISITED Index1");

        model.addAttribute("mySessionBean", mySessionBean);
        return "index1";
    }
}

Index2Action.java

package jp.end0tknr;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class Index2Action {
    @Autowired
    MySessionBean mySessionBean;

    @RequestMapping(value = "/index2")
    public String index(Model model) {
        model.addAttribute("mySessionBean", mySessionBean);

        return "index2";
    }
}

templates/index?.html

templates/index1.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SESSION TEST-1</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SESSION TEST-1</h1>
  <p>
    <a href="/index1">INDEX1</a><br/>
    <a href="/index2">INDEX2</a><br/>
  </p>

  <p><div th:text="${mySessionBean.userId}"></div></p>
  <p><div th:text="${mySessionBean.tmpMessage}"></div></p>
  </body>
</html>

templates/index2.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SESSION TEST-2</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SESSION TEST-2</h1>
  <p>
    <a href="/index1">INDEX1</a><br/>
    <a href="/index2">INDEX2</a><br/>
  </p>

  <p><div th:text="${mySessionBean.userId}"></div></p>
  <p><div th:text="${mySessionBean.tmpMessage}"></div></p>
  </body>
</html>

spring security for java における http header/cookie認証

login認証画面も、spring boot + spring security で実装した サンプルコードはよく見かけますが、 SSO認証部分をOpenAM等の別サービスに役割分担したい為、試してみました。

目次

参考url

今回の http header/cookie認証の為、参考にしました。というより、写経です

以下は、エラー画面表示の参考にしました

以下の 14章 チュートリアルで、まず、Springの基本?を確認しました。

dir 構成

PATH NOTE
pom.xml 通常のform認証同様、spring-boot-starter-security 使用
config/WebSecurityConfig.java 認証要のpathや、login filterを指定
action/*Action.java 通常?のspring bootの @Controller です
action/ErrorAction.java implements ErrorController で 認証や権限等のerror表示
filter/AppPreAuthenticatedFilter.java login filterです。認証情報取得以外は
spring securityに任せます
model/User.java @Entity ですが、@Proxy(lazy = false) もつけています
repository/UserRepository.java 通常の JpaRepository
service/AppUserDetails.java spring security が参照する為、model/User.java を内包
service/AppUserDetailsService.java implements AuthenticationUserDetailsService
resources/application.properties postgresへの接続設定のみ記載しています

pom.xml

余計な dependency も記載していますが、 通常のform認証同様、spring-boot-starter-security 使用がポイントです。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.end0tknr</groupId>
  <artifactId>MySpring2</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>MySpring2</name>
  <description>seasar2 to spring</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>

  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
  
</project>

config/WebSecurityConfig.java

認証要のpathや、login filterに使用する AppPreAuthenticatedFilter を記載しています。

package jp.end0tknr.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

import jp.end0tknr.domain.service.AppUserDetailsService;
import jp.end0tknr.filter.AppPreAuthenticatedFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>
    authenticationUserDetailsService() {
        return (AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>)
                new AppUserDetailsService();
    }

    @Bean
    public PreAuthenticatedAuthenticationProvider
                    preAuthenticatedAuthenticationProvider() {
        PreAuthenticatedAuthenticationProvider provider
            = new PreAuthenticatedAuthenticationProvider();
        provider.
            setPreAuthenticatedUserDetailsService(authenticationUserDetailsService());
        provider.
            setUserDetailsChecker(new AccountStatusUserDetailsChecker());
        return provider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.authenticationProvider(
                preAuthenticatedAuthenticationProvider()
                );
    }

    @Bean
    public AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter()
            throws Exception {
        AppPreAuthenticatedFilter filter = new AppPreAuthenticatedFilter();

        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
        .antMatchers("/", "/public","/error").permitAll()
        .antMatchers("/admin").hasRole("ADMIN")
        .anyRequest().authenticated()
        .and()
        .addFilter(preAuthenticatedProcessingFilter());
    }
}

action/*Action.java

通常?のspring bootの @Controller です。 今回の認証用に特別な処理は行っていません。

action/PrivateAction.java

package jp.end0tknr.action;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class PrivateAction {
    @RequestMapping(value = "/private", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "private";
    }
}

templates/private.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY PRIVATE PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY PRIVATE PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

action/AdminAction.java

package jp.end0tknr.action;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class AdminAction {
    @RequestMapping(value = "/admin", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "admin";
    }
}

templates/admin.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY ADMIN's PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY ADMIN's PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

action/PublicAction.java

package jp.end0tknr.action;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class PublicAction {

    @RequestMapping(value = "/public", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "public";
    }
}

templates/public.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY PUBLIC PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY PUBLIC PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

action/ErrorAction.java

認証エラーが発生すると、/error へ redirectするようですが、 error画面の表示方法を知らなかったので、取り敢えず実装しました。

403だけでなく、404や500エラーも同じ/errorで表示されるはずです。

package jp.end0tknr.action;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ErrorAction implements ErrorController {

    @Override
    public String getErrorPath() {
      return "/error";
    }

    @RequestMapping(value = "/error")
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "error";
    }
}

templates/error.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY ERROR PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY ERROR PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

filter/AppPreAuthenticatedFilter.java

認証情報取得のみ、記載すれば、後は spring securityに任せのようです。

以下では、ユーザIDを固定文字列(aaaaやcccc等)で渡していますが、

SpringBoot for java で、http headerやcookieを取得 - end0tknr's kipple - web写経開発

を参考に、http headerやcookie から取得して下さい。

package jp.end0tknr.filter;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

public class AppPreAuthenticatedFilter extends AbstractPreAuthenticatedProcessingFilter{

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        //リクエストからユーザーID等のユーザー情報を抽出
        //return "hoge";
        //return "aaaa";
        return "cccc";

    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        //リクエストから証明書情報を抽出。DB等にある場合もある?
        return "";
    }

}

model/User.java

通常の @Entity では、エラーとなった為、@Proxy(lazy = false) を追加しています。

package jp.end0tknr.domain.model;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.Proxy;

import lombok.Data;

@Entity
@Table(name = "usr")
@Proxy(lazy = false)
@Data
public class User {
    @Id
    private String userId;
    private String password;
    private String firstName;
    private String lastName;
    private String roleName;
}
myspring=> \d usr
                          Table "public.usr"
   Column   |          Type          | Collation | Nullable | Default 
------------+------------------------+-----------+----------+---------
 user_id    | character varying(255) |           | not null | 
 first_name | character varying(255) |           | not null | 
 last_name  | character varying(255) |           | not null | 
 password   | character varying(255) |           | not null | 
 role_name  | character varying(255) |           | not null | 
Indexes:
    "usr_pkey" PRIMARY KEY, btree (user_id)
Referenced by:
    TABLE "reservation" CONSTRAINT "fk_recqnfjcp370rygd9hjjxjtg" FOREIGN KEY (user_id) REFERENCES usr(user_id)

myspring=> select * from usr;
   user_id   | first_name | last_name |                 password             | role_name 
-------------+------------+-----------+--------------------------------------+-----------
 taro-yamada | 太郎       | 山田      | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER
 aaaa        | Aaa        | Aaa       | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER
 bbbb        | Bbb        | Bbb       | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER
 cccc        | Ccc        | Ccc       | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | ADMIN
(4 rows)

repository/UserRepository.java

package jp.end0tknr.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import jp.end0tknr.domain.model.User;

public interface UserRepository extends JpaRepository<User, String> {
}

service/AppUserDetails.java

spring security が参照する為、model/User.java を内包し、 spring security の UserDetails が必要とするmethodを追加しています。

package jp.end0tknr.domain.service;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import jp.end0tknr.domain.model.User;


// spring security 認証で使用する user定義。
// jp.end0tknr.domain.model.User を内包。
// Spring徹底入門 14章も参照。
//   https://www.shoeisha.co.jp/book/detail/9784798142470
public class AppUserDetails implements UserDetails {
    private final User user;

    public AppUserDetails(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // spring securityでは「ROLE_ADMIN」のように「ROLE_」の接頭語が必要な為
        // https://qiita.com/yokobonbon/items/7d729bd8085f3fb898bb
        // https://docs.spring.io/spring-security/site/docs/current/reference/html5
        return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName());
    }

    @Override
    public String getPassword() {
        return this.user.getPassword();
    }

    @Override
    public String getUsername() {
        return this.user.getUserId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

service/AppUserDetailsService.java

implements AuthenticationUserDetailsService なserviceクラスです。

package jp.end0tknr.domain.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Service;

import jp.end0tknr.domain.model.User;
import jp.end0tknr.domain.repository.UserRepository;

@Service
public class AppUserDetailsService
implements  AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token)
            throws UsernameNotFoundException {

        String userName=(String)token.getPrincipal();

        // Object credentials = token.getCredentials();


        User user;
        try {
            user = userRepository.getOne(userName);
        } catch (Exception e) {
            System.out.println("NOT FOUND USER A");
            throw new UsernameNotFoundException(userName + " is not found.");
        }

        if (user == null) {
            System.out.println("NOT FOUND USER B");
            throw new UsernameNotFoundException(userName + " is not found.");
        }

        System.out.println("FOUND USER");

        return new AppUserDetails(user);
    }

}

resources/application.properties

spring.jpa.database=POSTGRESQL
spring.datasource.url=jdbc:postgresql://192.168.63.3:5432/myspring
spring.datasource.username=ないしょ
spring.datasource.password=ないしょ

SpringBoot for java で、http headerやcookieを取得

以下の通り、

  • @RequestHeader や @CookieValue から取得
  • HttpServletRequest 経由で取得

の2種類があるみたい

package jp.co.sexy.bknkouhei.action;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloAction {
    static protected Logger logger =
            LogManager.getLogger(new Object(){}.getClass());

    @RequestMapping(value="/hello", method=RequestMethod.GET)
    public ModelAndView index(ModelAndView modelview) {

        logger.info("HOGEHOGE");

        modelview.setViewName("hello/index");
        return modelview;
    }

    @RequestMapping(value="/hello2", method=RequestMethod.GET)
    public ModelAndView index2(
            @RequestHeader(name="User-Agent",required=false) String userAgent,
            @CookieValue(name="JSESSIONID",required=false) String jSessionId,
            ModelAndView modelview) {

        logger.info("USER_AGENT:"+ userAgent );
        logger.info("JSESSIONID:"+ jSessionId);

        modelview.setViewName("hello/index");
        return modelview;
    }

    @RequestMapping(value="/hello3", method=RequestMethod.GET)
    public ModelAndView index3(HttpServletRequest httpRequest,
            ModelAndView modelview) {

        logger.info("HEADER>USER_AGENT:"+ httpRequest.getHeader("User-Agent"));

        Cookie[] cookies = httpRequest.getCookies();
        for (int i = 0; i<cookies.length; i++) {
            logger.info("COOKIE>"+ cookies[i].getName() +":"+cookies[i].getValue() );
        }

        modelview.setViewName("hello/index");
        return modelview;
    }

    @RequestMapping(value="/hello", method=RequestMethod.POST)
    public ModelAndView send(
            BindingResult errors,
            ModelAndView modelview) {

        logger.info("START This method.");

        String msg = "";

        // validation errorがある場合、メッセージ等を取得
        if(errors.hasErrors()){
            logger.error("This method has error.");

            for (FieldError fieldErr : errors.getFieldErrors()) {
                msg += "Field:" + fieldErr.getField();
                msg += " x Code:" + fieldErr.getCode();
                msg += " x Msg:" + fieldErr.getDefaultMessage();
                msg += "<hr/>";
            }
        }

        logger.info("MSG" + msg);

        modelview.addObject("msg",msg);
        modelview.setViewName("hello/index");
        return modelview;
    }
}

最近の SpringBootで thymeleaf-extras-java8time を利用する際、 @Configuration なクラスは不要

タイトルの通りです。

pom.xml への thymeleaf-extras-java8time 追加だけで十分になっており、 以前のような @Configuration ThymeleafConfig を作成すると、 SpringBoot 起動時にエラーとなります。

pom.xml

<dependency>
  <groupId>org.thymeleaf.extras</groupId>
  <artifactId>thymeleaf-extras-java8time</artifactId>
  <version>2.1.0.RELEASE</version>
</dependency>

jp.end0tknr.config.ThymeleafConfig.java

package jp.end0tknr.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;

@Configuration
public class ThymeleafConfig {
    @Bean
    public Java8TimeDialect java8TimeDialect() {
    return new Java8TimeDialect();
    }
}

エラーメッセージ

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
[32m :: Spring Boot :: [39m              [2m (v2.4.5)[0;39m

[2m2021-05-05 19:05:08.922[0;39m [32m INFO[0;39m [35m14240[0;39m [2m---[0;39m [2m[           main][0;39m [36mjp.end0tknr.MySpringApplication         [0;39m [2m:[0;39m Starting MySpringApplication using Java 1.8.0_202 on LAPTOP-Q767CC9I with PID 14240 (C:\Users\end0t\workspace_eclipse_2020\MySpring\target\classes started by end0t in C:\Users\end0t\workspace_eclipse_2020\MySpring)
[2m2021-05-05 19:05:08.926[0;39m [32m INFO[0;39m [35m14240[0;39m [2m---[0;39m [2m[           main][0;39m [36mjp.end0tknr.MySpringApplication         [0;39m [2m:[0;39m No active profile set, falling back to default profiles: default
[2m2021-05-05 19:05:09.509[0;39m [31mERROR[0;39m [35m14240[0;39m [2m---[0;39m [2m[           main][0;39m [36mo.s.boot.SpringApplication              [0;39m [2m:[0;39m Application run failed

java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration.propertySourcesPlaceholderConfigurer
    at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:60) ~[spring-boot-autoconfigure-2.4.5.jar:2.4.5]
    at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:193) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:153) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:129) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:343) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:247) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:311) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:112) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:746) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:564) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:782) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:774) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:339) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1340) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1329) [spring-boot-2.4.5.jar:2.4.5]
    at jp.end0tknr.MySpringApplication.main(MySpringApplication.java:10) [classes/:na]
Caused by: java.lang.IllegalStateException: Failed to introspect Class [jp.end0tknr.config.ThymeleafConfig] from ClassLoader [sun.misc.Launcher$AppClassLoader@764c12b6]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:481) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:358) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.util.ReflectionUtils.getUniqueDeclaredMethods(ReflectionUtils.java:414) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.lambda$getTypeForFactoryMethod$2(AbstractAutowireCapableBeanFactory.java:747) ~[spring-beans-5.3.6.jar:5.3.6]
    at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660) ~[na:1.8.0_202]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryMethod(AbstractAutowireCapableBeanFactory.java:746) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:685) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:656) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1670) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:570) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:542) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition.collectBeanNamesForType(OnBeanCondition.java:238) ~[spring-boot-autoconfigure-2.4.5.jar:2.4.5]
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getBeanNamesForType(OnBeanCondition.java:231) ~[spring-boot-autoconfigure-2.4.5.jar:2.4.5]
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getBeanNamesForType(OnBeanCondition.java:221) ~[spring-boot-autoconfigure-2.4.5.jar:2.4.5]
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchingBeans(OnBeanCondition.java:169) ~[spring-boot-autoconfigure-2.4.5.jar:2.4.5]
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchOutcome(OnBeanCondition.java:144) ~[spring-boot-autoconfigure-2.4.5.jar:2.4.5]
    at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47) ~[spring-boot-autoconfigure-2.4.5.jar:2.4.5]
    ... 18 common frames omitted
Caused by: java.lang.NoClassDefFoundError: org/thymeleaf/dialect/IExpressionEnhancingDialect
    at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_202]
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_202]
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[na:1.8.0_202]
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) ~[na:1.8.0_202]
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74) ~[na:1.8.0_202]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369) ~[na:1.8.0_202]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363) ~[na:1.8.0_202]
    at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_202]
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_202]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_202]
    at java.lang.Class.getDeclaredMethods0(Native Method) ~[na:1.8.0_202]
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) ~[na:1.8.0_202]
    at java.lang.Class.getDeclaredMethods(Class.java:1975) ~[na:1.8.0_202]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:463) ~[spring-core-5.3.6.jar:5.3.6]
    ... 34 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.IExpressionEnhancingDialect
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_202]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_202]
    ... 50 common frames omitted

[2m2021-05-05 19:05:09.516[0;39m [33m WARN[0;39m [35m14240[0;39m [2m---[0;39m [2m[           main][0;39m [36mo.s.boot.SpringApplication              [0;39m [2m:[0;39m Unable to close ApplicationContext

java.lang.IllegalStateException: Failed to introspect Class [jp.end0tknr.config.ThymeleafConfig] from ClassLoader [sun.misc.Launcher$AppClassLoader@764c12b6]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:481) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:358) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.util.ReflectionUtils.getUniqueDeclaredMethods(ReflectionUtils.java:414) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.lambda$getTypeForFactoryMethod$2(AbstractAutowireCapableBeanFactory.java:747) ~[spring-beans-5.3.6.jar:5.3.6]
    at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660) ~[na:1.8.0_202]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryMethod(AbstractAutowireCapableBeanFactory.java:746) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:685) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:656) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1670) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:570) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:542) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:667) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:659) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.context.support.AbstractApplicationContext.getBeansOfType(AbstractApplicationContext.java:1300) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.boot.SpringApplication.getExitCodeFromMappedException(SpringApplication.java:914) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.getExitCodeFromException(SpringApplication.java:902) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.handleExitCode(SpringApplication.java:889) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.handleRunFailure(SpringApplication.java:830) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:349) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1340) [spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1329) [spring-boot-2.4.5.jar:2.4.5]
    at jp.end0tknr.MySpringApplication.main(MySpringApplication.java:10) [classes/:na]
Caused by: java.lang.NoClassDefFoundError: org/thymeleaf/dialect/IExpressionEnhancingDialect
    at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_202]
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_202]
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[na:1.8.0_202]
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) ~[na:1.8.0_202]
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74) ~[na:1.8.0_202]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369) ~[na:1.8.0_202]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363) ~[na:1.8.0_202]
    at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_202]
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_202]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_202]
    at java.lang.Class.getDeclaredMethods0(Native Method) ~[na:1.8.0_202]
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) ~[na:1.8.0_202]
    at java.lang.Class.getDeclaredMethods(Class.java:1975) ~[na:1.8.0_202]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:463) ~[spring-core-5.3.6.jar:5.3.6]
    ... 21 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.IExpressionEnhancingDialect
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_202]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_202]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_202]
    ... 37 common frames omitted

utf8なclientから euc_jpな postgres への機種依存文字登録には、SET NAMES 'UTF8' と、mb_convert_encoding($org_char,'CP51932','UTF-8' ) (php版)

以下の通りで、ポイントは

  • SET NAMES 'UTF8'
  • mb_convert_encoding($org_char,'CP51932','UTF-8' ) によるsanitize?

DB & DB TABLE

db_rear_hon=> \l db_rear_hon
                           List of databases
    Name     | Owner  | Encoding | Collate | Ctype | Access privileges 
-------------+--------+----------+---------+-------+-------------------
 db_rear_hon | xxuser | EUC_JP   | C       | C     | 
(1 row)

db_rear_hon=> \d test_tbl
                     Table "public.test_tbl"
 Column |          Type          | Collation | Nullable | Default 
--------+------------------------+-----------+----------+---------
 id     | integer                |           | not null | 
 val    | character varying(256) |           |          | 
Indexes:
    "test_tbl_pkey" PRIMARY KEY, btree (id)

php

<?php

const COMMON_CONF = [
    "db" =>[
        "host"=>"pgsql:dbname=db_rear_hon".
        " host=localhost".
        " port=5432",
        "user"=>"bbuser",
        "pass"=>"bbuser" ],
    "postgres_client_encoding"=>"UTF-8"
];

main();


function main(){
    my_ini_set();
    
    $func_name = __CLASS__." ".__FUNCTION__;
    write_log( $func_name );

    $dbh = connect_db();
    write_log( print_r($dbh,true) );

    del_all($dbh);
    
    $add_chars = [
        "あ","髙","㎡","m²","~","〜",
        'Ⅰ','Ⅱ','Ⅲ','Ⅳ','Ⅴ','Ⅵ','Ⅶ','Ⅷ','Ⅸ','Ⅹ',
        '①','②','③','④','⑤','⑥','⑦','⑧','⑨',
        '㈱','㈲','㈳','㈵','㈶','㎡','m²',
        '髙','﨑','神','靑','黑','羽','福','館','朗','隆',
        '塚','晴','﨔','凞','凞','猪','益','祥','靖','精',
        '諸','逸','都','飯','飼','館','鶴','塚','增','寬',
        '晴','敎','朗','橫','淸','瀨','猪','靖','精','緖',
        '薰','諸','賴','郞','都','鄕','閒'];

    $i= 0;
    foreach($add_chars as $org_char){
        // sanitize for EUC_JP ≒ CP51932
        //   refer to https://www.softel.co.jp/blogs/tech/archives/5145
        $add_char = mb_convert_encoding($org_char,'CP51932','UTF-8' );
        $add_char = mb_convert_encoding($add_char,'UTF-8', 'CP51932' );
        add_row($dbh, $i++,$add_char);
    }
    
    $tbl_rows = get_all($dbh);
    write_log( print_r($tbl_rows,true) );
}


function add_row($dbh, $atri_id,$atri_val){
    $func_name = __CLASS__." ".__FUNCTION__;
    write_log( "$func_name '$atri_id':'$atri_val'" );

    $sql = "insert into test_tbl (id,val) values(?,?)";

    try {
        $sth = $dbh->prepare($sql);
        $sth->execute([$atri_id,$atri_val]);
    } catch (PDOException $e) {
        $tmp_msg = join(" ",
                        ['ERROR fail sql ',
                         $e->getMessage(),
                         $sql,
                         "'$atri_id':'$atri_val'"]);
        write_log($tmp_msg);
        return false;
    }
    return true;
}

function get_all($dbh){
    $func_name = __CLASS__." ".__FUNCTION__;
    write_log( $func_name );

    $sql = "select * from test_tbl";

    try {
        $sth = $dbh->prepare($sql);
        $sth->execute();
    } catch (PDOException $e) {
        write_log('ERROR fail sql '. $e->getMessage(). " $sql");
        return [];
    }

    $rows = $sth->fetchAll(PDO::FETCH_ASSOC);
    return $row;
}


function del_all($dbh){
    $func_name = __CLASS__." ".__FUNCTION__;
    write_log( $func_name );

    $sql = "delete from test_tbl";

    try {
        $sth = $dbh->prepare($sql);
        $sth->execute();
    } catch (PDOException $e) {
        write_log('ERROR fail sql '. $e->getMessage(). " $sql");
        return false;
    }
    return true;
}


function connect_db(){
    $func_name = __CLASS__." ".__FUNCTION__;
    write_log( $func_name );
        
    $common_conf = COMMON_CONF;

    try {
        $dbh = new PDO($common_conf['db']['host'],
                       $common_conf['db']['user'],
                       $common_conf['db']['pass'],
                       [PDO::ATTR_ERRMODE =>PDO::ERRMODE_EXCEPTION]);
        
    } catch (Exception $e) {
        $tmp_msg = "ERROR fail $func_name " . $e->getMessage();
        write_log($tmp_msg);
        exit($tmp_msg);
    }
    
    if($common_conf["postgres_client_encoding"]){
        set_postgres_client_encoding($dbh,'UTF8');
    }
    
    return $dbh;
}

// refer to https://www.postgresql.jp/document/12.0/html/multibyte.html
function set_postgres_client_encoding($dbh,$encoding){
    $func_name = __CLASS__." ".__FUNCTION__;
    write_log( $func_name );

    $sql = "SET NAMES '$encoding'";

    try {
        $sth = $dbh->prepare($sql);
        $sth->execute();
    } catch (PDOException $e) {
        $this->write_log('ERROR fail sql '. $e->getMessage(). " $sql");
        return false;
    }
    return true;
}


function write_log($msg){
    error_log($msg);
}


function my_ini_set(){
    ini_set("log_errors",     "On" );
    ini_set("error_reporting","E_ALL");
    
    ini_set("date.timezone","Asia/Tokyo");
    
    mb_language('Japanese');
    mb_internal_encoding('UTF-8');

    ini_set('error_log', 'php://stderr');
}

cross domainなjsonpでは、postが使用できない為、php + curlで proxy ? を作成

以下のような感じかと思います

<?php

const REMOTE_API = "http://192.168.63.3/cgi-bin2/form.php";

main();


function main(){
    my_ini_set();

    $getdata  = $_GET;
    $postdata = $_POST;
    $referer  = $_SERVER['HTTP_REFERER'];

    $response = post_to_remote_url($getdata,$postdata,$referer);

    http_response_code( $response["head"]["http_code"] );
    header("Content-Type: ". $response["head"]["content_type"]);
    echo $response["body"];
}


function my_ini_set(){  // php.ini への直接登録でなく、こちらで設定
    ini_set("log_errors",     "On" );
    ini_set("error_reporting","E_ALL");
    
    ini_set("date.timezone","Asia/Tokyo");
    
    mb_language('Japanese');
    mb_internal_encoding('UTF-8');
}

function post_to_remote_url($getdata,$postdata,$referer){

    $url = REMOTE_API;
    if( count($getdata) ){
        $url .= "?" . http_build_query($getdata);
    }

    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_TIMEOUT,10);

    curl_setopt($ch, CURLOPT_REFERER, $referer);
    curl_setopt($ch, CURLOPT_URL,     $url);
    curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($postdata));
    
    // 証明書の検証なし
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,false);
    // curl_exec()の返値を文字列で出力
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
    // 以下をtrueとすると、http_status≧400以上で error
    curl_setopt($ch, CURLOPT_FAILONERROR,   false);
    
    $ret_vals = [];
    $ret_vals["head"] = curl_getinfo($ch);
    $ret_vals["body"] = curl_exec($ch);
    
    curl_close($ch);

    return $ret_vals;
}

php7 の make で fatal error: my_global.h: No such file or directory

php7を再installしようと、configure & make したところ、 「fatal error: my_global.h: No such file or directory」の 以前はなかったerrorが発生。

参考urlを見て、 mysql_config → mysqlnd にすることで解消

発生したエラー

$ cat /etc/redhat-release 
CentOS Linux release 8.3.2011
$ pwd
/home/end0tknr/tmp/php-7.2.24

$ ./configure \
  --with-apxs2=/usr/bin/apxs \
  --enable-mbstring \
  --with-mysqli=/usr/bin/mysql_config \
  --with-pdo-mysql=/usr/bin/mysql_config \
  --with-pdo-pgsql=/usr/bin/pg_config \
  --with-curl=/usr/lib
  
$ make
  :
In file included from /home/end0tknr/tmp/php-7.2.24/ext/mysqli/mysqli.c:34:
/home/end0tknr/tmp/php-7.2.24/ext/mysqli/php_mysqli_structs.h:63:10:
fatal error: my_global.h: No such file or directory
 #include <my_global.h>
          ^~~~~~~~~~~~~
compilation terminated.
make: *** [Makefile:1123: ext/mysqli/mysqli.lo] Error 1

解消した configure

./configure \
  --with-apxs2=/usr/bin/apxs \
  --enable-mbstring \
  --with-mysqli=mysqlnd \
  --with-pdo-mysql=mysqlnd \
  --with-pdo-pgsql=/usr/bin/pg_config \
  --with-curl=/usr/lib

参考url

Lark + html.parser for python による jsp parse (拡張BNF記法?)

再x2 - antlr4-python3-runtime for python3 による java source の parse / 構文解析 / コメント抽出 - end0tknr's kipple - web写経開発

以前、記載した上記entryでは、.java を parse しましたが、今回は、.jsp の parse .

*.jsp 全体? の BNF記法(再帰下降)をする場合、手間が増える為、 今回は、html.parser でパースし、その後、残った 「<% ~ %>」のscriptlet部分のみを Lark for python でパースします。

ただ、何となく、動作することは確認しましたが、細かくは動作確認していません。

参考url

Pythonの構文解析ライブラリLarkを使って遊んでみました | Tricorn Tech Labs

EBNF notation for HTML5 tag syntax · GitHub

Python 構文解析ライブラリLarkを使って簡単な自作言語処理系をつくる - Qiita

Welcome to Lark’s documentation! — Lark documentation

Step1 - 文法 - jsp_grammar_.lark ファイル

jsp_tag    : "<%" scriptlet "%>"
scriptlet  : "--" ws* comment_text "--"
           | "@"  ws* attr_list
           | "="  ws* expression_text
           | "!"  ws* declare_text
           |      ws* script_text
attr_list  : attr (ws+ attr)*
attr       : attr_name | attr_0_quoted | attr_1_quoted | attr_2_quoted
attr_name  : /[^\s"'\<\>\/\=%]+/
attr_0_quoted : attr_name ws* "=" ws*      attr_0_quoted_val
attr_1_quoted : attr_name ws* "=" ws* "'"  attr_1_quoted_val "'"
attr_2_quoted : attr_name ws* "=" ws* "\"" attr_2_quoted_val "\""
attr_0_quoted_val : /[^\s"'\=\<\>%]+/
attr_1_quoted_val : /[^\'%]+/
attr_2_quoted_val : /[^\"%]+/

directive_text  : tag_text
expression_text : tag_text
declare_text    : tag_text
script_text     : tag_text

comment_text : /.[^-]+/
tag_text     : /.[^%]+/
ws           : /\s/

Step2 - parse実行するpython script - parse_jsp.py

# -*- coding: utf-8 -*-

import sys
from lark import Lark
from lark import Transformer
from functools import reduce


def main():
    args = sys.argv
    text = args[1]
    with open("./jsp_grammar.lark", encoding="utf-8") as grammar:
        parser = Lark(grammar.read(),
                      parser='earley',
#                      parser='lalr',
                      start="jsp_tag")

        tree = parser.parse(text)
        result = CalcTransformer().transform(tree)
        # print(result)


class CalcTransformer(Transformer):
    def attr_name(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def attr_0_quoted_val(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def attr_1_quoted_val(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def attr_2_quoted_val(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def comment_text(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def directive_text(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def expression_text(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def declare_text(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])
    def script_text(self, tree):
        func_name = sys._getframe().f_code.co_name
        print(func_name,tree[0])

if __name__ == '__main__':
    main()

Step3 - 素朴なtest

$ /usr/local/python3/bin/python3 ./parse_jsp_0.py \
   '<%-- ************************** --%>'

comment_text ************************** 
$ /usr/local/python3/bin/python3 ./parse_jsp_0.py
    '<%@page import="jp.end0tknr.common.CommonConst"%>'

attr_name page
attr_name import
attr_2_quoted_val jp.end0tknr.common.CommonConst
$ /usr/local/python3/bin/python3 ./parse_jsp_0.py \
    '<%=CommonConst.HEADER_LINK_REGIST %>'

expression_text Tree('tag_text',
                    [Token('__ANON_8',
                   'CommonConst.HEADER_LINK_REGIST ')])
$ /usr/local/python3/bin/python3 ./parse_jsp_0.py
    '<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>'
    
attr_name page
attr_name language
attr_2_quoted_val java
attr_name contentType
attr_2_quoted_val text/html; charset=UTF-8
attr_name pageEncoding
attr_2_quoted_val UTF-8
$ /usr/local/python3/bin/python3 ./parse_jsp_0.py \
   '<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>'
   
attr_name taglib
attr_name prefix
attr_2_quoted_val c
attr_name uri
attr_2_quoted_val http://java.sun.com/jsp/jstl/core

springboot + jsp 環境で、formクラスを jsp へ渡す場合、<s:form modelAttribute="~"> で指定

無理矢理?、springboot + jsp な環境を作成しようと、 jsp内(※1)で「<s:input type="hidden" path="xeiId" />」を使用したところ、 画面アクセス時にエラーが発生(※2)。

どうやら、「<s:form>」でなく、「<s:form modelAttribute="hello2Form">」のように 引渡すformクラスを指定する必要があるらしい。

【※1】

<%@page import="jp.co.sexy.bknkoutei.common.CommonConst"%>
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="f" uri="http://sastruts.seasar.org/functions" %>
<%@taglib prefix="s" uri="http://www.springframework.org/tags/form"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>

<body>
  <div id="body-base-middle">
    <s:form>
      <div id="main-middle">
        <s:input type="hidden" path="xeiId" /><br/>
    <input type="submit" name="back" value=" 案件詳細画面へ " /><br/>
        <br/>
      </div>
    </s:form>
  </div>
</body>
</html>

【※2】

org.springframework.web.servlet.tags.form.InputTag,
Neither BindingResult nor plain target object
for bean name 'command' available as request attribute 
java.lang.IllegalStateException:
Neither BindingResult nor plain target object for bean name 'command'
available as request attribute

ちなみにContorollerクラスは以下の通りです。

package jp.co.sexy.bknkoutei.action;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import jp.co.sexy.bknkoutei.form.Hello2Form;

@Controller
public class Hello2Action {
    static protected Logger logger = LogManager.getLogger(Hello2Action.class);
    @Autowired
    MessageSource msgsrc;


    @RequestMapping(value="/hello2", method=RequestMethod.GET)
    public ModelAndView index(
            Hello2Form loginForm,
            BindingResult errors,
            ModelAndView modelview) {

        //modelview.addObject("teiId",  "TEIID99");

        modelview.setViewName("hello/bknContentList_index");
        return modelview;
    }

}

springboot + jsp 環境に 独自taglib (el式)作成

seasar2で使用の「<%@taglib prefix="f" uri="http://sastruts.seasar.org/functions" %>」 「${f:url('~')}」cloneを作成。

<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@page pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="f" uri="http://sastruts.seasar.org/functions" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <link rel="shortcut icon" href="${f:url('/ico/favicon.ico')}" type="image/x-icon" />
  <link rel="icon" href="${f:url('/ico/favicon.ico')}" type="image/x-icon" />
  <link rel="stylesheet" href="${f:url('/css/jquery-ui.custom.css')}" type="text/css" />
  <link rel="stylesheet" href="${f:url('/css/common.css')}" type="text/css" />
  <script type="text/javascript" src="${f:url('/js/jquery-2.2.4.min.js')}"></script>
</head>
  :

参考url

https://pgbox.grush.jp/ref/java/jee/jsp/elFunction.html

step1 / 3 - S2Functions.java の クローンを作成

sa-struts/S2Functions.java at master · seasarorg/sa-struts · GitHub の内、 必要なmethodを jp.co.sexy.bknkoutei.cloneseasar.S2Functions として作成

package jp.co.sexy.bknkoutei.cloneseasar;

import java.util.Arrays;

public class S2Functions {

    private static final int HIGHEST_SPECIAL = '>';
    private static String CONTEXT_PATH = "";
    private static char[][] specialCharactersRepresentation = new char[HIGHEST_SPECIAL + 1][];

    static {
        specialCharactersRepresentation['&'] = "&amp;".toCharArray();
        specialCharactersRepresentation['<'] = "&lt;".toCharArray();
        specialCharactersRepresentation['>'] = "&gt;".toCharArray();
        specialCharactersRepresentation['"'] = "&#034;".toCharArray();
        specialCharactersRepresentation['\''] = "&#039;".toCharArray();
    }

    /**
     * HTMLをエスケープします。
     *
     * @param input
     *            入力値
     * @return エスケープした結果
     */
    public static String h(Object input) {
        if (input == null) {
            return "";
        }
        String str = "";
        if (input.getClass().isArray()) {
            Class<?> clazz = input.getClass().getComponentType();
            if (clazz == String.class) {
                str = Arrays.toString((Object[]) input);
            } else if (clazz == boolean.class) {
                str = Arrays.toString((boolean[]) input);
            } else if (clazz == int.class) {
                str = Arrays.toString((int[]) input);
            } else if (clazz == long.class) {
                str = Arrays.toString((long[]) input);
            } else if (clazz == byte.class) {
                str = Arrays.toString((byte[]) input);
            } else if (clazz == short.class) {
                str = Arrays.toString((short[]) input);
            } else if (clazz == float.class) {
                str = Arrays.toString((float[]) input);
            } else if (clazz == double.class) {
                str = Arrays.toString((double[]) input);
            } else if (clazz == char.class) {
                str = Arrays.toString((char[]) input);
            } else {
                str = Arrays.toString((Object[]) input);
            }
        } else {
            str = input.toString();
        }
        return escape(str);
    }

    /**
     * 文字列をHTMLエスケープします。
     *
     * @param buffer
     *            文字列
     * @return エスケープした結果
     */
    public static String escape(String buffer) {
        int start = 0;
        int length = buffer.length();
        char[] arrayBuffer = buffer.toCharArray();
        StringBuilder escapedBuffer = null;

        for (int i = 0; i < length; i++) {
            char c = arrayBuffer[i];
            if (c <= HIGHEST_SPECIAL) {
                char[] escaped = specialCharactersRepresentation[c];
                if (escaped != null) {
                    if (start == 0) {
                        escapedBuffer = new StringBuilder(length + 5);
                    }
                    if (start < i) {
                        escapedBuffer.append(arrayBuffer, start, i - start);
                    }
                    start = i + 1;
                    escapedBuffer.append(escaped);
                }
            }
        }
        if (start == 0) {
            return buffer;
        }
        if (start < length) {
            escapedBuffer.append(arrayBuffer, start, length - start);
        }
        return escapedBuffer.toString();
    }

    /**
     * URLを計算します。
     *
     * @param input
     *            入力値
     * @return エスケープした結果
     */
    public static String url(String input) {
        return CONTEXT_PATH + input;
    }

//    public static String url(String input) {
//        String contextPath = RequestUtil.getRequest().getContextPath();
//        StringBuilder sb = new StringBuilder();
//        if (contextPath.length() > 1) {
//            sb.append(contextPath);
//        }
//        if (StringUtil.isEmpty(input)) {
//            sb.append(ActionUtil.calcActionPath());
//        } else if (!input.startsWith("/")) {
//            sb.append(ActionUtil.calcActionPath()).append(input);
//        } else {
//            String[] names = StringUtil.split(input, "/");
//            S2Container container = SingletonS2ContainerFactory.getContainer();
//            StringBuilder sb2 = new StringBuilder(50);
//            String input2 = input;
//            for (int i = 0; i < names.length; i++) {
//                if (container.hasComponentDef(sb2 + names[i] + "Action")) {
//                    String actionPath = RoutingUtil.getActionPath(names, i);
//                    String paramPath = RoutingUtil.getParamPath(names, i + 1);
//                    if (StringUtil.isEmpty(paramPath)) {
//                        input2 = actionPath + "/";
//                        break;
//                    }
//                }
//                sb2.append(names[i] + "_");
//            }
//            sb.append(input2);
//        }
//        return ResponseUtil.getResponse().encodeURL(sb.toString());
//    }

}

step2 / 3 - src/main/webapp/WEB-INF/f.tld 作成

先程のS2Functions.java 同様、 sa-struts/f.tld at master · seasarorg/sa-struts · GitHub の内、 必要なmethod を f.tld に作成

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

<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
  version="2.0">

  <description>JSTL 1.1 functions library</description>
  <display-name>JSTL functions</display-name>
  <tlib-version>1.1</tlib-version>
  <short-name>f</short-name>
  <uri>http://sastruts.seasar.org/functions</uri>

  <function>
    <description>
      Escapes characters that could be interpreted as HTML.
    </description>
    <name>h</name>
    <function-class>jp.co.sexy.bknkoutei.cloneseasar.S2Functions</function-class>
    <function-signature>java.lang.String h(java.lang.Object)</function-signature>
    <example>
      ${f:h(param:info)}
    </example>
  </function>

  <function>
    <description>
      Calculates URL.
    </description>
    <name>url</name>
    <function-class>jp.co.sexy.bknkoutei.cloneseasar.S2Functions</function-class>
    <function-signature>java.lang.String url(java.lang.String)</function-signature>
    <example>
      &lt;a href="${f:url(param:info)}" ...
    </example>
  </function>
</taglib>

step 3 / 3 - *.jspで使用

後は、jsp で使用できます。

<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@page pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="f" uri="http://sastruts.seasar.org/functions" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <link rel="shortcut icon" href="${f:url('/ico/favicon.ico')}" type="image/x-icon" />
  <link rel="icon" href="${f:url('/ico/favicon.ico')}" type="image/x-icon" />
  <link rel="stylesheet" href="${f:url('/css/jquery-ui.custom.css')}" type="text/css" />
  <link rel="stylesheet" href="${f:url('/css/common.css')}" type="text/css" />
  <script type="text/javascript" src="${f:url('/js/jquery-2.2.4.min.js')}"></script>
</head>
  :

SpringBoot で 複合主キー DBテーブルへのRepository作成時に、org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userMstRepository' defined in jp.end0tknr.UserMstRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegi

SpringBoot で 主キーが複数ある DBテーブルに対しての Entity & Repository を作成後、 eclipseからSpringBootを起動すると、以下のエラーが発生。

org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'userMstRepository' defined
in jp.end0tknr.UserMstRepository defined in @EnableJpaRepositories
declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration:
Invocation of init method failed; nested exception is
java.lang.IllegalArgumentException: This class [class jp.end0tknr.UserMstEntity]
does not define an IdClass

どうやら、pkeyを表すクラスを作成した上で、 「@IdClass指定」or「@EmbeddedId による単一pkey化」とするとよいらしい。

参考url:

http://itdoc.hitachi.co.jp/manuals/link/cosmi_v0870/APKC/EU070333.HTM

https://dev.classmethod.jp/articles/spring-data-jpa-search-2/

https://qiita.com/t-iguchi/items/9a834aac3874706d80b3

対応の概要

今回は、pkeyを表すクラスを作成した上で、@IdClassを指定しています。

種別 ファイル名 対応概要
DB TABLE user_mst column:kyoten_id , user_xmileid が primary key
Pkey クラス UserMstEntityPkey pkeyのfieldのみあります
Entityクラス UserMstEntity 通常の @Id に加え、@IdClass を指定
Repository インタフェース UserMstRepository DBアクセス手段は複数ありますが、今回はJpaRepository
Controllerクラス HelloController 動作確認用に用意。複合キーへの特別な実装はなし
JSP hello2.jsp
properties application.properties
Maven pom.xml

DB TABLE - user_mst

今回の対象は、以下の user_mst テーブルで、 kyoten_id , user_xmileid を primary key に持ちます。

mysql> desc user_mst;
+------------------------------+--------------+------+-----+---------+-------+
| Field                        | Type         | Null | Key | Default | Extra |
+------------------------------+--------------+------+-----+---------+-------+
| kyoten_id                    | int          | NO   | PRI | NULL    |       |
| user_xmileid                 | varchar(50)  | NO   | PRI | NULL    |       |
| reg_xmileid                  | varchar(50)  | NO   |     | NULL    |       |
| reg_name                     | varchar(400) | NO   |     | NULL    |       |
| reg_date                     | datetime     | NO   |     | NULL    |       |
| last_upd_xmileid             | varchar(50)  | NO   |     | NULL    |       |
| last_upd_name                | varchar(400) | NO   |     | NULL    |       |
| last_upd_date                | datetime     | NO   |     | NULL    |       |
| is_delete                    | int          | NO   |     | 0       |       |
| version                      | int          | NO   |     | NULL    |       |
| user_name                    | varchar(400) | NO   |     | NULL    |       |
| user_shozokugroup            | varchar(20)  | NO   |     | NULL    |       |
+------------------------------+--------------+------+-----+---------+-------+

Pkey クラス - UserMstEntityPkey.java

pkeyのfieldのみのクラスです。 「implements Serializable」も必要なようです。

package jp.end0tknr;

import java.io.Serializable;
import lombok.Data;

@Data
public class UserMstEntityPkey implements Serializable {
    public Integer kyotenId;
    public String userXmileid;
}

Entityクラス - UserMstEntity.java

単一PkeyのEntityクラスで指定する「@Id」に加え、 「@IdClass(value=UserMstEntityPkey.class)」を指定します。

package jp.end0tknr;

import java.io.Serializable;
import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import javax.persistence.Version;

import lombok.Data;

@Entity
@Table(name = "user_mst")
@Data
@IdClass(value=UserMstEntityPkey.class)
public class UserMstEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    /** kyotenIdプロパティ */
    @Id
    @Column(precision = 10, nullable = false, unique = false)
    public Integer kyotenId;

    /** userXmileidプロパティ */
    @Id
    @Column(length = 50, nullable = false, unique = false)
    public String userXmileid;

    /** regXmileidプロパティ */
    @Column(length = 50, nullable = false, unique = false)
    public String regXmileid;

    /** regNameプロパティ */
    @Column(length = 400, nullable = false, unique = false)
    public String regName;

    /** regDateプロパティ */
    @Column(nullable = false, unique = false)
    public Timestamp regDate;

    /** lastUpdXmileidプロパティ */
    @Column(length = 50, nullable = false, unique = false)
    public String lastUpdXmileid;

    /** lastUpdNameプロパティ */
    @Column(length = 400, nullable = false, unique = false)
    public String lastUpdName;

    /** lastUpdDateプロパティ */
    @Column(nullable = false, unique = false)
    public Timestamp lastUpdDate;

    /** isDeleteプロパティ */
    @Column(precision = 10, nullable = false, unique = false)
    public Integer isDelete;

    /** versionプロパティ */
    @Version
    @Column(precision = 10, nullable = false, unique = false)
    public Integer version;

    /** userNameプロパティ */
    @Column(length = 400, nullable = false, unique = false)
    public String userName;

    /** userShozokugroupプロパティ */
    @Column(length = 20, nullable = false, unique = false)
    public String userShozokugroup;

}

Repository インタフェース - UserMstRepository.java

DBアクセス手段は複数ありますが、今回は殆ど実装が不要な JpaRepository を 利用したインターフェース。

package jp.end0tknr;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserMstRepository extends JpaRepository<UserMstEntity, Long>{
}

Controllerクラス - HelloController.java

package jp.end0tknr;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@EnableAutoConfiguration
public class HelloController {

    @Autowired
    UserMstRepository repository;

    @RequestMapping(value="/", method=RequestMethod.GET)
    public String hello(Model model) {
        model.addAttribute("click", false);
        model.addAttribute("message", "please click!");
        return "hello2";
    }

    @RequestMapping(value="/", method=RequestMethod.POST)
    public String click(Model model) {

        Iterable<UserMstEntity> userList = repository.findAll();
        model.addAttribute("userList", userList);

        model.addAttribute("click", true);
        model.addAttribute("afterClickMsg", "It was clicked.");
        return "hello2";
    }
}

JSP - hello2.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<%@ taglib uri='http://java.sun.com/jsp/jstl/core' prefix='c'%>
<%@ page session="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Hello JSP</title>
</head>
<body>
  <div>
    <c:if test="${click == false}" >
      <c:out value="${message}" />
      <form method="post" action="/">
        <input type="submit" value="Click" />
      </form>
    </c:if>
  </div>
  <div>
    <c:if test="${click == true}" >
      <c:out value="${afterClickMsg}" />
      <div>
        <input type="submit" value="Click" disabled="disabled"/>
      </div>
    </c:if>
  </div>

  <div>
    <c:forEach var="user" items="${userList}" varStatus="status">
      <c:out value="${user.userXmileid}" /><br/>
    </c:forEach>
  </div>

</body>
</html>

properties - application.properties

spring.mvc.view.prefix: /WEB-INF/views/
spring.mvc.view.suffix: .jsp

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.63.3:3306/bknkoutei
spring.datasource.username=xparcadm
spring.datasource.password=admxparc

Maven - 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.end0tknr</groupId>
  <artifactId>MySpringJsp</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>MySpringJsp</name>
  <description>Demo project for Spring Boot</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
      <!--
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
     -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jstl</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

mysql8で、ERROR 1118 (42000) at line 2596: Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. In current row format, BLOB prefix of 0 bytes is stored inline.

とある DB のdumpデータをインポートしたところ、以下のエラーが発生。

どうやら、合計 column長があまりに長いtableが原因らしい。

$  mysql -u root -p test_db < xparc_schema.sql 
Enter password: 
ERROR 1118 (42000) at line 2596: Row size too large (> 8126).
  Changing some columns to TEXT or BLOB may help. In current row format,
  BLOB prefix of 0 bytes is stored inline.

そこで、mysql-server.cnf ( my.cnf )を少々、設定変更し、対応。

以下のように、innodb_strict_mode と innodb_file_per_table を追加。

$ sudo vi /etc/my.cnf.d/mysql-server.cnf

[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
log-error=/var/log/mysql/mysqld.log
pid-file=/run/mysqld/mysqld.pid

innodb_strict_mode=0
innodb_file_per_table = 1

その後、mysql serverを再起動し、設定反映。

$ sudo systemctl start mysqld

が、そもそも、db tableの設計が悪い。 今回、とりあえず、インポートできる状態にしましたが、 今後、利用中にレコードが正しく登録されない等の問題が発覚するかも。

その他、インターネットで検索すると、 「innodb_file_format = Barracuda」を「mysql-server.cnf」へ追加した例もありましたが、 今回、利用した mysql8では、以下のエラーにより、起動できませんでした。

$ sudo tail -f /var/log/mysql/mysqld.log
2021-04-17T07:57:06.897406Z 0 [ERROR] [MY-000067] [Server] unknown variable 'innodb_file_format=Barracuda'.
2021-04-17T07:57:06.898027Z 0 [ERROR] [MY-010119] [Server] Aborting

eclipse の pom.xml for java 管理画面で「インデックスのダウンロードが使用不可です」エラー

単に eclipse の設定が不足していますので、以下の手順で対応できます。

step 0

初期状態で、pom.xml の編集画面で「依存関係」→「追加」を選択すると、 「インデックスのダウンロードが使用不可です」エラーが表示されます。

f:id:end0tknr:20210415063115p:plain

f:id:end0tknr:20210415063130p:plain

step 1

eclipseの「設定」→「Maven」画面を開き、 「起動時にリポジトリー・インデックス更新をダウンロード」を選択します。

f:id:end0tknr:20210415063153p:plain

選択後、eclipseを再起動すると、mavenリポジトリのインデックスがダウンロードされます。 (初回ダウンロードは、10-20分程度、要します)

f:id:end0tknr:20210415063224p:plain

step 3

改めて、 初期状態で、pom.xml の編集画面で「依存関係」→「追加」を選択すると、 検索できることを確認できます。

f:id:end0tknr:20210415063234p:plain