├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── src ├── main │ ├── resources │ │ ├── application-local.yml │ │ ├── application-alpha.yml │ │ ├── application.yml │ │ └── templates │ │ │ └── chat │ │ │ ├── room.ftl │ │ │ └── roomdetail.ftl │ └── java │ │ └── com │ │ └── websocket │ │ └── chat │ │ ├── controller │ │ ├── IndexController.java │ │ ├── ChatController.java │ │ └── ChatRoomController.java │ │ ├── model │ │ ├── LoginInfo.java │ │ ├── ChatRoom.java │ │ └── ChatMessage.java │ │ ├── WebsocketchatApplication.java │ │ ├── config │ │ ├── EmbeddedRedisConfig.java │ │ ├── WebSockConfig.java │ │ ├── WebSecurityConfig.java │ │ ├── RedisConfig.java │ │ └── handler │ │ │ └── StompHandler.java │ │ ├── pubsub │ │ └── RedisSubscriber.java │ │ ├── service │ │ ├── ChatService.java │ │ └── JwtTokenProvider.java │ │ └── repo │ │ └── ChatRoomRepository.java └── test │ └── java │ └── com │ └── websocket │ └── chat │ └── service │ └── JwtTokenProviderTest.java ├── .gitignore ├── gradlew.bat ├── README.md └── gradlew /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codej99/websocket-chat-server/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | rootProject.name = 'chat' 7 | -------------------------------------------------------------------------------- /src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | redis: 5 | host: localhost 6 | port: 6379 -------------------------------------------------------------------------------- /src/main/resources/application-alpha.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | redis: 5 | host: Standalone Redis 호스트 6 | port: Standalone Redis 포트 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 29 13:47:27 KST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | devtools: 5 | livereload: 6 | enabled: true 7 | restart: 8 | enabled: false 9 | add-properties: false 10 | freemarker: 11 | cache: false 12 | jwt: 13 | secret: govlepel@$& -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class IndexController { 8 | @GetMapping({"","/index"}) 9 | public String index() { 10 | return "redirect:/chat/room"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/model/LoginInfo.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class LoginInfo { 8 | private String name; 9 | private String token; 10 | 11 | @Builder 12 | public LoginInfo(String name, String token) { 13 | this.name = name; 14 | this.token = token; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/WebsocketchatApplication.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class WebsocketchatApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(WebsocketchatApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | /build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | /out/ 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | 29 | ### VS Code ### 30 | .vscode/ 31 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/model/ChatRoom.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.io.Serializable; 7 | import java.util.UUID; 8 | 9 | @Getter 10 | @Setter 11 | public class ChatRoom implements Serializable { 12 | 13 | private static final long serialVersionUID = 6494678977089006639L; 14 | 15 | private String roomId; 16 | private String name; 17 | private long userCount; // 채팅방 인원수 18 | 19 | public static ChatRoom create(String name) { 20 | ChatRoom chatRoom = new ChatRoom(); 21 | chatRoom.roomId = UUID.randomUUID().toString(); 22 | chatRoom.name = name; 23 | return chatRoom; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/model/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class ChatMessage { 10 | 11 | public ChatMessage() { 12 | } 13 | 14 | @Builder 15 | public ChatMessage(MessageType type, String roomId, String sender, String message, long userCount) { 16 | this.type = type; 17 | this.roomId = roomId; 18 | this.sender = sender; 19 | this.message = message; 20 | this.userCount = userCount; 21 | } 22 | 23 | // 메시지 타입 : 입장, 퇴장, 채팅 24 | public enum MessageType { 25 | ENTER, QUIT, TALK 26 | } 27 | 28 | private MessageType type; // 메시지 타입 29 | private String roomId; // 방번호 30 | private String sender; // 메시지 보낸사람 31 | private String message; // 메시지 32 | private long userCount; // 채팅방 인원수, 채팅방 내에서 메시지가 전달될때 인원수 갱신시 사용 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/config/EmbeddedRedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Profile; 6 | import redis.embedded.RedisServer; 7 | 8 | import javax.annotation.PostConstruct; 9 | import javax.annotation.PreDestroy; 10 | 11 | /** 12 | * 로컬 환경일경우 내장 레디스가 실행된다. 13 | */ 14 | @Profile("local") 15 | @Configuration 16 | public class EmbeddedRedisConfig { 17 | 18 | @Value("${spring.redis.port}") 19 | private int redisPort; 20 | 21 | private RedisServer redisServer; 22 | 23 | @PostConstruct 24 | public void redisServer() { 25 | redisServer = new RedisServer(redisPort); 26 | redisServer.start(); 27 | } 28 | 29 | @PreDestroy 30 | public void stopRedis() { 31 | if (redisServer != null) { 32 | redisServer.stop(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/pubsub/RedisSubscriber.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.pubsub; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.websocket.chat.model.ChatMessage; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.messaging.simp.SimpMessageSendingOperations; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Slf4j 11 | @RequiredArgsConstructor 12 | @Service 13 | public class RedisSubscriber { 14 | 15 | private final ObjectMapper objectMapper; 16 | private final SimpMessageSendingOperations messagingTemplate; 17 | 18 | /** 19 | * Redis에서 메시지가 발행(publish)되면 대기하고 있던 Redis Subscriber가 해당 메시지를 받아 처리한다. 20 | */ 21 | public void sendMessage(String publishMessage) { 22 | try { 23 | // ChatMessage 객채로 맵핑 24 | ChatMessage chatMessage = objectMapper.readValue(publishMessage, ChatMessage.class); 25 | // 채팅방을 구독한 클라이언트에게 메시지 발송 26 | messagingTemplate.convertAndSend("/sub/chat/room/" + chatMessage.getRoomId(), chatMessage); 27 | } catch (Exception e) { 28 | log.error("Exception {}", e); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/websocket/chat/service/JwtTokenProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.service; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.junit4.SpringRunner; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertNotNull; 11 | 12 | @RunWith(SpringRunner.class) 13 | @SpringBootTest 14 | public class JwtTokenProviderTest { 15 | 16 | @Autowired 17 | private JwtTokenProvider jwtTokenProvider; 18 | 19 | // @Test 20 | // public void createAndValidToken() { 21 | // String userId = "happy"; 22 | // String nickname = "아이유"; 23 | // String jwt = jwtTokenProvider.generateToken(nickname); 24 | // assertNotNull(jwt); 25 | // String decToken = jwtTokenProvider.getUserNameFromJwt(jwt); 26 | // assertEquals(nickname, decToken); 27 | // } 28 | 29 | @Test 30 | public void createAndValidToken() { 31 | String id = "/sub/chat/room/3f0f893a-5849-4028-9755-8c6c8ab1846b"; 32 | int lastIndex = id.lastIndexOf("/"); 33 | if(lastIndex != -1) 34 | id = id.substring(lastIndex+1); 35 | System.out.println(id); 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/controller/ChatController.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.controller; 2 | 3 | import com.websocket.chat.model.ChatMessage; 4 | import com.websocket.chat.repo.ChatRoomRepository; 5 | import com.websocket.chat.service.ChatService; 6 | import com.websocket.chat.service.JwtTokenProvider; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.messaging.handler.annotation.Header; 10 | import org.springframework.messaging.handler.annotation.MessageMapping; 11 | import org.springframework.stereotype.Controller; 12 | 13 | @Slf4j 14 | @RequiredArgsConstructor 15 | @Controller 16 | public class ChatController { 17 | 18 | private final JwtTokenProvider jwtTokenProvider; 19 | private final ChatRoomRepository chatRoomRepository; 20 | private final ChatService chatService; 21 | 22 | /** 23 | * websocket "/pub/chat/message"로 들어오는 메시징을 처리한다. 24 | */ 25 | @MessageMapping("/chat/message") 26 | public void message(ChatMessage message, @Header("token") String token) { 27 | String nickname = jwtTokenProvider.getUserNameFromJwt(token); 28 | // 로그인 회원 정보로 대화명 설정 29 | message.setSender(nickname); 30 | // 채팅방 인원수 세팅 31 | message.setUserCount(chatRoomRepository.getUserCount(message.getRoomId())); 32 | // Websocket에 발행된 메시지를 redis로 발행(publish) 33 | chatService.sendChatMessage(message); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/config/WebSockConfig.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.config; 2 | 3 | import com.websocket.chat.config.handler.StompHandler; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.messaging.simp.config.ChannelRegistration; 7 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 8 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 9 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 10 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 11 | 12 | @RequiredArgsConstructor 13 | @Configuration 14 | @EnableWebSocketMessageBroker 15 | public class WebSockConfig implements WebSocketMessageBrokerConfigurer { 16 | 17 | private final StompHandler stompHandler; 18 | 19 | @Override 20 | public void configureMessageBroker(MessageBrokerRegistry config) { 21 | config.enableSimpleBroker("/sub"); 22 | config.setApplicationDestinationPrefixes("/pub"); 23 | } 24 | 25 | @Override 26 | public void registerStompEndpoints(StompEndpointRegistry registry) { 27 | registry.addEndpoint("/ws-stomp").setAllowedOrigins("*") 28 | .withSockJS(); // sock.js를 통하여 낮은 버전의 브라우저에서도 websocket이 동작할수 있게 합니다. 29 | } 30 | 31 | @Override 32 | public void configureClientInboundChannel(ChannelRegistration registration) { 33 | registration.interceptors(stompHandler); 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/service/ChatService.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.service; 2 | 3 | import com.websocket.chat.model.ChatMessage; 4 | import com.websocket.chat.repo.ChatRoomRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.listener.ChannelTopic; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class ChatService { 13 | 14 | private final ChannelTopic channelTopic; 15 | private final RedisTemplate redisTemplate; 16 | private final ChatRoomRepository chatRoomRepository; 17 | 18 | /** 19 | * destination정보에서 roomId 추출 20 | */ 21 | public String getRoomId(String destination) { 22 | int lastIndex = destination.lastIndexOf('/'); 23 | if (lastIndex != -1) 24 | return destination.substring(lastIndex + 1); 25 | else 26 | return ""; 27 | } 28 | 29 | /** 30 | * 채팅방에 메시지 발송 31 | */ 32 | public void sendChatMessage(ChatMessage chatMessage) { 33 | chatMessage.setUserCount(chatRoomRepository.getUserCount(chatMessage.getRoomId())); 34 | if (ChatMessage.MessageType.ENTER.equals(chatMessage.getType())) { 35 | chatMessage.setMessage(chatMessage.getSender() + "님이 방에 입장했습니다."); 36 | chatMessage.setSender("[알림]"); 37 | } else if (ChatMessage.MessageType.QUIT.equals(chatMessage.getType())) { 38 | chatMessage.setMessage(chatMessage.getSender() + "님이 방에서 나갔습니다."); 39 | chatMessage.setSender("[알림]"); 40 | } 41 | redisTemplate.convertAndSend(channelTopic.getTopic(), chatMessage); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 | 9 | /** 10 | * Web Security 설정 11 | */ 12 | @Configuration 13 | @EnableWebSecurity 14 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 15 | 16 | @Override 17 | protected void configure(HttpSecurity http) throws Exception { 18 | http 19 | .csrf().disable() // 기본값이 on인 csrf 취약점 보안을 해제한다. on으로 설정해도 되나 설정할경우 웹페이지에서 추가처리가 필요함. 20 | .headers() 21 | .frameOptions().sameOrigin() // SockJS는 기본적으로 HTML iframe 요소를 통한 전송을 허용하지 않도록 설정되는데 해당 내용을 해제한다. 22 | .and() 23 | .formLogin() // 권한없이 페이지 접근하면 로그인 페이지로 이동한다. 24 | .and() 25 | .authorizeRequests() 26 | .antMatchers("/chat/**").hasRole("USER") // chat으로 시작하는 리소스에 대한 접근 권한 설정 27 | .anyRequest().permitAll(); // 나머지 리소스에 대한 접근 설정 28 | } 29 | 30 | /** 31 | * 테스트를 위해 In-Memory에 계정을 임의로 생성한다. 32 | * 서비스에 사용시에는 DB데이터를 이용하도록 수정이 필요하다. 33 | */ 34 | @Override 35 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 36 | auth.inMemoryAuthentication() 37 | .withUser("happydaddy") 38 | .password("{noop}1234") 39 | .roles("USER") 40 | .and() 41 | .withUser("angrydaddy") 42 | .password("{noop}1234") 43 | .roles("USER") 44 | .and() 45 | .withUser("guest") 46 | .password("{noop}1234") 47 | .roles("GUEST"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/service/JwtTokenProvider.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.service; 2 | 3 | import io.jsonwebtoken.*; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Date; 9 | 10 | @Slf4j 11 | @Component 12 | public class JwtTokenProvider { 13 | 14 | @Value("${spring.jwt.secret}") 15 | private String secretKey; 16 | 17 | private long tokenValidMilisecond = 1000L * 60 * 60; // 1시간만 토큰 유효 18 | 19 | /** 20 | * 이름으로 Jwt Token을 생성한다. 21 | */ 22 | public String generateToken(String name) { 23 | Date now = new Date(); 24 | return Jwts.builder() 25 | .setId(name) 26 | .setIssuedAt(now) // 토큰 발행일자 27 | .setExpiration(new Date(now.getTime() + tokenValidMilisecond)) // 유효시간 설정 28 | .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘, secret값 세팅 29 | .compact(); 30 | } 31 | 32 | /** 33 | * Jwt Token을 복호화 하여 이름을 얻는다. 34 | */ 35 | public String getUserNameFromJwt(String jwt) { 36 | return getClaims(jwt).getBody().getId(); 37 | } 38 | 39 | /** 40 | * Jwt Token의 유효성을 체크한다. 41 | */ 42 | public boolean validateToken(String jwt) { 43 | return this.getClaims(jwt) != null; 44 | } 45 | 46 | private Jws getClaims(String jwt) { 47 | try { 48 | return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt); 49 | } catch (SignatureException ex) { 50 | log.error("Invalid JWT signature"); 51 | throw ex; 52 | } catch (MalformedJwtException ex) { 53 | log.error("Invalid JWT token"); 54 | throw ex; 55 | } catch (ExpiredJwtException ex) { 56 | log.error("Expired JWT token"); 57 | throw ex; 58 | } catch (UnsupportedJwtException ex) { 59 | log.error("Unsupported JWT token"); 60 | throw ex; 61 | } catch (IllegalArgumentException ex) { 62 | log.error("JWT claims string is empty."); 63 | throw ex; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/controller/ChatRoomController.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.controller; 2 | 3 | import com.websocket.chat.model.ChatRoom; 4 | import com.websocket.chat.model.LoginInfo; 5 | import com.websocket.chat.repo.ChatRoomRepository; 6 | import com.websocket.chat.service.JwtTokenProvider; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import java.util.List; 15 | 16 | @RequiredArgsConstructor 17 | @Controller 18 | @RequestMapping("/chat") 19 | public class ChatRoomController { 20 | 21 | private final ChatRoomRepository chatRoomRepository; 22 | private final JwtTokenProvider jwtTokenProvider; 23 | 24 | @GetMapping("/room") 25 | public String rooms() { 26 | return "/chat/room"; 27 | } 28 | 29 | @GetMapping("/rooms") 30 | @ResponseBody 31 | public List room() { 32 | List chatRooms = chatRoomRepository.findAllRoom(); 33 | chatRooms.stream().forEach(room -> room.setUserCount(chatRoomRepository.getUserCount(room.getRoomId()))); 34 | return chatRooms; 35 | } 36 | 37 | @PostMapping("/room") 38 | @ResponseBody 39 | public ChatRoom createRoom(@RequestParam String name) { 40 | return chatRoomRepository.createChatRoom(name); 41 | } 42 | 43 | @GetMapping("/room/enter/{roomId}") 44 | public String roomDetail(Model model, @PathVariable String roomId) { 45 | model.addAttribute("roomId", roomId); 46 | return "/chat/roomdetail"; 47 | } 48 | 49 | @GetMapping("/room/{roomId}") 50 | @ResponseBody 51 | public ChatRoom roomInfo(@PathVariable String roomId) { 52 | return chatRoomRepository.findRoomById(roomId); 53 | } 54 | 55 | @GetMapping("/user") 56 | @ResponseBody 57 | public LoginInfo getUserInfo() { 58 | Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 59 | String name = auth.getName(); 60 | return LoginInfo.builder().name(name).token(jwtTokenProvider.generateToken(name)).build(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.config; 2 | 3 | import com.websocket.chat.pubsub.RedisSubscriber; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.redis.connection.RedisConnectionFactory; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | import org.springframework.data.redis.listener.ChannelTopic; 10 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; 11 | import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; 12 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 13 | import org.springframework.data.redis.serializer.StringRedisSerializer; 14 | 15 | @RequiredArgsConstructor 16 | @Configuration 17 | public class RedisConfig { 18 | 19 | /** 20 | * 단일 Topic 사용을 위한 Bean 설정 21 | */ 22 | @Bean 23 | public ChannelTopic channelTopic() { 24 | return new ChannelTopic("chatroom"); 25 | } 26 | 27 | /** 28 | * redis에 발행(publish)된 메시지 처리를 위한 리스너 설정 29 | */ 30 | @Bean 31 | public RedisMessageListenerContainer redisMessageListener(RedisConnectionFactory connectionFactory, 32 | MessageListenerAdapter listenerAdapter, 33 | ChannelTopic channelTopic) { 34 | RedisMessageListenerContainer container = new RedisMessageListenerContainer(); 35 | container.setConnectionFactory(connectionFactory); 36 | container.addMessageListener(listenerAdapter, channelTopic); 37 | return container; 38 | } 39 | 40 | /** 41 | * 실제 메시지를 처리하는 subscriber 설정 추가 42 | */ 43 | @Bean 44 | public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) { 45 | return new MessageListenerAdapter(subscriber, "sendMessage"); 46 | } 47 | 48 | /** 49 | * 어플리케이션에서 사용할 redisTemplate 설정 50 | */ 51 | @Bean 52 | public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { 53 | RedisTemplate redisTemplate = new RedisTemplate<>(); 54 | redisTemplate.setConnectionFactory(connectionFactory); 55 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 56 | redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class)); 57 | return redisTemplate; 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/repo/ChatRoomRepository.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.repo; 2 | 3 | import com.websocket.chat.model.ChatRoom; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.data.redis.core.HashOperations; 6 | import org.springframework.data.redis.core.ValueOperations; 7 | import org.springframework.stereotype.Service; 8 | 9 | import javax.annotation.Resource; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @RequiredArgsConstructor 14 | @Service 15 | public class ChatRoomRepository { 16 | // Redis CacheKeys 17 | private static final String CHAT_ROOMS = "CHAT_ROOM"; // 채팅룸 저장 18 | public static final String USER_COUNT = "USER_COUNT"; // 채팅룸에 입장한 클라이언트수 저장 19 | public static final String ENTER_INFO = "ENTER_INFO"; // 채팅룸에 입장한 클라이언트의 sessionId와 채팅룸 id를 맵핑한 정보 저장 20 | 21 | @Resource(name = "redisTemplate") 22 | private HashOperations hashOpsChatRoom; 23 | @Resource(name = "redisTemplate") 24 | private HashOperations hashOpsEnterInfo; 25 | @Resource(name = "redisTemplate") 26 | private ValueOperations valueOps; 27 | 28 | // 모든 채팅방 조회 29 | public List findAllRoom() { 30 | return hashOpsChatRoom.values(CHAT_ROOMS); 31 | } 32 | 33 | // 특정 채팅방 조회 34 | public ChatRoom findRoomById(String id) { 35 | return hashOpsChatRoom.get(CHAT_ROOMS, id); 36 | } 37 | 38 | // 채팅방 생성 : 서버간 채팅방 공유를 위해 redis hash에 저장한다. 39 | public ChatRoom createChatRoom(String name) { 40 | ChatRoom chatRoom = ChatRoom.create(name); 41 | hashOpsChatRoom.put(CHAT_ROOMS, chatRoom.getRoomId(), chatRoom); 42 | return chatRoom; 43 | } 44 | 45 | // 유저가 입장한 채팅방ID와 유저 세션ID 맵핑 정보 저장 46 | public void setUserEnterInfo(String sessionId, String roomId) { 47 | hashOpsEnterInfo.put(ENTER_INFO, sessionId, roomId); 48 | } 49 | 50 | // 유저 세션으로 입장해 있는 채팅방 ID 조회 51 | public String getUserEnterRoomId(String sessionId) { 52 | return hashOpsEnterInfo.get(ENTER_INFO, sessionId); 53 | } 54 | 55 | // 유저 세션정보와 맵핑된 채팅방ID 삭제 56 | public void removeUserEnterInfo(String sessionId) { 57 | hashOpsEnterInfo.delete(ENTER_INFO, sessionId); 58 | } 59 | 60 | // 채팅방 유저수 조회 61 | public long getUserCount(String roomId) { 62 | return Long.valueOf(Optional.ofNullable(valueOps.get(USER_COUNT + "_" + roomId)).orElse("0")); 63 | } 64 | 65 | // 채팅방에 입장한 유저수 +1 66 | public long plusUserCount(String roomId) { 67 | return Optional.ofNullable(valueOps.increment(USER_COUNT + "_" + roomId)).orElse(0L); 68 | } 69 | 70 | // 채팅방에 입장한 유저수 -1 71 | public long minusUserCount(String roomId) { 72 | return Optional.ofNullable(valueOps.decrement(USER_COUNT + "_" + roomId)).filter(count -> count > 0).orElse(0L); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Java_8](https://img.shields.io/badge/java-v1.8-red?logo=java) 2 | ![Spring_Boot](https://img.shields.io/badge/Spring_Boot-v2.1.5-green.svg?logo=spring) 3 | ![Spring_Boot_Websocket](https://img.shields.io/badge/Spring_Boot_Websocket-v2.1.5-green.svg?logo=spring) 4 | ![Spring_Security](https://img.shields.io/badge/Spring_Cloud_Security-v2.1.5-green.svg?logo=spring) 5 | ![Spring_Freemarker](https://img.shields.io/badge/Freemarker-v2.1.5-blue.svg) 6 | ![Vuejs](https://img.shields.io/badge/vue.js-v2.5.16-blue.svg?logo=vue.js) 7 | ![GitHub stars](https://img.shields.io/github/stars/codej99/websocket-chat-server?style=social) 8 | 9 | # SpringBoot + Websocket을 이용한 채팅 서버 구축 10 | 11 | ### 0. 개요 12 | - SpringBoot 환경을 기반으로 하여 Websocket을 이용한 채팅서버 구축 실습. 13 | - daddyprogrammer.org에서 연재 및 소스 Github 등록 14 | - https://daddyprogrammer.org/post/series/spring-websocket-chat-server/ 15 | 16 | ### 1. 실습 환경 17 | - Java 8~11 18 | - SpringBoot 2.x 19 | - Websocket 20 | - Stomp 21 | - Redis pub/sub 22 | - vue.js, freemarker, bootstrap 23 | - Intellij Community 24 | 25 | ### 2. 실습 내용 26 | - Spring websocket chatting server(1) – basic websocket server 27 | - Document 28 | - https://daddyprogrammer.org/post/4077/spring-websocket-chatting/ 29 | - Git 30 | - https://github.com/codej99/websocket-chat-server/tree/feature/basic-websocket-server 31 | - Spring websocket chatting server(2) – Stomp로 채팅서버 고도화하기 32 | - Document 33 | - https://daddyprogrammer.org/post/4691/spring-websocket-chatting-server-stomp-server/ 34 | - Git 35 | - https://github.com/codej99/websocket-chat-server/tree/feature/stomp 36 | 37 | - Spring websocket chatting server(3) - 여러대의 채팅서버간에 메시지 공유하기 by Redis pub/sub 38 | - Document 39 | - https://daddyprogrammer.org/post/4731/spring-websocket-chatting-server-redis-pub-sub/ 40 | - Git 41 | - https://github.com/codej99/websocket-chat-server/tree/feature/redis-pub-sub 42 | 43 | - Spring websocket chatting server(4) - SpringSecurity + Jwt를 적용하여 보안강화하기 44 | - Document 45 | - https://daddyprogrammer.org/post/5072/spring-websocket-chatting-server-spring-security-jwt/ 46 | - Git 47 | - https://github.com/codej99/websocket-chat-server/tree/feature/security 48 | 49 | - Spring websocket chatting server(5) – 채팅방 입장/퇴장 이벤트 처리, 인원수 표시 50 | - Document 51 | - https://daddyprogrammer.org/post/5290/spring-websocket-chatting-server-enter-qut-event-view-user-count/ 52 | - Git 53 | - https://github.com/codej99/websocket-chat-server/tree/feature/developchatroom 54 | - Spring websocket chatting server(6) – Nginx+Certbot 무료 SSL인증서로 WSS(Websocket Secure) 구축하기 55 | - Document 56 | - https://daddyprogrammer.org/post/5353/spring-websocket-chatting-server-ngix-certbot-ssl-websocket-secure/ 57 | 58 | ### 3. 기타 59 | - Websocket Client 60 | - 실습1에서 사용. 61 | - Simple websocket client 62 | - Chrome store : https://chrome.google.com/webstore/search/websocket?hl=ko 63 | - 채팅룸 화면 접속 64 | - 실습2,3에서 구현하는 채팅 웹뷰 접속 주소 65 | - http://localhost:8080/chat/room 66 | - SpringSecurity 아이디/비번 67 | - 실습4에서 사용 68 | - http://localhost:8080/chat/room 69 | - happydaddy/1234 : ROLE_USER 70 | - angrydaddy/1234 : ROLE_USER 71 | - guest/1234 : ROLE_GUEST -------------------------------------------------------------------------------- /src/main/java/com/websocket/chat/config/handler/StompHandler.java: -------------------------------------------------------------------------------- 1 | package com.websocket.chat.config.handler; 2 | 3 | import com.websocket.chat.model.ChatMessage; 4 | import com.websocket.chat.repo.ChatRoomRepository; 5 | import com.websocket.chat.service.ChatService; 6 | import com.websocket.chat.service.JwtTokenProvider; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.messaging.Message; 10 | import org.springframework.messaging.MessageChannel; 11 | import org.springframework.messaging.simp.stomp.StompCommand; 12 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor; 13 | import org.springframework.messaging.support.ChannelInterceptor; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.security.Principal; 17 | import java.util.Optional; 18 | 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | @Component 22 | public class StompHandler implements ChannelInterceptor { 23 | 24 | private final JwtTokenProvider jwtTokenProvider; 25 | private final ChatRoomRepository chatRoomRepository; 26 | private final ChatService chatService; 27 | 28 | // websocket을 통해 들어온 요청이 처리 되기전 실행된다. 29 | @Override 30 | public Message preSend(Message message, MessageChannel channel) { 31 | StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); 32 | if (StompCommand.CONNECT == accessor.getCommand()) { // websocket 연결요청 33 | String jwtToken = accessor.getFirstNativeHeader("token"); 34 | log.info("CONNECT {}", jwtToken); 35 | // Header의 jwt token 검증 36 | jwtTokenProvider.validateToken(jwtToken); 37 | } else if (StompCommand.SUBSCRIBE == accessor.getCommand()) { // 채팅룸 구독요청 38 | // header정보에서 구독 destination정보를 얻고, roomId를 추출한다. 39 | String roomId = chatService.getRoomId(Optional.ofNullable((String) message.getHeaders().get("simpDestination")).orElse("InvalidRoomId")); 40 | // 채팅방에 들어온 클라이언트 sessionId를 roomId와 맵핑해 놓는다.(나중에 특정 세션이 어떤 채팅방에 들어가 있는지 알기 위함) 41 | String sessionId = (String) message.getHeaders().get("simpSessionId"); 42 | chatRoomRepository.setUserEnterInfo(sessionId, roomId); 43 | // 채팅방의 인원수를 +1한다. 44 | chatRoomRepository.plusUserCount(roomId); 45 | // 클라이언트 입장 메시지를 채팅방에 발송한다.(redis publish) 46 | String name = Optional.ofNullable((Principal) message.getHeaders().get("simpUser")).map(Principal::getName).orElse("UnknownUser"); 47 | chatService.sendChatMessage(ChatMessage.builder().type(ChatMessage.MessageType.ENTER).roomId(roomId).sender(name).build()); 48 | log.info("SUBSCRIBED {}, {}", name, roomId); 49 | } else if (StompCommand.DISCONNECT == accessor.getCommand()) { // Websocket 연결 종료 50 | // 연결이 종료된 클라이언트 sesssionId로 채팅방 id를 얻는다. 51 | String sessionId = (String) message.getHeaders().get("simpSessionId"); 52 | String roomId = chatRoomRepository.getUserEnterRoomId(sessionId); 53 | // 채팅방의 인원수를 -1한다. 54 | chatRoomRepository.minusUserCount(roomId); 55 | // 클라이언트 퇴장 메시지를 채팅방에 발송한다.(redis publish) 56 | String name = Optional.ofNullable((Principal) message.getHeaders().get("simpUser")).map(Principal::getName).orElse("UnknownUser"); 57 | chatService.sendChatMessage(ChatMessage.builder().type(ChatMessage.MessageType.QUIT).roomId(roomId).sender(name).build()); 58 | // 퇴장한 클라이언트의 roomId 맵핑 정보를 삭제한다. 59 | chatRoomRepository.removeUserEnterInfo(sessionId); 60 | log.info("DISCONNECTED {}, {}", sessionId, roomId); 61 | } 62 | return message; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/templates/chat/room.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Websocket Chat 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |
19 |

채팅방 리스트

20 |
21 |
22 | 로그아웃 23 |
24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 |
    35 |
  • 36 |
    {{item.name}} {{item.userCount}}
    37 |
  • 38 |
39 |
40 | 41 | 42 | 43 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/resources/templates/chat/roomdetail.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Websocket ChatRoom 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |
20 |
21 |

{{roomName}} {{userCount}}

22 |
23 | 27 |
28 |
29 |
30 | 31 |
32 | 33 |
34 | 35 |
36 |
37 |
    38 |
  • 39 | {{message.sender}} - {{message.message}} 40 |
  • 41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 92 | 93 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | --------------------------------------------------------------------------------