1. 程式人生 > >SpringBoot學習-第四章 SpringMVC基礎-

SpringBoot學習-第四章 SpringMVC基礎-

SpringMvc 快速搭建

  • 依賴 : 這裡直接使用SpringBoot的快速搭建
<!-- 包含常用的web/mvc等依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 用於管理spring相關的依賴版本 -->
<dependency>
    <groupId
>
org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency>
  • 日誌 : Spring4推薦使用logback

    簡歷一個logback.xml檔案進行日誌配置 ,內容與log4j差不多

  • 頁面 : SpringBoot習慣把頁面放置在resources下面

快速配置

用SpringBoot(註解風格)配置代替web.xml和spring-mvc.xml

  • web.xml
/**
 * @WebApplicationInitializer 用來配置Servlet3.0的介面 ,也就代替了web.xml ,裡面配置的內容和xml配置基本一致 ,部署在tomcat時容器會自動尋找並載入這個實現
 * <p>
 * SpringBoot方式啟動的話 ,可以通過配置類(類裡定義servlet bean ,然後import到Application)
 */
public
class WebInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(MvcConfig.class); context.setServletContext(servletContext); ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(context)); servlet.addMapping("/"); servlet.setLoadOnStartup(1); } }
  • SpringMvc
@Configuration
@EnableWebMvc //開啟一些預設配置MessageConverters,ViewResolvers等
@ComponentScan("demo2.springboot.mvc")
public class MvcConfig {
    /**
     * 註冊檢視轉換器 ,為mvc返回的頁面路徑新增前後綴
     */
    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/classes/views/");
        viewResolver.setSuffix(".jsp");
        viewResolver.setViewClass(JstlView.class);
        return viewResolver;
    }
}

頁面和Controller

@Controller
public class HelloController {

    @RequestMapping("/index")
    public String hello() {
        System.out.println("進入controller");
        return "index";
    }
}

省略index.jsp

部署

SpringBoot(-web)有自帶的Tomcat ,先排除再關聯Servlet3.0需要的包
用Maven打包成war ,部署在tomcat即可

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
 </dependencies>

SpringMvc常用註解

  • @Controller 宣告控制器Bean ,容器的DispatcherServlet會把控制器和url繫結
  • @RequestMapping 指定訪問路徑 ,produces可指定返回資源的型別
  • @ResponseBody 支援返回值放在response內 ,用於AJAX返回資料而非頁面
  • @RequestBody 允許引數在request內 ,通常處理POST體
  • @PathVarible 接受路徑中的引數 ,例如: /user/add/10002 -> ( /user/add/{id} ) -> id=10002
  • @RestController 等效 @Controller + @ResponseBody
@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping(produces = "text/plain;charset=UTF-8")
    @ResponseBody
    public String index(HttpServletRequest request) {
        return "url:" + request.getRequestURL() + "can access";
    }

    @RequestMapping(value = "/login/{userId}", produces = "text/plain;charset=UTF-8")
    @ResponseBody
    public String demoLogin(@PathVariable Integer userId, HttpServletRequest request) {
        return "url:" + request.getRequestURL() + "can access , id is :" + userId;
    }

    @RequestMapping(value = "/register", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String demoRegister(UserBean user, HttpServletRequest request) {
        return "url:" + request.getRequestURL() + "can access , User:[" + user.getId() + "," + user.getName() + "]";
    }

    @RequestMapping(value = {"/name1", "/name2"}, produces = "application/xml;charset=UTF-8")
    @ResponseBody
    public String demoMultiPath(HttpServletRequest request) {
        return "url:" + request.getRequestURL() + "can access";
    }
}

MVC基本配置

  • DispatcherServlet通常攔截所有URL ,而靜態資源 js/html/css 需要直接訪問 ,需要對Mvc進行配置
    • 靜態資源放置在resources(根目錄)下
    • 配置類繼承WebMvcConfigurerAdapter
    • 新增靜態資源路徑 ,覆蓋addResourceHandlers(ResourceHandlerRegistry registry)方法
public class MvcConfig extends WebMvcConfigurerAdapter {

    //...
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/assets/");
    }
}
  • 攔截器配置
    • 實現HandlerInterceptor介面 或者 繼承HandlerInterceptorAdapter類 ,實現自定義攔截器
    • WebMvcConfigurerAdapter中新增interceptor
public class DemoInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}
}

public class MvcConfig extends WebMvcConfigurerAdapter {

    //...
   @Bean
   public DemoInterceptor demoInterceptor() {
       return new DemoInterceptor();
   }
   @Override
    public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(demoInterceptor());
    }
}
  • ControllerAdvice - 控制器行為總控 (類似AOP)
    • 使用@ControllerAdvice註解一個類
    • 註解ControllerAdvice類中的方法 ,對所有@RequesMapping的方法生效
@ControllerAdvice //註解啟動了一個總控的Controller ,裡面的方法會應用到所有@RequestMapping方法 ,並根據註解的不同產生不同作用
public class DemoControllerAdvice {

    @ModelAttribute //在目標方法執行前 , 產生一個物件 , 並setAttribute
    public UserBean addAttribute() {
        System.out.println("============應用到所有@RequestMapping註解方法,在其執行之前把返回值放入Model");
        return new UserBean(1, "admin");
    }

    @InitBinder // 針對WebDataBinder的預處理
    public void initBinder(WebDataBinder binder) {
        System.out.println("============應用到所有@RequestMapping註解方法,在其執行之前初始化資料繫結器");
    }

    @ExceptionHandler(NoClassDefFoundError.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ModelAndView handleException(WebRequest request, NoClassDefFoundError e) {
        System.out.println("===========應用到所有@RequestMapping註解的方法,在其丟擲NoClassDefFoundError異常時執行");
        ModelAndView modelAndView = new ModelAndView("error");
        modelAndView.addObject("errorMsg", e.getMessage());
        return modelAndView;
    }
}
  • 路徑引數預設忽略”.”後的 ,例如 /user/{xx.yy} ,接收到的只有 xx , 需要在Mvc配置中手動關閉
public class MvcConfig extends WebMvcConfigurerAdapter {
    //...
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseRegisteredSuffixPatternMatch(false);
    }
}

檔案上傳

  • 引入commons-fileupload包
  • MVC配置對媒體資源的處理
  • Controller處理 ,儲存收到的檔案
public class MvcConfig extends WebMvcConfigurerAdapter {

    //...

    /**
     * 對multipart型別 (檔案)的預設處理設定
     * 由commons-upload實現 需要引入
     */
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setMaxUploadSize(1000000);
        multipartResolver.setDefaultEncoding("UTF-8");
        return multipartResolver;
    }
}

@Controller
@RequestMapping("/file")
public class FileController {

    @RequestMapping(value = "upload", method = RequestMethod.POST)
    @ResponseBody
    public String uploadFile(MultipartFile file) {
        try {
            File newFile = new File("D:/upload/" + file.getOriginalFilename());
            FileUtils.writeByteArrayToFile(newFile, file.getBytes());
            return "上傳成功";
        } catch (Exception e) {
            e.printStackTrace();
            return "上傳失敗 : " + e.getMessage();
        }
    }
}

MessageCovertor

  • Spring內建了很多 ,預設Jackson
  • 如果需要自定義 ,繼承 AbstractHttpMessageCovertor

服務端推送

  • Ajax心跳 : 頻率不好控制 ,伺服器壓力
  • WebSocket
  • 非同步等待 ,伺服器抓住請求 ,等待推送時再返回(Server Send Event) - 實質還是瀏覽器不斷請求 非同步處理

SSE : 需要瀏覽器支援 ,使用SourceEvent去不斷地請求伺服器(非同步 ,監聽到返回再進行下一步) ,

伺服器可以hold這個連線直到合適的時候
@Controller
public class SSEController {
    @RequestMapping(value = "/push", produces = "text/event-stream")
    @ResponseBody
    public String push() {
        Random r = new Random();
        try{
            Thread.sleep(5000);
        }catch (Exception e){
            e.printStackTrace();
        }
        return "data:Testing 1,2,3"+r.nextInt()+"\n\n";
    }
}
if (!!window.EventSource) {
    //設定連線後端的方法(url)
    var source = new EventSource('push');

    source.addEventListener('message', function (e) {
        //監聽正常返回的訊息
    });

    source.addEventListener('open', function (e) {
        //監聽開啟連線時
    }, false);

    source.addEventListener('error', function (e) {
        //監聽error
    }, false);
} 

servlet 3.0+ 開啟非同步方法

  • 使用DeferredResult ,非同步返回 ,頁面使用Ajax迴圈訪問即可
/**
* 控制器呼叫 具有非同步特性的service層 ,在呼叫結束後控制器就完成任務
* 由service(實質上時DeferredResult)去控制何時返回響應給客戶端
*/
@Controller
public class AysncController {
    @Autowired PushService pushService;

    @RequestMapping(value = "/defer")
    @ResponseBody
    public DeferredResult<String> defer() {
        return pushService.getAysncUpdate();
    }
}

@Service
public class PushService {
    /**
     * @DeferredResult 是用來實現非同步請求的(業務邏輯耗時很長)
     * 原servlet流程: request->servlet.service()->執行業務邏輯(servlet阻塞)->response
     * 新的servlet流程: request->建立子執行緒執行業務邏輯->servlet結束(但不反悔response)->子執行緒結束返回response(子執行緒中有req,res)
     */
    DeferredResult<String> deferredResult;

    /**
     * 方法建立了一個新的DeferredResult 並直接返回 ,servlet接受到這個result過後就結束任務並返回執行緒池
     * 而request和response移交到了DeferredResult內 ,待setResult後 ,才會返回
     */
    public DeferredResult<String> getAysncUpdate() {
        deferredResult = new DeferredResult();
        return deferredResult;
    }

    @Scheduled(fixedDelay = 3000)
    public void refresh() {
        deferredResult.setResult(String.valueOf(System.currentTimeMillis()));
    }
}

SpringMVC的測試

  • Spring-test + Junit:使用一些模擬的元件對MVC部分進行單元測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringMVCConfig.class})
@WebAppConfiguration("src/main/resources")//標示web資源位置,預設webapp,spring一般是resources
public class TestControllerIntegrationTests {
    private MockMvc mockMvc;//模擬的mvc物件,使用MockMvcBuilders構造

    //測試時注入bean和各種模擬的部件
    @Autowired private DemoService demoService;
    @Autowired WebApplicationContext context;
    @Autowired MockHttpSession session;
    @Autowired MockHttpServletRequest request;

    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
                                      .build();
    }

    @Test
    public void testNormalController() throws Exception {
        //模擬傳送請求
        mockMvc.perform(MockMvcRequestBuilders.get("/normal"))
               //各種預期結果
               .andExpect(MockMvcResultMatchers.status()
                                               .isOk())
               .andExpect(MockMvcResultMatchers.view()
                                               .name("index"))
               .andExpect(MockMvcResultMatchers.forwardedUrl("/WEB-INF/classes/views/index.jsp"))
               .andExpect(MockMvcResultMatchers.model()
                                               .attribute("msg", demoService.saySomething()));
    }

    @Test
    public void testRestController() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/rest/testRest"))
               .andExpect(MockMvcResultMatchers.status()
                                               .isOk())
               .andExpect(MockMvcResultMatchers.content()
                                               .contentType("text/plain;charset=UTF-8"))
               .andExpect(MockMvcResultMatchers.content()
                                               .string(demoService.saySomething()));
    }
}