使用Spring框架各个组件实现一个在线聊天网页,当有用户连接WebSocket,服务器监听到用户连接会使用Stomp推送最新用户列表,有用户断开刷新在线列表,实时推送用户聊天信息。引入Jetty服务器,直接嵌入整个工程可以脱离Java Web容器独立运行,使用插件打包成一个jar文件,就像Spring Boot一样运行,部署。

pom.xml 依赖

 <properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.encoding>UTF-8</maven.compiler.encoding><jetty.version>9.4.8.v20171121</jetty.version><spring.version>5.0.4.RELEASE</spring.version><jackson.version>2.9.4</jackson.version><lombok.version>1.16.18</lombok.version><dbh2.version>1.4.196</dbh2.version><jcl.slf4j.version>1.7.25</jcl.slf4j.version><spring.security.version>5.0.3.RELEASE</spring.security.version><logback.version>1.2.3</logback.version><activemq.version>5.15.0</activemq.version></properties><dependencies><dependency><groupId>org.eclipse.jetty</groupId><artifactId>jetty-servlet</artifactId><version>${jetty.version}</version></dependency><!-- 添加websocket 依赖不然会出现 java.lang.IllegalStateException: No suitable defaultRequestUpgradeStrategy found --><dependency><groupId>org.eclipse.jetty.websocket</groupId><artifactId>websocket-server</artifactId><version>${jetty.version}</version></dependency><dependency><groupId>org.eclipse.jetty.websocket</groupId><artifactId>websocket-api</artifactId><version>${jetty.version}</version></dependency><!--spring mvc --><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>${spring.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>${spring.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webflux</artifactId><version>${spring.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-websocket</artifactId><version>${spring.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-messaging</artifactId><version>${spring.version}</version></dependency><!--spring security --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId><version>${spring.security.version}</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId><version>${spring.security.version}</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-client</artifactId><version>${spring.security.version}</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId><version>${spring.security.version}</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-messaging</artifactId><version>${spring.security.version}</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>${jackson.version}</version></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>${dbh2.version}</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>${jackson.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>${jcl.slf4j.version}</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>${logback.version}</version></dependency><dependency><groupId>org.apache.activemq</groupId><artifactId>activemq-broker</artifactId><version>${activemq.version}</version></dependency><dependency><groupId>io.projectreactor.ipc</groupId><artifactId>reactor-netty</artifactId><version>0.7.2.RELEASE</version></dependency></dependencies>

1. 配置H2 嵌入式数据库

    @Bean  //内存模式public DataSource  dataSource(){EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();EmbeddedDatabase build = builder.setType(EmbeddedDatabaseType.H2).addScript("db/sql/create-db.sql") //每次创建数据源都会执行脚本.addScript("db/sql/insert-data.sql").build();return build;}

这种方式是利用Spring 内置的嵌入式数据库的数据源模板,创建的数据源,比较简单,但是这种方式不支持定制,数据只能保存在内存中,项目重启数据就会丢失了。

设置数据保存到硬盘

    @Beanpublic DataSource dataSource() {DriverManagerDataSource dataSource = new DriverManagerDataSource();dataSource.setDriverClassName("org.h2.Driver");dataSource.setUsername("embedded");dataSource.setPassword("embedded");dataSource.setUrl("jdbc:h2:file:./data;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;");return dataSource;}

如果你还想每次创建数据源执行初始化sql,使用org.springframework.jdbc.datasource.init.ResourceDatabasePopulator 装载sql 脚本用于初始化或清理数据库

    @Beanpublic ResourceDatabasePopulator databasePopulator() {ResourceDatabasePopulator populator = new ResourceDatabasePopulator();populator.addScript(schema);populator.addScripts(data);populator.setContinueOnError(true);return populator;}

设置DatabasePopulator 对象,用户数据源启动或者消耗的时候执行脚本

 @Beanpublic DataSourceInitializer initializer() {DataSourceInitializer initializer = new DataSourceInitializer();initializer.setDatabasePopulator(databasePopulator());initializer.setDataSource(dataSource());return initializer;}

启用H2 web Console

 @Bean(initMethod = "start",destroyMethod = "stop")public Server DatasourcesManager() throws SQLException {return Server.createWebServer("-web","-webAllowOthers","-webPort","8082");}

浏览器打开 http://localhost:8082 访问H2 控制台

设置事务管理器

 @Beanpublic PlatformTransactionManager transactionManager() {PlatformTransactionManager manager = new DataSourceTransactionManager(dataSource());return manager;}
}

到这里,嵌入H2数据库配置基本已经设置完成了

2. Spring MVC配置

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.List;@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.ting.spring.controller", //基包路径设置includeFilters = @ComponentScan.Filter(value = {ControllerAdvice.class,Controller.class})) //只扫描MVC controll的注解
public class WebMvcConfiguration implements WebMvcConfigurer {public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {converters.add(new MappingJackson2HttpMessageConverter());}@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {//添加静态路径映射registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");}
}

3. Jetty嵌入式服务

因为Spring 注解扫描只能注册一个类, 使用@Import引入其他的配置类

@Configuration
@ComponentScan(basePackages = "org.ting.spring",excludeFilters = {@ComponentScan.Filter(value = {Controller.class,ControllerAdvice.class})})
@Import({WebMvcConfiguration.class}) //引入Spring MVC配置类
public class WebRootConfiguration {@Autowiredprivate DataSource dataSource;@Beanpublic JdbcTemplate jdbcTemplate(){JdbcTemplate template = new JdbcTemplate(dataSource);return template;}
}

使用Spring AnnotationConfigWebApplicationContext 启动注解扫描,注册创建bean将WebApplicationContext,在将对象传给DispatcherServlet

public class JettyEmbedServer {private final static int DEFAULT_PORT = 9999;private final static String DEFAULT_CONTEXT_PATH = "/";private final static String MAPPING_URL = "/*";public static void main(String[] args) throws Exception {Server server = new Server(DEFAULT_PORT);JettyEmbedServer helloServer = new JettyEmbedServer();server.setHandler(helloServer.servletContextHandler());server.start();server.join();}private ServletContextHandler servletContextHandler() {WebApplicationContext context = webApplicationContext();ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);servletContextHandler.setContextPath(DEFAULT_CONTEXT_PATH);ServletHolder servletHolder = new ServletHolder(new DispatcherServlet(context));servletHolder.setAsyncSupported(true);servletContextHandler.addServlet(servletHolder, MAPPING_URL);return servletContextHandler;}private WebApplicationContext webApplicationContext() {AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();context.register(WebRootConfiguration.class);return context;}                  

3. 配置Spring Security

默认Spring Security拦截请求,登录失败,登录成功都是页面跳转的方式,我们希望ajax请求的时候,无论是被拦截了,或者登录失败,成功都可以返回json格式数据,由前端人员来处理。
根据HttpRequestServlet 请求头 X-Requested-With是否等于XMLHttpRequest 判断是否是ajax。

public class RespnonseJson {public static void jsonType(HttpServletResponse response) {response.setContentType("application/json;charset=UTF-8");response.setCharacterEncoding("utf-8");}public static boolean ajaxRequest(HttpServletRequest request){String header = request.getHeader("X-Requested-With");return ! StringUtils.isEmpty(header) && header.equals("XMLHttpRequest");}public static boolean matchURL(String url) {Pattern compile = Pattern.compile("^/api/.+");return compile.matcher(url).matches();}
}

登录认证处理器

public class RestAuthenticationEntryPoint  extends LoginUrlAuthenticationEntryPoint {/*** @param loginFormUrl URL where the login page can be found. Should either be*                     relative to the web-app context path (include a leading {@code /}) or an absolute*                     URL.*/public RestAuthenticationEntryPoint(String loginFormUrl) {super(loginFormUrl);}@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {String uri = request.getRequestURI();if (matchURL(uri)) {  //  /api 都是ajax 请求jsonType(response);response.getWriter().println(getErr(authException.getMessage()));}else if (ajaxRequest(request)){jsonType(response);response.getWriter().println(getErr(authException.getMessage()));}else super.commence(request,response,authException);}private String getErr(String description) throws JsonProcessingException {Result result = Result.error(Result.HTTP_FORBIDDEN, description);ObjectMapper mapper = new ObjectMapper();return mapper.writeValueAsString(result);}
}

登录成功处理

public class RestAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {String uri = request.getRequestURI();if (matchURL(uri)){jsonType(response);String value = loginSuccess();response.getWriter().println(value);}else if (ajaxRequest(request)){jsonType(response);String success = loginSuccess();response.getWriter().println(success);}else super.onAuthenticationSuccess(request,response,authentication);}private String loginSuccess() throws JsonProcessingException {Result success = Result.success("sign on success go to next!");ObjectMapper mapper = new ObjectMapper();return mapper.writeValueAsString(success);}
}

登录失败处理

public class RestAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {if (ajaxRequest(request)){jsonType(response);String err = getErr(exception.getMessage());response.getWriter().println(err);}else super.onAuthenticationFailure(request,response,exception);}public String getErr(String description) throws JsonProcessingException {Result result = Result.error(Result.HTTP_AUTH_FAILURE, description);ObjectMapper mapper = new ObjectMapper();return mapper.writeValueAsString(result);}
}

我在网上搜索ajax 认证错误,很多博客是这样写的

response.sendError(500, "Authentication failed");

这个错误会被Jetty 错误页面捕获,扰乱返回JSON数据,这个细节要注意下

注册Handler

   @Beanpublic AuthenticationEntryPoint entryPoint() {RestAuthenticationEntryPoint entryPoint = new RestAuthenticationEntryPoint("/static/html/login.html"); return entryPoint;}@Beanpublic SimpleUrlAuthenticationSuccessHandler successHandler() {RestAuthSuccessHandler successHandler = new RestAuthSuccessHandler();return successHandler;}@Beanpublic SimpleUrlAuthenticationFailureHandler failureHandler() {RestAuthFailureHandler failureHandler = new RestAuthFailureHandler();return failureHandler;}

配置url 认证

    @Beanpublic SessionRegistry sessionManager() {return new SessionRegistryImpl();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.exceptionHandling().authenticationEntryPoint(entryPoint()).and().authorizeRequests().antMatchers("/static/html/jetty-chat.html","/api/user/online", "/api/user/loginuser") .authenticated() //设置需要认证才可以请求的接口.and().formLogin().successHandler(successHandler()) //登录成功处理.failureHandler(failureHandler()) //登录失败处理.loginPage("/static/html/login.html") //登录页面.loginProcessingUrl("/auth/login") //登录表单url .defaultSuccessUrl("/static/html/jetty-chat.html") //成功跳转url.permitAll().and().csrf().disable()//禁用csrf 因为没有使用模板引擎.sessionManagement().maximumSessions(1)  //设置同一个账户,同时在线次数.sessionRegistry(sessionManager()) // 设置Session 管理器,.expiredUrl("/static/html/login.html") //session 失效后,跳转url.maxSessionsPreventsLogin(false) //设置true,达到session 最大登录次数后,后面的账户都会登录失败,false 顶号 前面登录账户会被后面顶下线;//注销账户,跳转到登录页面http.logout().logoutUrl("/logout").logoutSuccessUrl("/static/html/login.html");

在配置类添加@EnableWebSecurity,在扫描类上引入Spring Security配置,大功告成了,并没有!Spring Security 是使用Filter来处理一些认证请求,需要我们在Jetty中手动注册拦截器

        //手动注册拦截器,让Spring Security 生效FilterHolder filterHolder = new FilterHolder(new DelegatingFilterProxy("springSecurityFilterChain"));servletContextHandler.addFilter(filterHolder, MAPPING_URL, null);servletContextHandler.addEventListener(new ContextLoaderListener(context));servletContextHandler.addEventListener(new HttpSessionEventPublisher()); //使用security session 监听器 限制只允许一个用户登录

4. 配置WebSocketStompConfig

@Configuration
@EnableWebSocketMessageBroker
@ComponentScan(basePackages = "org.ting.spring.stomp.message")
@Slf4j
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {//设置连接的端点路径@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("endpoint").withSockJS();}@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {// 定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息registry.enableSimpleBroker("/topic", "/queue");// 定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀registry.setApplicationDestinationPrefixes("/app");//使用客户端一对一通信registry.setUserDestinationPrefix("/user");registry.setPathMatcher(new AntPathMatcher("."));}}

配置stomp 频道认证

@Configuration
public class SocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {@Overrideprotected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {messages.simpDestMatchers("/user/**").authenticated()//认证所有user 链接.anyMessage().permitAll();}//允许跨域  不然会出现 Could not verify the provided CSRF token because your session was not found 异常@Overrideprotected boolean sameOriginDisabled() {return true;}
}

信息处理

@Controller
@Slf4j
public class StompController {@Autowiredprivate SimpMessagingTemplate messagingTemplate;@MessageExceptionHandler@SendToUser("/queue.errors")public String handleException(Throwable exception) {return exception.getMessage();}@MessageMapping("receive.messgae")public void forwardMsg(ChatMessage message){log.info("message :  {}",message);message.setLocalDateTime(LocalDateTime.now());messagingTemplate.convertAndSendToUser(message.getTargetUser().getEmail(),"queue.notification",message);}}

@MessageMapping 作用与@RequestMapping 功能差不多用于匹配url
更多Spring WebSocket 官方文档查看

我们使用一个集合来保存连接上的用户,使用连接,断开监听器来修改集合的列表,并将集合的数据发布到频道上。

websocket 断开连接监听器

@Component
@Slf4j
public class WebSocketDisconnectListener implements ApplicationListener<SessionDisconnectEvent> {@Autowiredprivate UserService userService;@Autowiredprivate SimpMessagingTemplate messageTemplate;@Overridepublic void onApplicationEvent(SessionDisconnectEvent event) {Principal principal = event.getUser();log.info("client sessionId : {} name : {} disconnect ....",event.getSessionId(),principal.getName());if (principal != null){ //已经认证过的用户User user = userService.findByEmail(principal.getName());Online.remove(user);messageTemplate.convertAndSend("/topic/user.list",Online.onlineUsers());}}
}

注册连接websocket 监听器

@Component
@Slf4j
public class WebSocketSessionConnectEvent implements ApplicationListener<SessionConnectEvent>{@Autowiredprivate SimpMessagingTemplate messageTemplate;@Autowiredprivate UserService userService;@Overridepublic void onApplicationEvent(SessionConnectEvent event) {Principal principal = event.getUser();log.info("client name: {} connect.....",principal.getName());if (principal != null){User user = userService.findByEmail(principal.getName());Online.add(user);messageTemplate.convertAndSend("/topic/user.list",Online.onlineUsers());}}
}

保存在线列表

public class Online {private static Map<String,User> maps = new ConcurrentHashMap<>();public static void add(User user){maps.put(user.getEmail(),user);}public static void remove(User user){maps.remove(user.getEmail());}public static Collection<User> onlineUsers(){return maps.values();}}

4. Spring Security OAuth2 Client 配置

手动配置ClientRegistrationRepository 设置client-id,client-secret,redirect-uri-template

   @Beanpublic ClientRegistrationRepository clientRegistrationRepository() {return new InMemoryClientRegistrationRepository(githubClientRegstrationRepository(),googleClientRegistrionRepository());}public ClientRegistration githubClientRegstrationRepository(){return CommonOAuth2Provider.GITHUB.getBuilder("github").clientId(env.getProperty("registration.github.client-id")).clientSecret(env.getProperty("registration.github.client-secret")).redirectUriTemplate(env.getProperty("registration.github.redirect-uri-template")).build();}public ClientRegistration googleClientRegistrionRepository(){return CommonOAuth2Provider.GOOGLE.getBuilder("google").clientId(env.getProperty("registration.google.client-id")).clientSecret(env.getProperty("registration.google.client-secret")).redirectUriTemplate(env.getProperty("registration.google.redirect-uri-template")).scope( "profile", "email").build();}@Beanpublic OAuth2AuthorizedClientService authorizedClientService() {return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository());}

我们使用github,google OAuth2 授权登录的账户,登录通过后保存起来,则需求继承DefaultOAuth2UserService


@Service
@Slf4j
public class CustomOAuth2UserService extends DefaultOAuth2UserService {@Autowiredprivate UserService userService;@Overridepublic OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {OAuth2User oAuth2User = super.loadUser(userRequest);try {oAuth2User = processOAuth2User(oAuth2User,userRequest);} catch (Exception e) {log.error("processOAuth2User error {}",e);}return oAuth2User;}private OAuth2User processOAuth2User(OAuth2User oAuth2User,OAuth2UserRequest userRequest) {String clientId = userRequest.getClientRegistration().getRegistrationId();if (clientId.equalsIgnoreCase("github")) {Map<String, Object> map = oAuth2User.getAttributes();String login =  map.get("login")+"_oauth_github";String name = (String) map.get("name");String avatarUrl = (String) map.get("avatar_url");User user = userService.findByEmail(login);if (user == null) {user = new User();user.setUsername(name);user.setEmail(login);user.setAvatar(avatarUrl);user.setPassword("123456");userService.insert(user);}else {user.setUsername(name);user.setAvatar(avatarUrl);userService.update(user);}return UserPrincipal.create(user, oAuth2User.getAttributes());}else if (clientId.equalsIgnoreCase("google")){Map<String, Object> result = oAuth2User.getAttributes();String email = result.get("email")+"_oauth_google";String username = (String) result.get("name");String imgUrl = (String) result.get("picture");User user = userService.findByEmail(email);if (user == null){user = new User();user.setEmail(email);user.setPassword("123456");user.setAvatar(imgUrl);user.setUsername(username);userService.insert(user);}else {user.setUsername(username);user.setAvatar(imgUrl);userService.update(user);}return UserPrincipal.create(user,oAuth2User.getAttributes());}return null;}
}

重写UserDetails

public class UserPrincipal implements OAuth2User,UserDetails {private long id;private String name;private String password;private boolean enable;private Collection<? extends GrantedAuthority> authorities;private Map<String,Object> attributes;UserPrincipal(long id,String name,String password,boolean enable,Collection<? extends GrantedAuthority> authorities){this.id = id;this.name = name;this.password = password;this.authorities = authorities;this.enable = enable;}public static UserPrincipal create(User user){return new UserPrincipal(user.getId(),user.getEmail(),user.getPassword(),user.isEnable(),Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));}public static UserPrincipal create(User user, Map<String, Object> attributes) {UserPrincipal userPrincipal = UserPrincipal.create(user);userPrincipal.attributes = attributes;return userPrincipal;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return name;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return this.enable;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic Map<String, Object> getAttributes() {return this.attributes;}@Overridepublic String getName() {return String.valueOf(this.id);}
}

设置Spring Security OAuth2 Client

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate CustomOAuth2UserService customOAuth2UserService;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().clientRegistrationRepository(clientRegistrationRepository()).authorizedClientService(authorizedClientService()).userInfoEndpoint().userService(customOAuth2UserService).and().defaultSuccessUrl("/static/html/jetty-chat.html");
}
}

默认授权端点,点击后直接重定向到授权服务器的登录页面,Spring 默认是: oauth2/authorization/{clientId}
默认授权成功跳转url/login/oauth2/code/{clientId}

这个项目参考的教程:
https://www.baeldung.com/spring-security-5-oauth2-login
https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/

这个教程只展示了一部分的代码,想查看完整的项目代码,可以去github: spring-stomp-security-webflux-embedded-jetty查看

Spring MVC实现Spring Security,Spring Stomp websocket Jetty嵌入式运行相关推荐

  1. Java Spring MVC框架 VIII 之 Spring MVC拦截器

    Java Spring MVC框架 VIII 之 Spring MVC拦截器 Spring MVC拦截器 1.拦截器简介 拦截器是SpringMvc框架提供的功能 它可以在控制器方法运行之前或运行之后 ...

  2. Spring MVC,Thymeleaf,Spring Security应用程序中的CSRF保护

    跨站点请求伪造(CSRF)是一种攻击,它迫使最终用户在当前已通过身份验证的Web应用程序上执行不需要的操作. 如果您使用Spring Security 3.2及更高版本,在Spring MVC / T ...

  3. spring mvc 渲染html,在Spring MVC中使用Thymeleaf模板渲染Web视图

    Thymeleaf模板是原生的,不依赖于标签库.它能在接受原始HTML的地方进行编辑和渲染.由于没有与Servlet规范耦合,因此Thymeleaf模板能够进入JSP所无法涉及的领域 如果想要在Spr ...

  4. java spring mvc 上传_Java Spring MVC 上传下载文件配置及controller方法详解

    下载: 1.在spring-mvc中配置(用于100M以下的文件下载) 下载文件代码 @RequestMapping("/file/{name.rp}") public Respo ...

  5. spring mvc使用html页面,Spring MVC静态页面

    以下示例显示如何使用Spring MVC Framework编写一个简单的基于Web的应用程序,它可以使用标记访问静态页面和动态页面.首先使用Eclipse IDE创建一个动态WEB项目,并按照以下步 ...

  6. spring mvc 小结-51cto学院Spring MVC

    一.Spring MVC 基础 Spring mvc 框架 是一个MVC框架,通过实现MVC很好地将数据.业务.展现进行分离,其底层仍然是servlet 要在web.xml 中配置servlet Sp ...

  7. 简单的Spring MVC入门程序,对于Spring mvc工作流程的理解,servlet标签和servlet-mapping 理解,视图解析器

    javaweb SpringMvc的组成:jsp,JavaBean,servlet 可以使用Spring所提供的功能 提供了前端控制器DispatcherServlet,不需要细化Servlet 执行 ...

  8. 在spring MVC项目中集成Spring session redis (使用spring session框架,redis作为存储缓存)...

    2019独角兽企业重金招聘Python工程师标准>>> 1.为项目增加以来  pom.xml中使用 <!-- spring session 单点登录 --> //本项目使 ...

  9. spring mvc传值html页面,spring mvc向前台页面传值-ModelAndView

    ModelAndView 该对象中包含了一个model属性和一个view属性: model:其实是一个ModelMap类型.ModelMap是一个LinkedHashMap的子类. view:包含了一 ...

最新文章

  1. mybatis中![CDATA[]]的作用
  2. NLP入门竞赛,搜狗新闻文本分类!拿几十万奖金!
  3. 试编写一个汇编语言程序,要求从键盘接收一个四位的十六进制数,并在终端上显示与它等值的二进制数
  4. 手机黑圆点怎么打_黑鲨游戏手机3S手机配置怎么样,是否值得购买?
  5. 劳动力工资调整模型的探讨——数学建模
  6. bat代码雨代码流星_bat-入门系列-03-判断结构2
  7. 2017.05.01
  8. python 版本控制及django,git的使用
  9. 改变form里面input,textarea.select等的默认样式
  10. 松下FP-XH系列PLC 断电保持寄存器使用注意事项
  11. Qt如何获取外网IP地址
  12. php 查询数据传值,php-如何在Laravel中传递数据进行查看?
  13. linux下hwclock及clock命令详解
  14. 【光线追踪系列九】物体动态模糊
  15. defer=defer
  16. 微服务架构集成RabbitMQ给用户推送消息(发送短信,发送邮件,发送站内信息)
  17. C++重点之“引用变量”用法
  18. Linux学习从入门到精通推荐书籍
  19. html侧边工具栏,侧边栏工具条
  20. MySQL查询年龄最大学生信息_查询xsda表中年龄最大的学生的出生日期

热门文章

  1. 什么是栈,栈存储结构详情
  2. pytest第二版 进阶学习
  3. linux按函数数字大小排序,linux awk 数组排序多种实现方法
  4. html 两个iframe重叠,解决同一页面中两个iframe互相调用jquery,js函数的方法
  5. 树结构练习——排序二叉树的中序遍历
  6. 【Linux】6.服务器会话的screen用法
  7. 利用TinyXML读取VOC2012数据集的XML标注文件裁剪出所有人体目标保存为文件
  8. 国内CVPR和图像处理领域的公司和研究机构
  9. MyBatis 源码分析 - 插件机制
  10. TreeSet源码解析