Spring Boot搭建Web项目常用功能

搭建WEB项目过程中,哪些点需要注意:

1、技术选型:

前端:freemarker、vue

后端:spring boot、spring mvc

2、如何包装返回统一结构结果数据?

首先要弄清楚为什么要包装统一结构结果数据,这是因为当任意的ajax请求超时或者越权操作时,系统能返回统一的错误信息给到前端,前端通过封装统一的ajax请求统一处理这类错误信息(这样统一就避免每次都需要额外处理)。

那如何包装结构呢?

先封装统一返回结果结构对象 JsonMessage:

<code class="hljs java">public class JsonMessage extends HashMap<String, Object> { private static final long serialVersionUID = -7149712196874923440L; public JsonMessage() { this.put("status", 200); } public JsonMessage(boolean status) { putStatus(status); } public JsonMessage(String msg) { this.put("status", 200); this.put("msg", msg); } public JsonMessage(boolean status, String msg) { this.put("msg", msg); putStatus(status); } public JsonMessage(String key,Object object) { this.put("status", 200); this.put(key, object); } public JsonMessage(boolean status, String msg, String key, Object value) { this.put("msg", msg); putStatus(status); this.put(key, value); } public JsonMessage putStatusAndMsg(int code, String msg) { this.put("status", code); this.put("msg", msg); return this; } public JsonMessage putStatusAndMsg(boolean status, String msg) { putStatus(status); this.put("msg", msg); return this; } public JsonMessage putStatus(int code) { this.put("status", code); return this; } public JsonMessage putStatus(boolean status) { if(status){ this.put("status", 200); }else{ this.put("status", 500); } return this; } public JsonMessage putRedirectUrl(String redirectUrl) { this.put("url", redirectUrl); this.put("status", 501); return this; } public JsonMessage putMsg(String msg) { this.put("msg", msg); return this; } public JsonMessage put(String arg0, Object arg1) { super.put(arg0, arg1); return this; } }

如何处理包装结果给前端呢?

方法一:所有的controller里ajax请求都返回JsonMessage对象;

方法二:通过 ResponseBodyAdvice 处理;

方法三:通过 HandlerMethodReturnValueHandler 拦截@ResponseBody注解或自定义注解 处理(不太懂的童鞋请百度);

3、如果统一处理异常?

继承 HandlerExceptionResolver 接口即可处理所有异常了,所以这也得分是否ajax请求。然后按不同请求类型处理:

<code class="hljs java">
/**
 * 统一异常处理,不论是正常跳转请求还是ajax请求都能处理,
 */
@Component
public class GlobalExceptionResolver implements HandlerExceptionResolver { private static Logger logger = LoggerFactory.getLogger(GlobalExceptionResolver.class); @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; String referer = request.getHeader("Referer"); String exceptionMsg = "系统异常,请稍后操作"; String userId = null; String userName = null; Object object = request.getSession().getAttribute(Constant.SESSION_USER); if (object != null) { LoginUser user = (LoginUser) object; userName = user.userName(); userId = user.getUserId(); } if (e instanceof BusinessException) { logger.warn(StringUtil.format("业务异常,当前请求URL:{} 操作用户编号:{} {} \n访问来源:{} \n参数:{}", request.getRequestURL(), userId, userName, referer, JsonUtils.beanToJson(request.getParameterMap())), e); BusinessException exception = (BusinessException) e; exceptionMsg = exception.getMessage(); } else { logger.error(StringUtil.format("系统异常,当前请求URL:{} 操作用户编号:{} {} \n访问来源:{} \n参数:{}", request.getRequestURL(), userId, userName, referer, JsonUtils.beanToJson(request.getParameterMap())), e); } if (AnnotationHandleUtils.isAjaxAnnotation(handlerMethod)) { JsonMessage jmsg = new JsonMessage(false, exceptionMsg); try { WebHelper.write(response, jmsg.toString(), HttpStatus.OK.value()); } catch (IOException e1) { logger.error("发送数据异常", e1); } return new ModelAndView(); } ModelAndView modelView = new ModelAndView("common/500"); //跳转到500错误页面 return modelView.addObject(Constant.ERROR_MES_KEY, exceptionMsg); } return null; } }

配置servlet 404、500异常跳转地址:

<code class="hljs java">@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() { return new EmbeddedServletContainerCustomizer() { @Override public void customize(ConfigurableEmbeddedServletContainer container) { ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/common/404.html"); ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/common/500.html"); ErrorPage errorpage = new ErrorPage("/common/500.html"); container.addErrorPages(error404Page, error500Page,errorpage); } }; }

4、如果优雅的处理按钮级别权限?

因为前端采用的是Vue,清楚vue的知道它的表现就是通过model控制view的,所以前端就是在页面渲染 mounted 的时候用ajax去请求,通过返回的字段信息判断是否要显示某按钮或者链接或者视图块。

那后端要如何才能做到验证权限呢?

采用 HandlerMethodReturnValueHandler 拦截所有需要返回权限信息的ajax请求,再根据 methodParameter能获取到method对象,然后就能获取到method上的权限注解信息了再统一调用鉴权服务,再把结果包装到JsonMessage对象返回就可以了。

5、如何配置消息装换器?

首先要弄清楚为什么需要配,因为我们需要按项目要求来下自定义Jackson转换json规范,比如:date类型默认情况是转成时间戳,那这对于前端就需要再装换才可以。再比如null值的对象是否要在json中输出默认是会输出,那我们也可以改成不输出。当然还有其他的就不举例了。

<code class="hljs java">
/**
 * 通过继承 WebMvcConfigurerAdapter 来配置spring mvc
 *
 */
@Configuration
public class ApplicationConfiguration extends WebMvcConfigurerAdapter{ @Autowired private LoginInterceptor loginInterceptor; @Autowired private PermissionInterceptor permissionInterceptor; @Autowired private ResponseBodyResolver responseBodyResolver; /** * 可以注入spring mvc提供的 RequestMappingHandlerAdapter bean */ @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; @Autowired private DateConverter dateConverter; /** * 添加interceptors */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor); registry.addInterceptor(permissionInterceptor); super.addInterceptors(registry); } /** * 配置消息转换器 */ @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new ByteArrayHttpMessageConverter()); converters.add(mappingJackson2HttpMessageConverter()); //配置jackson2 super.configureMessageConverters(converters); } public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(){ MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); mappingJackson2HttpMessageConverter.setSupportedMediaTypes( Lists.newArrayList(MediaType.TEXT_PLAIN,MediaType.APPLICATION_JSON_UTF8)); ObjectMapper objectMapper= new ObjectMapper(); //属性命名规则,这个一般不需要配 objectMapper.setPropertyNamingStrategy(new LowerCasePropertyNamingStrategy()); objectMapper.configure(MapperFeature.ALLOW_EXPLICIT_PROPERTY_RENAMING, true); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); //默认date属性格式,可以其它的 objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper); return mappingJackson2HttpMessageConverter; } @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { returnValueHandlers.add(responseBodyResolver); super.addReturnValueHandlers(returnValueHandlers); } /** * 配置属性编辑器,主要是当前端form提交字符串时转成date类型 */ @PostConstruct public void webBindingInitializer(){ requestMappingHandlerAdapter.setWebBindingInitializer(dateConverter); } } @Component public class DateConverter implements WebBindingInitializer{ @Override public void initBinder(WebDataBinder binder, WebRequest request) { binder.setAutoGrowCollectionLimit(Integer.MAX_VALUE); // CustomDateEditor只要继承PropertyEditorSupport CustomDateEditor dateEditor = new CustomDateEditor(CustomDateEditor.TIMEFORMAT, true); //注册自定义的属性编辑器 表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 binder.registerCustomEditor(Date.class, dateEditor); } } 

6、项目示例:

6.1 项目依赖 pom.xml:

<code class="hljs xml"><?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.2.RELEASE</version> <relativePath /> </parent> <artifactId>web-demo</artifactId> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <!-- spring-boot --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.spring.boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>--> </dependencies> </project> 

application.properties:

<code class="hljs bash">logging.config=classpath:conf/xml/logback.xml

#freemarker config info
spring.freemarker.templateEncoding=UTF-8
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false spring.freemarker.request-context-attribute=rc spring.freemarker.templateLoaderPath=classpath:/templates/pages #这没加后缀是因为在代码里手动标名后缀 spring.freemarker.suffix= spring.freemarker.view-names=*.html #server config server.session.timeout=1800 server.contextPath=/demo server.port=8080 #server.compression.enabled=true #server.compression.min-response-size=1024 #server.tomcat.max-threads=500 #resource config spring.resources.chain.cache=false spring.resources.static-locations=classpath:/static/ #spring.resources.cache-period=60 #cache config spring.cache.type=guava #缓存最大数量1000条, 缓存失效时间5分钟 spring.cache.guava.spec=maximumSize=1000,expireAfterAccess=10m spring.http.multipart.max-file-size=5Mb spring.http.multipart.max-request-size=5Mb spring.http.multipart.enabled=true

6.2 启动类:

@PropertySource(value={"classpath:conf/env/datasource.properties",
    "classpath:conf/env/message.properties",
    "classpath:conf/env/config.properties"})
@SpringBootApplication
@EnableTransactionManagement
@EnableAutoConfiguration(exclude=RabbitAutoConfiguration.class)
@EnableCaching
@MapperScan("com.test.demo.persistence")
@ComponentScan(value={"com.test.demo"})
//导入spring xml文件
//@ImportResource(locations = { "classpath*:/spring.xml" })
public class WebDemoApplication {
    
    private final static Logger logger = LogManager.getLogger(WebDemoApplication.class);
    
    public static void main(String[] args) {
       System.setProperty("spring.config.location", "classpath:conf/env/application.properties");
       SpringApplication.run(WebDemoApplication .class, args);
       logger.info("start completed !");
    }
    
    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {

        return new embeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
            ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/common/404.html");
                ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/common/500.html");
                ErrorPage errorpage = new ErrorPage("/common/500.html");
                container.addErrorPages(error404Page, error500Page,errorpage);
            }
        };
    }
    
}