반응형

스프링 EL(Expression Language) 란 객체 그래프를 조회하고 조작하는 기능을 제공하는 언어를 말한다.

spEL은 모든 스프링 프로젝트에서 사용하는 expression language로 만들었다.

문법이나 규칙은 배우기가 쉽다.

  • #{"표현식"}

  • ${"프로퍼티"}

  • 이런식으로 특정 객체를 가져와서 문자열처럼 사용할 수 있고, 계산도 할 수 있다. 표현식은 프로퍼티를 포함할 수 있지만, 반대로는 불가능하다.

사용처

  • @Value 애노테이션 안에 spEL을 쓰면, 아래 필드값에 결과가 주입된다.
  • 스프링 시큐리티의 경우 메소드 시큐리티, @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter, XML 인터셉터 URL 설정 등에 사용된다.
  • 스프렝 데이터에서 @Query 에 사용된다.
  • 화면을 만드는 템플릿엔진인 타임리프(thymeleaf) 등에서도 사용된다.

예시

@Value("#{1 + 1}")
int value;
 
@Value("#{'안녕 ' + 'red'}")
String greeting;

@Value("#{1 eq 1}")
boolean yn;

@Value("red")
String red;

// application.properties (프로퍼티) 에서 blue.value 변수를 가져온다.
@Value("${blue.value}")
int blueValue;

// Sample 객체의 data 필드 값을 가져온다.
@Value("#{sample.data}")
int sampleData;

 

사용 종류

Default Value

@Value("${car.type:Sedan}")
private String type; // yml 에 값이 없으면 Sedan 로 default value 설정

System Variables

@Value("${user.name}")
// Or
@Value("${username}")
private String userName;

@Value("${number.of.processors}")
// Or
@Value("${number_of_processors}")
private int numberOfProcessors;

@Value("${java.home}")
private String java;

 

Parameter Method Value

@Value("${car.brand}")
public CarData setCarData(@Value("${car.color}") String color, String brand) {
    carData.setCarColor(color);
    carData.setCarBrand(brand);
}

systemProperties

@Value("#{systemProperties['user.name']}")
private String userName;
@Value("#{systemProperties}")
private Map<String, String> properties;
public Driver(@Value("#{systemProperties['user.name']}") String name, String location) {
    this.name = name;
    this.location = location;
}

Injecting into Maps

student.hobbies={indoor: 'reading, drawing', outdoor: 'fishing, hiking, bushcraft'}
@Value("#{${student.hobbies}}")
private Map<String, List<String>> hobbies;
반응형
반응형

스프링에서 설정파일 값을 외부에 노출하고 싶지 않을떄, Jasypt 를 사용하면 된다.

 

라이브러리

spring boot starter 용

3.0.3 이 작성기준 2021년 1월 28일 기준 최신버젼이다.

3,0.3 이 출시된 날짜는 2020년 5월 31일 이다.

<dependency>
	<groupId>com.github.ulisesbocchio</groupId>
	<artifactId>jasypt-spring-boot-starter</artifactId>
	<version>3.0.3</version>
</dependency>

위와 같이 라이브러리를 추가하면

@SpringBootApplication or @EnableAutoConfiguration 어노테이션을 메인에 추가해 주어야 한다.

이렇게 해주면 환경설정 파일 command line argument, application.properties, yaml properties 들을 암호화 할 수 있다.

 

@SpringBootApplication or @EnableAutoConfiguration 을 추가해주지 않으면 pom.xml 과 다른 어노테이션을 추가해주어야 한다.

<dependency>
        <groupId>com.github.ulisesbocchio</groupId>
        <artifactId>jasypt-spring-boot</artifactId>
        <version>3.0.3</version>
</dependency>
@Configuration
@EncryptablePropertySources({@EncryptablePropertySource("classpath:encrypted.properties"),
							@EncryptablePropertySource("classpath:encrypted2.properties")})
public class MyApplication {
...
}

위 작업이 필요하다. 그래서 그냥 @SpringBootApplication or @EnableAutoConfiguration 어노테이션 을 추가하자.

 

 

속성값에 대한 정의

Key Required Default Value
jasypt.encryptor.password True -
jasypt.encryptor.algorithm False PBEWITHHMACSHA512ANDAES_256
jasypt.encryptor.key-obtention-iterations False 1000
jasypt.encryptor.pool-size False 1
jasypt.encryptor.provider-name False SunJCE
jasypt.encryptor.provider-class-name False null
jasypt.encryptor.salt-generator-classname False org.jasypt.salt.RandomSaltGenerator
jasypt.encryptor.iv-generator-classname False org.jasypt.iv.RandomIvGenerator
jasypt.encryptor.string-output-type False base64
jasypt.encryptor.proxy-property-sources False false
jasypt.encryptor.skip-property-sources False empty list

 

configuration 설정파일

@Configuration
public class JasyptConfig {

    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor() {
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword("password");
        config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
        config.setKeyObtentionIterations("1000");
        config.setPoolSize("1");
        config.setProviderName("SunJCE");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        return encryptor;
    }
}

password 를 제외하고 기본 값들로 작성되어 있다.

 

 

application.yml 설정 추가

jasypt:
  encryptor:
    bean: jasyptStringEncrptor

설정으로 등록한 빈의 명을 명시해줘야 한다.

 

암호화 및 복호화 예시

위에서 암호화를 위한 모든 작업은 끝났다. 아래와 같이 설정파일의 값을 암호화 복호화 해가면서 사용하면 된다.

@SpringBootApplication
public class Application implements CommandLineRunner {

	public static void main(String [] args) {
        SpringApplication.run(Application.class, args);
        System.out.println("=========== Server Start ===========");
	}
	
	@Override
   public void run(String... args) throws Exception {
		StandardPBEStringEncryptor pbeEnc = new StandardPBEStringEncryptor();
		pbeEnc.setAlgorithm("PBEWithMD5AndDES");
		pbeEnc.setPassword("test"); //2번 설정의 암호화 키를 입력
		
		String enc = pbeEnc.encrypt("1234"); //암호화 할 내용
		System.out.println("enc = " + enc); //암호화 한 내용을 출력
		
		//테스트용 복호화
		String des = pbeEnc.decrypt(enc);
		System.out.println("des = " + des);
   }
}

 

application.yml 사용예시

datasource:
  url: ENC(인크립트된dburl)
  username: ENC(인크립트된유저명)
  password: ENC(인크립트된패스워드)

 

 

더 많은 사용방법 및 기능들이 아래 참고 주소에 있어서 제대로 사용할거면 아래 문서를 참고하자

https://github.com/ulisesbocchio/jasypt-spring-boot [깃허브 jasypt-spring-boot 업데이트 문서]
반응형
반응형

spring boot 2 와 이전 버젼의 차이점

 

1. Java 8 이 최소 버젼이다.

   java9 를 지원하는 최초의 버젼이다.

2. tomcat 8,5 가 최소버젼이다.

3. Hibernate 5.2 가 최소 버젼이다.

4. Gradle 3.4 가 최소 버젼이다.

5. Spring Security 구성이 더 쉬워지고 Spring Security Oauth2가 Spring Security에 합쳐졌다. 보안 자동 구성은 더 이상 옵션을 노출하지 않고 가능한 한 Spring Security 기본값을 사용한다.  -Spring Security5

사용자가 한 곳에서 명시적으로 환경설정을 할 수 있다. 이런 게 WebSecurityConfigurerAdapter 의 순서 문제를 막을 수 있다.

예를 들어 Actuator 와 앱의 보안 문제를 커스텀 할 수 있게 해준다.

http.authorizeRequests()
  .requestMatchers(EndpointRequest.to("health"))
    .permitAll() // Actuator rules per endpoint
  .requestMatchers(EndpointRequest.toAnyEndpoint())
    .hasRole("admin") // Actuator general rules
  .requestMatchers(PathRequest.toStaticResources().atCommonLocations()) 
    .permitAll() // Static resource security 
  .antMatchers("/**") 
    .hasRole("user") // Application security rules 

6. 리액티브를 지원한다.

여러 리액티브 모듈의 스타터를 제공한다. 예를 들어 WebFlux 와 MongoDB 와 Cassandra 또는 Redis.

7. 커넥션 풀이 HikariCP 로 설정된다.

 

8. Spring Boot 2.X에서 많은 구성 속성의 이름이 변경 / 제거되었으며 개발자는 그에 따라 application.properties/application.yml을 업데이트해야한다.

9. Mockito 1.x는 더 이상 지원되지 않는다. spring-boot-starter-test를 사용하여 종속성을 관리하지 않는 경우 Mockito 2.x로 업그레이드해야한다.

10. Spring-boot-starter-data-redis를 사용할 때 Redis 드라이버로 Jedis 대신 Letuce가 사용된다.

11. Elasticsearch가 5.4 이상으로 업그레이드되었다.

 

반응형
반응형

스프링 부트에서 카프카 클라이언트 라이브러리를 추가하면, 이런오류가 생긴다.

Error registering AppInfo mbean

해당오류는 카프카 컨슈머 측에서만 발생한다.

javax.management.InstanceAlreadyExistsException: kafka.consumer:type=app-info,id=clientid-0

    at com.sun.jmx.mbeanserver.Repository.addMBean(Repository.java:437)

    at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.registerWithRepository(DefaultMBeanServerInterceptor.java:1898)

    at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.registerDynamicMBean(DefaultMBeanServerInterceptor.java:966)

    at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.registerObject(DefaultMBeanServerInterceptor.java:900)

    at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.registerMBean(DefaultMBeanServerInterceptor.java:324)

    at com.sun.jmx.mbeanserver.JmxMBeanServer.registerMBean(JmxMBeanServer.java:522)

    at org.apache.kafka.common.utils.AppInfoParser.registerAppInfo(AppInfoParser.java:64)

    at org.apache.kafka.clients.consumer.KafkaConsumer.<init>(KafkaConsumer.java:816)

    at org.apache.kafka.clients.consumer.KafkaConsumer.<init>(KafkaConsumer.java:631)

    at org.springframework.kafka.core.DefaultKafkaConsumerFactory.createRawConsumer(DefaultKafkaConsumerFactory.java:340)

    at org.springframework.kafka.core.DefaultKafkaConsumerFactory.createKafkaConsumer(DefaultKafkaConsumerFactory.java:308)

    at org.springframework.kafka.core.DefaultKafkaConsumerFactory.createConsumerWithAdjustedProperties(DefaultKafkaConsumerFactory.java:293)

    at org.springframework.kafka.core.DefaultKafkaConsumerFactory.createKafkaConsumer(DefaultKafkaConsumerFactory.java:267)

    at org.springframework.kafka.core.DefaultKafkaConsumerFactory.createConsumer(DefaultKafkaConsumerFactory.java:241)

    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.<init>(KafkaMessageListenerContainer.java:606)

    at org.springframework.kafka.listener.KafkaMessageListenerContainer.doStart(KafkaMessageListenerContainer.java:302)

    at org.springframework.kafka.listener.AbstractMessageListenerContainer.start(AbstractMessageListenerContainer.java:338)

    at org.springframework.kafka.listener.ConcurrentMessageListenerContainer.doStart(ConcurrentMessageListenerContainer.java:204)

    at org.springframework.kafka.listener.AbstractMessageListenerContainer.start(AbstractMessageListenerContainer.java:338)

    at org.springframework.kafka.config.KafkaListenerEndpointRegistry.startIfNecessary(KafkaListenerEndpointRegistry.java:312)

    at org.springframework.kafka.config.KafkaListenerEndpointRegistry.start(KafkaListenerEndpointRegistry.java:257)

    at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:182)

    at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:53)

    at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:360)

    at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:158)

    at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:122)

    at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:895)

    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:554)

    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)

    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)

    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)

    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)

    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)

    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)

    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)

    at kr.co.lunasoft.productdb.ProductDbBotApplication.main(ProductDbBotApplication.java:14)

이런 경우는 clientId 등록의 문제다.

이대로 어플리케이션을 올려도 상관은 없다.

하지만 오류가 뜨는걸 별로 보고싶지는 않기 떄문에 안뜨게 해주려면 카프카 리슨을 하고 있는 각각의 컨슈머에 client id 를 명시해줘야 한다.

카프카 토픽이 여러개 있을텐데 모두 같은 clientid 를 사용할때 다음과 같은 오류로그가 발생한다.

 

아래 코드와 같이 clientIdPrefix 로 해결

카프카컨슈머 - KafkaListener

    @KafkaListener(topics = {"topic-test"}, containerFactory = "KafkaListenerContainerFactory", clientIdPrefix = "test-topic-client")
    public void consumer(ConsumerRecord<String, Object> consumerRecord) {
        log.info("카프카 컨슈머")
    }

카프카컨테이너 팩토리 - ConsumerFactory

@Slf4j
@Configuration
public class KafkaConsumerConfig {

    @Value("#{'${spring.kafka.bootstrap-servers}'.split(',')}")
    List<String> bootstrapAddress;
    @Value("${spring.kafka.consumer.group-id}")
    String groupId;
    @Value("${spring.kafka.consumer.auto-offset-reset}")
    String autoOffsetReset;

    @Bean
    public ConsumerFactory<String, String> consumerFactory() {

        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        return new DefaultKafkaConsumerFactory<>(props);
    }

    @Bean("kafkaListenerContainerFactory")
    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }

}

참고로 카프카 컨테이너 를 위와같이 구성해서 쓴다.

또한 스프링부트 2.3.# 버젼 코드이다.

반응형
반응형

스프링부트 2.3.3 을 쓰게되면서 부터 라이브러리 사용법들이 조금 많이 달라졌다.

예를 들어 몽고db 가 있다.

spring-data-mongodb:3.0.0 이후의 버젼이다.

 

몽고 db 쿼리 사용 예시이다.

Date dateLogStartDate = new Date(); // date 조건검색 시작
Date dateLogEndDate = new Date(); // date 조건검색 종료

Query query = new Query();
query.with(Sort.by(Sort.Direction.DESC, "log_date")); // date 내림차순
Criteria criteria = Criteria
        .where("log_date").gte(dateLogStartDate).lte(dateLogEndDate);

query.addCriteria(criteria);
Map<String, Object> result = new HashMap<String, Object>();
List<Test> mongo = null;
long count = mongoTemplate.count(query, Test.class);
log.info( "count : {}", count);
int perPage = 50;
int currentPage = 1;
if(count > 0) {
    query.with(Sort.by(Sort.Direction.DESC, "log_date")); // date 내림차순
    query.limit(perPage);
    query.skip(perPage * (currentPage - 1) );
    mongo = mongoTemplate.find(query, Test.class);
}

document 클래스

@Document(collection="test")
public class Test {

	private Date log_date;

	private String data;

	private String text;

}

spring-data-mongodb:3.0.0 이전 버젼에서는 어노테이션을 붙이지 않아도 동작을 했다.

 

하지만 스프링부트 2.3.0 이후버젼에서 사용하는 spring-data-mongodb:3.0.0 버젼이상은 

@Field 어노테이션을 꼭 붙여줘야 한다.

@Document(collection="test")
public class Test {

	@Field("log_date")
	private Date log_date;

	@Field("data")
	private String data;

	@Field("text")
	private String text;

}

 

@Field 을 붙여주지 않고 쿼리를 조회시 다음과 같은 오류가 생긴다.

org.springframework.data.mapping.PropertyReferenceException: No property log found for type Test!

 

반응형
반응형

프론트에서 post 로 데이터 객체를 보내게 되면 

서버에서 RequestBody 로 객체를 받아야 하는 경우가 있다.

 

이때 받게 되는 데이터를 vo로 만들어서 받게 된다.

필드명을 프론트에서 보내는 객체명과 다르게 쓰고 싶은 경우 필드명을 어노테이션으로 받아야 한다.

 

같게 하려면 프론트필드명과 스프링객체 필드명을 같게만 하면 된다.

안되는 경우는 예를 들어 프론트 json 필드명에 . dot 가 들어간 경우는 아래 @JsonProperty 를 사용해야 한다.

 

 

프론트에서는 다음과 같이 post 로 데이터를 전송시

{
  "relationship.name": "someting"
}

서버에서는 프론트 필드명과 서버받는 객체의 필드명을 달리 하려면 아래와 같이

@JsonProperty 를 추가하면 된다.

public class Request {

    @JsonProperty("relationship.name")
    private String relationshipName;

    ...
}

물론 이때 컨트롤러는 @RequestBody 로 받을 경우이다.

public ResponseEntity<String> test(@RequestBody Request request) throws Exception {
반응형
반응형

JPA 를 사용하면서 DB 에 어떤 특정값이 들어오면 변환해줘야 할 떄가 있다.

예를 들어 empty string 을 null 로 넣어줘야 할떄다. (mysql)

 

빈스트링이 "" 가 들어올때 null 로 바꿔서 db 에 넣고 싶을때 @Convert 가 필요하다, 

이 외에도 어떤 고정된 값들은 특정 값으로 변환해서 db 에 넣어줄떄 이 Convert 가 필요하다.

 

 

구현 방법은 변경하고자 하는 컬럼명 위에 @Convert 적고, 커스텀한 Class 명을 기입한다.

@Table(name = "db1.test")
public class Test {   

    ...

    @Convert(converter = EmptyStringToNullConverter.class)
    @Column(name = "test_data")
    private String test_data;
    
    ...
}

커스텀할 Classs 에는 AttributeConverter 를 구현한다.

이름 그대로 디비 넣기 전에 변환해주는 메소드명은  convertToDatabaseColumn 에서 구현한다.

반대로 db 에서 꺼내서 vo,entity 에서 사용할떄는 convertToEntityAttribute 를 사용해서 다시 변환해 준다.

@Converter(autoApply = true)
public class EmptyStringToNullConverter implements AttributeConverter<String, String> {

    @Override
    public String convertToDatabaseColumn(String string) {
        // Use defaultIfEmpty to preserve Strings consisting only of whitespaces
        return "".equals(string) ? null : string;
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        //If you want to keep it null otherwise transform to empty String
        return dbData;
    }
}

 

 

참고문헌

https://www.baeldung.com/jpa-attribute-converters [jpa 컨버터]

 

반응형
반응형

spring kill 시키기

SIGTERM 과 SIGKILL

주의해야할 것은 "정상(?) 종료" 되었을 때에 호출된다는 것이다.
무슨 말이냐면 애플리케이션이 종료될 때 일반적인 인터럽트는 SIGTERM 이라는 인터럽트다.
이 인터럽트(SIGTERM)가 발생하면 이벤트로 감지하고 수행하는 작업이라는 것이다.
SIGTERM을 정상적인 종료라고 봤을 때, 비정상 종료는 SIGKILL 이다.
리눅스에서 kill -9 옵션과 같이 강제적으로 꺼버리는 것과 윈도우에서 작업관리자가 작업을 끝내버리는 등의 인터럽트가 SIGKILL이다.
위의 예제를 따라했는데 종료 이벤트에 대한 메서드가 호출되지 않았다면 SIGKILL을 이용해서 종료했을 가능성이 높다.
혹시나하고 윈도우 환경에서 커맨드창에 ctrl + c 로 종료해보았는데 이 단축키는 SIGTERM을 발생하는 이벤트라서 온전히 종료되는 것을 아래 그림에서 볼 수 있다.

kill 시킬시

kill -9 는 급 종료
kill -15 는 현재 프로세스 다 실행시키고 종료 

 

권장 종료 방법

$kill -term 프로세스ID

kill -term 이 -15 와 동일하다

 

$ kill -l

1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR

 

 

참고문헌

https://jeong-pro.tistory.com/179 [SIGTERM 과 SIGKILL 및 스프링종료시 생명주기]
https://heowc.dev/2018/12/27/spring-boot-graceful-shutdown/ [스프링boot 안전하게 종료하기]
https://www.baeldung.com/spring-boot-shutdown [스프링 boot 종료 공식 문서]
www.lesstif.com/system-admin/unix-linux-kill-12943674.html [kill 종료 명령어 코드 및 방법]

 

반응형

+ Recent posts