搭建WEB項目過程中,哪些點需要註意: 1、技術選型: 前端:freemarker、vue 後端:spring boot、spring mvc 2、如何包裝返回統一結構結果數據? 首先要弄清楚為什麼要包裝統一結構結果數據,這是因為當任意的ajax請求超時或者越權操作時,系統能返回統一的錯誤信息給到前 ...
搭建WEB項目過程中,哪些點需要註意:
1、技術選型:
前端:freemarker、vue
後端:spring boot、spring mvc
2、如何包裝返回統一結構結果數據?
首先要弄清楚為什麼要包裝統一結構結果數據,這是因為當任意的ajax請求超時或者越權操作時,系統能返回統一的錯誤信息給到前端,前端通過封裝統一的ajax請求統一處理這類錯誤信息(這樣統一就避免每次都需要額外處理)。
那如何包裝結構呢?
先封裝統一返回結果結構對象 JsonMessage:
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請求。然後按不同請求類型處理:
/**
* 統一異常處理,不論是正常跳轉請求還是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異常跳轉地址:
@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中輸出預設是會輸出,那我們也可以改成不輸出。當然還有其他的就不舉例了。
/**
* 通過繼承 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:
<?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:
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); } }; } }