จะจัดการการกำหนดเวอร์ชัน REST API ด้วยสปริงได้อย่างไร


119

ฉันค้นหาวิธีจัดการเวอร์ชัน REST API โดยใช้ Spring 3.2.x แต่ฉันไม่พบสิ่งที่ดูแลรักษาง่าย ฉันจะอธิบายปัญหาที่ฉันมีก่อนจากนั้นวิธีแก้ปัญหา ... แต่ฉันสงสัยว่าฉันกำลังประดิษฐ์วงล้อขึ้นมาใหม่ที่นี่

ฉันต้องการจัดการเวอร์ชันโดยใช้ส่วนหัว Accept และตัวอย่างเช่นหากคำขอมีส่วนหัว Accept application/vnd.company.app-1.1+jsonฉันต้องการให้ MVC ส่งต่อไปยังวิธีการที่จัดการเวอร์ชันนี้ และเนื่องจากไม่ใช่วิธีการทั้งหมดใน API ที่เปลี่ยนแปลงในรุ่นเดียวกันฉันจึงไม่ต้องการไปที่คอนโทรลเลอร์แต่ละตัวของฉันและเปลี่ยนแปลงอะไรสำหรับตัวจัดการที่ไม่มีการเปลี่ยนแปลงระหว่างเวอร์ชัน ฉันไม่ต้องการให้ตรรกะในการคิดว่าจะใช้เวอร์ชันใดในคอนโทรลเลอร์ด้วยตัวเอง (โดยใช้ตัวระบุตำแหน่งบริการ) เนื่องจาก Spring กำลังค้นพบวิธีการเรียกใช้แล้ว

ดังนั้นจึงใช้ API ที่มีเวอร์ชัน 1.0 ถึง 1.8 ซึ่งมีการแนะนำตัวจัดการในเวอร์ชัน 1.0 และแก้ไขใน v1.7 ฉันต้องการจัดการสิ่งนี้ด้วยวิธีต่อไปนี้ ลองนึกภาพว่าโค้ดนั้นอยู่ในคอนโทรลเลอร์และมีโค้ดบางตัวที่สามารถแยกเวอร์ชันออกจากส่วนหัวได้ (ต่อไปนี้ไม่ถูกต้องในฤดูใบไม้ผลิ)

@RequestMapping(...)
@VersionRange(1.0,1.6)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(...) //same Request mapping annotation
@VersionRange(1.7)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

สิ่งนี้ไม่สามารถทำได้ในฤดูใบไม้ผลิเนื่องจากทั้ง 2 วิธีมีRequestMappingคำอธิบายประกอบเหมือนกันและ Spring ไม่สามารถโหลดได้ แนวคิดคือVersionRangeคำอธิบายประกอบสามารถกำหนดช่วงเวอร์ชันเปิดหรือปิดได้ วิธีแรกใช้ได้ตั้งแต่เวอร์ชัน 1.0 ถึง 1.6 ในขณะที่วิธีที่สองสำหรับเวอร์ชัน 1.7 เป็นต้นไป (รวมถึงเวอร์ชันล่าสุด 1.8) ฉันรู้ว่าแนวทางนี้พังทลายหากมีคนตัดสินใจที่จะส่งผ่านเวอร์ชัน 99.99 แต่นั่นเป็นสิ่งที่ฉันยินดีที่จะอยู่ด้วย

ตอนนี้เนื่องจากข้างต้นเป็นไปไม่ได้หากไม่มีการปรับปรุงใหม่อย่างจริงจังเกี่ยวกับวิธีการทำงานของสปริงฉันจึงคิดที่จะแก้ไขด้วยวิธีที่ตัวจัดการจับคู่กับคำขอโดยเฉพาะอย่างยิ่งการเขียนของฉันเองProducesRequestConditionและมีช่วงเวอร์ชันอยู่ที่นั่น ตัวอย่างเช่น

รหัส:

@RequestMapping(..., produces = "application/vnd.company.app-[1.0-1.6]+json)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(..., produces = "application/vnd.company.app-[1.7-]+json)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

ด้วยวิธีนี้ฉันสามารถกำหนดช่วงเวอร์ชันปิดหรือเปิดที่กำหนดไว้ในส่วนการสร้างของคำอธิบายประกอบ ผมทำงานในการแก้ปัญหานี้ตอนนี้มีปัญหาที่ฉันยังคงมีการแทนที่บางชั้นเรียนหลักฤดูใบไม้ผลิ MVC ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappingและRequestMappingInfo) ซึ่งผมไม่ชอบเพราะมันหมายถึงการทำงานพิเศษทุกครั้งที่ผมตัดสินใจที่จะอัพเกรดเป็นรุ่นที่ใหม่กว่า ฤดูใบไม้ผลิ

ฉันจะขอบคุณทุกความคิด ... และโดยเฉพาะอย่างยิ่งข้อเสนอแนะใด ๆ ให้ทำสิ่งนี้ด้วยวิธีที่ง่ายกว่าและง่ายต่อการดูแล


แก้ไข

การเพิ่มค่าหัว หากต้องการรับรางวัลโปรดตอบคำถามด้านบนโดยไม่แนะนำให้มีตรรกะนี้ในตัวควบคุม Spring มีตรรกะมากมายในการเลือกวิธีการควบคุมที่จะเรียกใช้และฉันต้องการที่จะทำอย่างนั้น


แก้ไข 2

ฉันได้แบ่งปัน POC ดั้งเดิม (พร้อมการปรับปรุงบางอย่าง) ใน github: https://github.com/augusto/restVersioning



1
@flup ฉันไม่เข้าใจความคิดเห็นของคุณ เพียงแค่บอกว่าคุณสามารถใช้ส่วนหัวได้และอย่างที่ฉันบอกไปสิ่งที่สปริงให้มาจากกล่องนั้นไม่เพียงพอที่จะรองรับ API ที่อัปเดตอยู่ตลอดเวลา ยิ่งแย่ไปกว่านั้นลิงก์ในคำตอบนั้นใช้เวอร์ชันใน URL
สิงหาคม

อาจจะไม่ตรงกับสิ่งที่คุณกำลังมองหา แต่ Spring 3.2 รองรับพารามิเตอร์ "ผลิต" บน RequestMapping ข้อแม้ประการหนึ่งคือรายการเวอร์ชันจะต้องมีความชัดเจน เช่นproduces={"application/json-1.0", "application/json-1.1"}ฯลฯ
bimsapi

1
เราจำเป็นต้องรองรับ API หลายเวอร์ชันความแตกต่างเหล่านี้มักจะเป็นการเปลี่ยนแปลงเล็กน้อยที่จะทำให้การโทรจากไคลเอนต์บางตัวเข้ากันไม่ได้ (คงไม่แปลกหากเราต้องรองรับเวอร์ชันรอง 4 เวอร์ชันซึ่งอุปกรณ์ปลายทางบางส่วนไม่สามารถใช้ร่วมกันได้) ฉันขอขอบคุณสำหรับคำแนะนำที่จะใส่ไว้ใน url แต่เรารู้ว่ามันเป็นขั้นตอนที่ผิดเนื่องจากเรามีแอพสองสามตัวที่มีเวอร์ชันใน URL และมีงานมากมายที่เกี่ยวข้องทุกครั้งที่เราต้องชน รุ่น
สิงหาคม

1
@ สิงหาคมคุณก็ไม่ได้ทำเช่นกัน เพียงแค่ออกแบบ API ของคุณก็เปลี่ยนวิธีที่ไม่ทำลายความเข้ากันได้แบบย้อนหลัง เพียงแค่ยกตัวอย่างการเปลี่ยนแปลงที่ทำลายความเข้ากันได้และฉันจะแสดงวิธีทำการเปลี่ยนแปลงเหล่านี้ในแบบที่ไม่ทำลาย
Alexey Andreev

คำตอบ:


62

ไม่ว่าจะสามารถหลีกเลี่ยงการกำหนดเวอร์ชันได้โดยทำการเปลี่ยนแปลงที่เข้ากันได้แบบย้อนหลัง (ซึ่งอาจเป็นไปไม่ได้เสมอไปเมื่อคุณผูกพันตามหลักเกณฑ์ขององค์กรหรือไคลเอนต์ API ของคุณถูกนำไปใช้ในทางที่ผิดพลาดและจะหยุดชะงักแม้ว่าจะไม่ควรก็ตาม) ข้อกำหนดที่เป็นนามธรรมเป็นสิ่งที่น่าสนใจ หนึ่ง:

ฉันจะทำการแมปคำขอแบบกำหนดเองที่ทำการประเมินค่าส่วนหัวโดยพลการจากคำขอโดยไม่ทำการประเมินในเนื้อหาของวิธีการได้อย่างไร

ตามที่อธิบายไว้ในคำตอบ SO นี้คุณสามารถมีเหมือนกัน@RequestMappingและใช้คำอธิบายประกอบอื่นเพื่อแยกความแตกต่างระหว่างการกำหนดเส้นทางจริงที่เกิดขึ้นระหว่างรันไทม์ ในการทำเช่นนั้นคุณจะต้อง:

  1. VersionRangeสร้างคำอธิบายประกอบใหม่
  2. ใช้RequestCondition<VersionRange>. เนื่องจากคุณจะมีบางอย่างเช่นอัลกอริทึมที่ตรงที่สุดคุณจะต้องตรวจสอบว่าวิธีการที่ใส่คำอธิบายประกอบด้วยVersionRangeค่าอื่น ๆให้การจับคู่ที่ดีกว่าสำหรับคำขอปัจจุบันหรือไม่
  3. ใช้งานVersionRangeRequestMappingHandlerMappingตามคำอธิบายประกอบและเงื่อนไขการร้องขอ (ตามที่อธิบายไว้ในโพสต์วิธีใช้คุณสมบัติที่กำหนดเองของ @RequestMapping )
  4. กำหนดค่าสปริงเพื่อประเมินของคุณVersionRangeRequestMappingHandlerMappingก่อนใช้ค่าเริ่มต้นRequestMappingHandlerMapping(เช่นโดยการตั้งค่าลำดับเป็น 0)

สิ่งนี้ไม่จำเป็นต้องมีการเปลี่ยนส่วนประกอบ Spring แบบแฮ็ก แต่ใช้กลไกการกำหนดค่า Spring และส่วนขยายดังนั้นจึงควรใช้งานได้แม้ว่าคุณจะอัปเดตเวอร์ชัน Spring ของคุณ (ตราบใดที่เวอร์ชันใหม่รองรับกลไกเหล่านี้)


ขอบคุณที่เพิ่มความคิดเห็นของคุณเป็นคำตอบ xwoker ถึงตอนนี้เป็นสิ่งที่ดีที่สุด ฉันได้ใช้โซลูชันตามลิงก์ที่คุณกล่าวถึงแล้วและก็ไม่ได้แย่ขนาดนั้น ปัญหาที่ใหญ่ที่สุดจะประจักษ์เมื่ออัพเกรดเป็นรุ่นใหม่ของฤดูใบไม้ผลิที่จะต้องมีการตรวจสอบการเปลี่ยนแปลงใด ๆ mvc:annotation-drivenที่อยู่เบื้องหลังตรรกะ หวังว่า Spring จะมีเวอร์ชันmvc:annotation-drivenที่สามารถกำหนดเงื่อนไขที่กำหนดเองได้
ออกัสโต้

@ สิงหาคมครึ่งปีต่อมาสิ่งนี้ได้ผลสำหรับคุณอย่างไร? นอกจากนี้ฉันอยากรู้ว่าคุณกำลังกำหนดเวอร์ชันตามวิธีการหรือไม่? ณ จุดนี้ฉันสงสัยว่ามันจะไม่ชัดเจนกว่าสำหรับเวอร์ชันในรายละเอียดระดับต่อคลาส / ต่อคอนโทรลเลอร์หรือไม่?
Sander Verhagen

1
@SanderVerhagen ใช้งานได้ แต่เราทำเวอร์ชัน API ทั้งหมดไม่ใช่ตามวิธีการหรือคอนโทรลเลอร์ (API ค่อนข้างเล็กเนื่องจากเน้นด้านเดียวของธุรกิจ) เรามีโครงการที่ใหญ่กว่ามากซึ่งพวกเขาเลือกใช้เวอร์ชันที่แตกต่างกันต่อทรัพยากรและระบุสิ่งนั้นใน URL (ดังนั้นคุณสามารถมีปลายทางใน / v1 / เซสชันและทรัพยากรอื่นในเวอร์ชันที่แตกต่างกันโดยสิ้นเชิงเช่น / v4 / orders) ... มีความยืดหยุ่นกว่าเล็กน้อย แต่จะสร้างแรงกดดันให้กับลูกค้ามากขึ้นในการรู้ว่าควรเรียกใช้อุปกรณ์ปลายทางแต่ละเครื่องในเวอร์ชันใด
สิงหาคม

1
น่าเสียดายที่สิ่งนี้เล่นได้ไม่ดีกับ Swagger เนื่องจากการกำหนดค่าอัตโนมัติจำนวนมากถูกปิดเมื่อขยาย WebMvcConfigurationSupport
Rick

ฉันลองวิธีแก้ปัญหานี้แล้ว แต่มันใช้ไม่ได้กับ 2.3.2 ปล่อยออกมา คุณมีโครงการตัวอย่างที่จะแสดงหรือไม่?
Patrick

54

ฉันเพิ่งสร้างโซลูชันที่กำหนดเอง ฉันใช้@ApiVersionคำอธิบายประกอบร่วมกับ@RequestMappingคำอธิบายประกอบใน@Controllerชั้นเรียน

ตัวอย่าง:

@Controller
@RequestMapping("x")
@ApiVersion(1)
class MyController {

    @RequestMapping("a")
    void a() {}         // maps to /v1/x/a

    @RequestMapping("b")
    @ApiVersion(2)
    void b() {}         // maps to /v2/x/b

    @RequestMapping("c")
    @ApiVersion({1,3})
    void c() {}         // maps to /v1/x/c
                        //  and to /v3/x/c

}

การดำเนินงาน:

คำอธิบายประกอบApiVersion.java :

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestMappingHandlerMapping.java (ส่วนใหญ่จะคัดลอกและวางจากRequestMappingHandlerMapping):

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private final String prefix;

    public ApiVersionRequestMappingHandlerMapping(String prefix) {
        this.prefix = prefix;
    }

    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
        if(info == null) return null;

        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        if(methodAnnotation != null) {
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            // Concatenate our ApiVersion with the usual request mapping
            info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
        } else {
            ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
            if(typeAnnotation != null) {
                RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
                // Concatenate our ApiVersion with the usual request mapping
                info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
            }
        }

        return info;
    }

    private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
        int[] values = annotation.value();
        String[] patterns = new String[values.length];
        for(int i=0; i<values.length; i++) {
            // Build the URL prefix
            patterns[i] = prefix+values[i]; 
        }

        return new RequestMappingInfo(
                new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
                new RequestMethodsRequestCondition(),
                new ParamsRequestCondition(),
                new HeadersRequestCondition(),
                new ConsumesRequestCondition(),
                new ProducesRequestCondition(),
                customCondition);
    }

}

ฉีดเข้าไปใน WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}

4
ฉันเปลี่ยน int [] เป็น String [] เพื่ออนุญาตเวอร์ชันเช่น "1.2" และฉันจึงสามารถจัดการคำหลักเช่น "ล่าสุด" ได้
Maelig

3
ใช่มันค่อนข้างสมเหตุสมผล สำหรับโครงการในอนาคตฉันจะเปลี่ยนไปด้วยเหตุผลบางประการ: 1. URL แสดงถึงทรัพยากร /v1/aResourceและ/v2/aResourceดูเหมือนทรัพยากรที่แตกต่างกันแต่เป็นเพียงการนำเสนอทรัพยากรเดียวกันที่แตกต่างกัน! 2.การใช้ส่วนหัว HTTP จะดูดีกว่าแต่คุณไม่สามารถให้ URL แก่ใครได้เนื่องจาก URL ไม่มีส่วนหัว 3.ใช้พารามิเตอร์ URL เช่น/aResource?v=2.1(btw: นั่นคือวิธีที่ Google กำหนดเวอร์ชัน) ...ฉันยังไม่แน่ใจว่าจะใช้ตัวเลือก2หรือ3แต่ฉันจะไม่ใช้1อีกด้วยเหตุผลข้างต้น
เบนจามินม

5
หากคุณต้องการที่จะฉีดของตัวเองRequestMappingHandlerMappingลงไปในของคุณWebMvcConfigurationคุณควรเขียนทับcreateRequestMappingHandlerMappingแทนrequestMappingHandlerMapping! มิฉะนั้นคุณจะพบปัญหาแปลก ๆ (จู่ๆฉันก็มีปัญหากับการเริ่มต้นที่ขี้เกียจของ Hibernates เนื่องจากเซสชั่นปิด)
stuXnet

1
วิธีการนี้ดูดี แต่ดูเหมือนว่าจะใช้ไม่ได้กับกรณีทดสอบของ junti (SpringRunner) โอกาสใดก็ตามที่คุณมีแนวทางในการทำงานกับกรณีทดสอบ
JDev

1
ไม่มีทางที่จะทำให้การทำงานนี้คือไม่ขยายแต่ขยายWebMvcConfigurationSupport DelegatingWebMvcConfigurationสิ่งนี้ได้ผลสำหรับฉัน (ดูstackoverflow.com/questions/22267191/… )
SeB.Fr

17

ฉันยังคงแนะนำให้ใช้ URL สำหรับการกำหนดเวอร์ชันเนื่องจากใน URLs @RequestMapping รองรับรูปแบบและพารามิเตอร์พา ธ ซึ่งสามารถระบุรูปแบบด้วย regexp

และเพื่อจัดการกับการอัปเกรดไคลเอนต์ (ซึ่งคุณกล่าวถึงในความคิดเห็น) คุณสามารถใช้นามแฝงเช่น 'ล่าสุด' หรือมี API เวอร์ชันที่ไม่ได้แปลงซึ่งใช้เวอร์ชันล่าสุด (ใช่)

นอกจากนี้การใช้พารามิเตอร์พา ธ คุณสามารถใช้ตรรกะการจัดการเวอร์ชันที่ซับซ้อนใด ๆ และหากคุณต้องการมีช่วงอยู่แล้วคุณอาจต้องการบางสิ่งที่เร็วกว่านั้น

นี่คือตัวอย่างสองสามตัวอย่าง:

@RequestMapping({
    "/**/public_api/1.1/method",
    "/**/public_api/1.2/method",
})
public void method1(){
}

@RequestMapping({
    "/**/public_api/1.3/method"
    "/**/public_api/latest/method"
    "/**/public_api/method" 
})
public void method2(){
}

@RequestMapping({
    "/**/public_api/1.4/method"
    "/**/public_api/beta/method"
})
public void method2(){
}

//handles all 1.* requests
@RequestMapping({
    "/**/public_api/{version:1\\.\\d+}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//handles 1.0-1.6 range, but somewhat ugly
@RequestMapping({
    "/**/public_api/{version:1\\.[0123456]?}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//fully manual version handling
@RequestMapping({
    "/**/public_api/{version}/method"
})
public void methodManual2(@PathVariable("version") String version){
    int[] versionParts = getVersionParts(version);
    //manual handling of versions
}

public int[] getVersionParts(String version){
    try{
        String[] versionParts = version.split("\\.");
        int[] result = new int[versionParts.length];
        for(int i=0;i<versionParts.length;i++){
            result[i] = Integer.parseInt(versionParts[i]);
        }
        return result;
    }catch (Exception ex) {
        return null;
    }
}

จากแนวทางสุดท้ายคุณสามารถนำสิ่งที่ต้องการไปใช้ได้จริง

ตัวอย่างเช่นคุณสามารถมีคอนโทรลเลอร์ที่มีเฉพาะเมธอดแทงที่มีการจัดการเวอร์ชัน

ในการจัดการนั้นคุณดู (โดยใช้ไลบรารีการสะท้อน / AOP / การสร้างโค้ด) ใน Spring service / component หรือในคลาสเดียวกันสำหรับเมธอดที่มีชื่อ / ลายเซ็นเดียวกันและต้องการ @VersionRange และเรียกใช้โดยส่งผ่านพารามิเตอร์ทั้งหมด


14

ผมได้ดำเนินการแก้ปัญหาที่จัดการอย่างสมบูรณ์ปัญหากับเวอร์ชันส่วนที่เหลือ

การพูดทั่วไปมี 3 แนวทางหลักสำหรับการกำหนดเวอร์ชันที่เหลือ:

  • Path -based Approch ซึ่งไคลเอ็นต์กำหนดเวอร์ชันใน URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • ส่วนหัวประเภทเนื้อหาซึ่งไคลเอ็นต์กำหนดเวอร์ชันในส่วนหัวยอมรับ :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • ส่วนหัวแบบกำหนดเองซึ่งไคลเอ็นต์กำหนดเวอร์ชันในส่วนหัวแบบกำหนดเอง

ปัญหากับครั้งแรกวิธีการก็คือว่าถ้าคุณเปลี่ยนรุ่นสมมติว่าจาก v1 -> v2 อาจจะคุณจะต้องคัดลอกวางทรัพยากร v1 ที่ยังไม่ได้เปลี่ยนไปยังเส้นทาง v2

ปัญหากับสองวิธีคือว่าเครื่องมือบางอย่างเช่นhttp://swagger.io/ไม่สามารถที่แตกต่างกันระหว่างการดำเนินการกับเส้นทางเดียวกัน แต่แตกต่างกันชนิดเนื้อหา (ออกเช็คhttps://github.com/OAI/OpenAPI-Specification/issues/ 146 )

การแก้ไขปัญหา

เนื่องจากฉันทำงานกับเครื่องมือจัดทำเอกสารส่วนที่เหลืออยู่มากฉันจึงชอบใช้แนวทางแรกมากกว่า โซลูชันของฉันจัดการปัญหาด้วยวิธีการแรกดังนั้นคุณไม่จำเป็นต้องคัดลอกและวางจุดสิ้นสุดไปยังเวอร์ชันใหม่

สมมติว่าเรามีเวอร์ชัน v1 และ v2 สำหรับ User controller:

package com.mspapant.example.restVersion.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * The user controller.
 *
 * @author : Manos Papantonakos on 19/8/2016.
 */
@Controller
@Api(value = "user", description = "Operations about users")
public class UserController {

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v1/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV1() {
         return "User V1";
    }

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v2/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV2() {
         return "User V2";
    }
 }

ต้องการคือถ้าฉันขอv1สำหรับทรัพยากรผู้ใช้ฉันต้องใช้เวลา"V1 ผู้ใช้" repsonse มิฉะนั้นถ้าฉันขอv2 , v3และอื่น ๆ ผมต้องใช้"V2 ผู้ใช้"การตอบสนอง

ใส่คำอธิบายภาพที่นี่

ในการดำเนินการนี้ในฤดูใบไม้ผลิเราจำเป็นต้องลบล้างพฤติกรรมRequestMappingHandlerMappingเริ่มต้น:

package com.mspapant.example.restVersion.conf.mapping;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Value("${server.apiContext}")
    private String apiContext;

    @Value("${server.versionContext}")
    private String versionContext;

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        HandlerMethod method = super.lookupHandlerMethod(lookupPath, request);
        if (method == null && lookupPath.contains(getApiAndVersionContext())) {
            String afterAPIURL = lookupPath.substring(lookupPath.indexOf(getApiAndVersionContext()) + getApiAndVersionContext().length());
            String version = afterAPIURL.substring(0, afterAPIURL.indexOf("/"));
            String path = afterAPIURL.substring(version.length() + 1);

            int previousVersion = getPreviousVersion(version);
            if (previousVersion != 0) {
                lookupPath = getApiAndVersionContext() + previousVersion + "/" + path;
                final String lookupFinal = lookupPath;
                return lookupHandlerMethod(lookupPath, new HttpServletRequestWrapper(request) {
                    @Override
                    public String getRequestURI() {
                        return lookupFinal;
                    }

                    @Override
                    public String getServletPath() {
                        return lookupFinal;
                    }});
            }
        }
        return method;
    }

    private String getApiAndVersionContext() {
        return "/" + apiContext + "/" + versionContext;
    }

    private int getPreviousVersion(final String version) {
        return new Integer(version) - 1 ;
    }

}

การใช้งานจะอ่านเวอร์ชันใน URL และขอตั้งแต่ฤดูใบไม้ผลิเพื่อแก้ไข URL ในกรณีที่ไม่มี URL นี้ (เช่นไคลเอ็นต์ร้องขอv3 ) จากนั้นเราจะลองใช้v2ไปเรื่อย ๆ จนกว่าเราจะพบเวอร์ชันล่าสุดสำหรับทรัพยากร .

เพื่อดูประโยชน์จากการใช้งานนี้สมมติว่าเรามีทรัพยากรสองอย่าง: ผู้ใช้และ บริษัท :

http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company

สมมติว่าเราได้ทำการเปลี่ยนแปลงใน "สัญญา" ของ บริษัท ที่ทำลายลูกค้า ดังนั้นเราจึงใช้http://localhost:9001/api/v2/companyและเราขอจากไคลเอนต์ให้เปลี่ยนเป็น v2 แทนบน v1

ดังนั้นคำขอใหม่จากลูกค้าคือ:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company

แทน:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company

ส่วนที่ดีที่สุดคือด้วยโซลูชันนี้ไคลเอนต์จะได้รับข้อมูลผู้ใช้จาก v1 และข้อมูล บริษัท จาก v2 โดยไม่จำเป็นต้องสร้างจุดสิ้นสุดใหม่ (เดียวกัน) จากผู้ใช้ v2!

เอกสารที่เหลือดัง ที่ฉันได้กล่าวไว้ก่อนหน้านี้เหตุผลที่ฉันเลือกแนวทางการกำหนดเวอร์ชันตาม URL คือเครื่องมือบางอย่างเช่น swagger ไม่ได้จัดทำเอกสารปลายทางที่แตกต่างกันด้วย URL เดียวกัน แต่เป็นประเภทเนื้อหาที่แตกต่างกัน ด้วยโซลูชันนี้จุดสิ้นสุดทั้งสองจะแสดงเนื่องจากมี URL ที่แตกต่างกัน:

ใส่คำอธิบายภาพที่นี่

GIT

การใช้งานโซลูชันที่: https://github.com/mspapant/restVersioningExample/


9

@RequestMappingคำอธิบายประกอบสนับสนุนheadersองค์ประกอบที่ช่วยให้คุณเพื่อทำการร้องขอการจับคู่ โดยเฉพาะอย่างยิ่งคุณสามารถใช้Acceptส่วนหัวที่นี่

@RequestMapping(headers = {
    "Accept=application/vnd.company.app-1.0+json",
    "Accept=application/vnd.company.app-1.1+json"
})

นี่ไม่ใช่สิ่งที่คุณอธิบายอย่างตรงไปตรงมาเนื่องจากไม่ได้จัดการกับช่วงโดยตรง แต่องค์ประกอบนั้นรองรับ * wildcard เช่นเดียวกับ! = ดังนั้นอย่างน้อยคุณก็สามารถหลีกเลี่ยงการใช้สัญลักษณ์แทนสำหรับกรณีที่ทุกเวอร์ชันรองรับปลายทางที่เป็นปัญหาหรือแม้แต่เวอร์ชันรองทั้งหมดของเวอร์ชันหลักที่กำหนด (เช่น 1. *)

ฉันไม่คิดว่าฉันเคยใช้องค์ประกอบนี้มาก่อน (ถ้าฉันจำไม่ได้) ดังนั้นฉันจะปิดเอกสารที่

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html


2
ฉันรู้เกี่ยวกับเรื่องนี้ แต่อย่างที่คุณสังเกตในแต่ละเวอร์ชันฉันจะต้องไปที่คอนโทรลเลอร์ทั้งหมดของฉันและเพิ่มเวอร์ชันแม้ว่าจะไม่ได้เปลี่ยนแปลงก็ตาม ช่วงที่คุณกล่าวถึงใช้งานได้กับประเภทเต็มเท่านั้นเช่นapplication/*ไม่ใช่บางส่วนของประเภท "Accept=application/vnd.company.app-1.*+json"ตัวอย่างต่อไปนี้ไม่ถูกต้องในฤดูใบไม้ผลิ สิ่งนี้เกี่ยวข้องกับวิธีการMediaTypeทำงานของชั้นเรียนฤดูใบไม้ผลิ
สิงหาคม

@ สิงหาคมคุณไม่จำเป็นต้องทำสิ่งนี้ ด้วยวิธีนี้คุณไม่ได้กำลังกำหนดเวอร์ชัน "API" แต่เป็น "ปลายทาง" แต่ละจุดสิ้นสุดอาจมีเวอร์ชันที่แตกต่างกัน สำหรับฉันมันเป็นวิธีที่ง่ายที่สุดที่เวอร์ชันของ API เมื่อเทียบ กับรุ่น API Swagger ยังตั้งค่าได้ง่ายกว่า กลยุทธ์นี้เรียกว่า Versioning ผ่านการเจรจาต่อรองเนื้อหา
Dherik

3

สิ่งที่เกี่ยวกับการใช้การสืบทอดเพื่อสร้างโมเดล นั่นคือสิ่งที่ฉันใช้ในโปรเจ็กต์ของฉันและไม่ต้องใช้การกำหนดค่าสปริงพิเศษและทำให้ฉันได้รับสิ่งที่ฉันต้องการ

@RestController
@RequestMapping(value = "/test/1")
@Deprecated
public class Test1 {
...Fields Getters Setters...
    @RequestMapping(method = RequestMethod.GET)
    @Deprecated
    public Test getTest(Long id) {
        return serviceClass.getTestById(id);
    }
    @RequestMapping(method = RequestMethod.PUT)
    public Test getTest(Test test) {
        return serviceClass.updateTest(test);
    }

}

@RestController
@RequestMapping(value = "/test/2")
public class Test2 extends Test1 {
...Fields Getters Setters...
    @Override
    @RequestMapping(method = RequestMethod.GET)
    public Test getTest(Long id) {
        return serviceClass.getAUpdated(id);
    }

    @RequestMapping(method = RequestMethod.DELETE)
    public Test deleteTest(Long id) {
        return serviceClass.deleteTestById(id);
    }
}

การตั้งค่านี้ช่วยให้มีการทำซ้ำโค้ดเพียงเล็กน้อยและความสามารถในการเขียนทับวิธีการลงใน API เวอร์ชันใหม่โดยใช้งานเพียงเล็กน้อย นอกจากนี้ยังช่วยประหยัดความจำเป็นในการทำให้ซอร์สโค้ดของคุณยุ่งยากด้วยตรรกะการสลับเวอร์ชัน หากคุณไม่ได้เขียนโค้ดปลายทางในเวอร์ชันระบบจะดึงเวอร์ชันก่อนหน้านี้ไปโดยค่าเริ่มต้น

เมื่อเทียบกับสิ่งที่คนอื่นทำแบบนี้ดูเหมือนง่ายกว่า มีบางอย่างที่ฉันขาดหายไป?


1
+1 สำหรับการแบ่งปันรหัส อย่างไรก็ตามการสืบทอดอย่างแน่นแฟ้นนั้นคู่กัน แทน. ตัวควบคุม (Test1 และ Test2) ควรเป็นเพียงการส่งผ่าน ... ไม่มีการใช้ตรรกะ ตรรกะทุกอย่างควรอยู่ในคลาสเซอร์วิส, someService ในกรณีนั้นให้ใช้องค์ประกอบที่เรียบง่ายและไม่ได้รับมรดกจากตัวควบคุมอื่น
Dan Hunex

1
@ dan-hunex ดูเหมือนว่า Ceekay จะใช้การสืบทอดเพื่อจัดการ API เวอร์ชันต่างๆ ถ้าคุณเอามรดกออกทางออกคืออะไร? และเหตุใดคู่รักจึงเป็นปัญหาในตัวอย่างนี้? จากมุมมองของฉัน Test2 ขยาย Test1 เพราะเป็นการปรับปรุง (ด้วยบทบาทเดียวกันและความรับผิดชอบเดียวกัน) ใช่หรือไม่
jeremieca

2

ฉันได้ลองเวอร์ชัน API ของฉันแล้วโดยใช้URI Versioningเช่น:

/api/v1/orders
/api/v2/orders

แต่มีความท้าทายบางอย่างเมื่อพยายามทำให้งานนี้: จัดระเบียบรหัสของคุณด้วยเวอร์ชันต่างๆอย่างไร? วิธีจัดการสองเวอร์ชัน (หรือมากกว่า) ในเวลาเดียวกัน? อะไรคือผลกระทบเมื่อลบบางเวอร์ชัน?

ทางเลือกที่ดีที่สุดที่ผมพบว่าไม่ได้เป็นรุ่นทั้ง API แต่ควบคุมรุ่นในแต่ละปลายทาง รูปแบบนี้เรียกว่าการกำหนดเวอร์ชันโดยใช้ส่วนหัวยอมรับหรือการกำหนดเวอร์ชันผ่านการเจรจาต่อรองเนื้อหา :

วิธีนี้ช่วยให้เราสามารถกำหนดเวอร์ชันการแสดงทรัพยากรเดียวแทนการกำหนดเวอร์ชัน API ทั้งหมดซึ่งช่วยให้เราสามารถควบคุมเวอร์ชันได้ละเอียดยิ่งขึ้น นอกจากนี้ยังสร้างขนาดเล็กลงในฐานรหัสเนื่องจากเราไม่ต้องแยกแอปพลิเคชันทั้งหมดเมื่อสร้างเวอร์ชันใหม่ ข้อดีอีกอย่างของวิธีนี้คือไม่ต้องใช้กฎการกำหนดเส้นทาง URI ที่แนะนำโดยการกำหนดเวอร์ชันผ่านเส้นทาง URI

การใช้งานในฤดูใบไม้ผลิ

ขั้นแรกคุณต้องสร้างคอนโทรลเลอร์ที่มีแอตทริบิวต์สร้างพื้นฐานซึ่งจะใช้โดยค่าเริ่มต้นสำหรับจุดสิ้นสุดแต่ละจุดภายในคลาส

@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController {

}

หลังจากนั้นให้สร้างสถานการณ์ที่เป็นไปได้โดยที่คุณมีปลายทางสองเวอร์ชันสำหรับสร้างคำสั่ง:

@Deprecated
@PostMapping
public ResponseEntity<OrderResponse> createV1(
        @RequestBody OrderRequest orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

@PostMapping(
        produces = "application/vnd.company.etc.v2+json",
        consumes = "application/vnd.company.etc.v2+json")
public ResponseEntity<OrderResponseV2> createV2(
        @RequestBody OrderRequestV2 orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

ทำ! เพียงโทรหาแต่ละจุดสิ้นสุดโดยใช้Http Headerเวอร์ชันที่ต้องการ:

Content-Type: application/vnd.company.etc.v1+json

หรือเพื่อเรียกรุ่นที่สอง:

Content-Type: application/vnd.company.etc.v2+json

เกี่ยวกับความกังวลของคุณ:

และเนื่องจากไม่ใช่วิธีการทั้งหมดในการเปลี่ยนแปลง API ในรุ่นเดียวกันฉันจึงไม่ต้องการไปที่คอนโทรลเลอร์แต่ละตัวของฉันและเปลี่ยนแปลงอะไรสำหรับตัวจัดการที่ไม่ได้เปลี่ยนระหว่างเวอร์ชัน

ตามที่อธิบายไว้กลยุทธ์นี้จะดูแลคอนโทรลเลอร์และปลายทางแต่ละตัวด้วยเวอร์ชันจริงของเขา คุณแก้ไขเฉพาะปลายทางที่มีการปรับเปลี่ยนและต้องการเวอร์ชันใหม่

และผยอง?

การตั้งค่า Swagger ด้วยเวอร์ชันต่างๆก็ทำได้ง่ายมากโดยใช้กลยุทธ์นี้ ดูคำตอบสำหรับรายละเอียดเพิ่มเติม


1

ในการผลิตคุณสามารถปฏิเสธได้ ดังนั้นสำหรับ method1 พูดproduces="!...1.7"และใน method2 มีค่าบวก

Producer ยังเป็นอาร์เรย์ดังนั้นคุณสำหรับ method1 คุณสามารถพูดproduces={"...1.6","!...1.7","...1.8"}ฯลฯ ได้ (ยอมรับทั้งหมดยกเว้น 1.7)

แน่นอนว่าไม่เหมาะเท่าช่วงที่คุณคิด แต่ฉันคิดว่าง่ายต่อการบำรุงรักษามากกว่าสิ่งที่กำหนดเองอื่น ๆ หากนี่เป็นสิ่งที่ผิดปกติในระบบของคุณ โชคดี!


ขอบคุณ codealsa ฉันกำลังพยายามหาวิธีที่ง่ายต่อการบำรุงรักษาและไม่จำเป็นต้องมีบางอย่างในการอัปเดตแต่ละจุดสิ้นสุดทุกครั้งที่เราต้องชนรุ่น
สิงหาคม

0

คุณสามารถใช้ AOP เกี่ยวกับการสกัดกั้น

พิจารณาการทำแผนที่คำขอซึ่งได้รับทั้งหมด/**/public_api/*และในวิธีนี้ไม่ต้องทำอะไรเลย

@RequestMapping({
    "/**/public_api/*"
})
public void method2(Model model){
}

หลังจาก

@Override
public void around(Method method, Object[] args, Object target)
    throws Throwable {
       // look for the requested version from model parameter, call it desired range
       // check the target object for @VersionRange annotation with reflection and acquire version ranges, call the function if it is in the desired range


}

ข้อ จำกัด เพียงอย่างเดียวคือทั้งหมดจะต้องอยู่ในคอนโทรลเลอร์เดียวกัน

สำหรับการกำหนดค่า AOP ดูได้ที่http://www.mkyong.com/spring/spring-aop-examples-advice/


ขอบคุณ hevi ฉันกำลังมองหาวิธีที่เป็นมิตรกับ "สปริง" มากขึ้นเนื่องจาก Spring เลือกวิธีการโทรโดยไม่ใช้ AOP แล้ว ฉันเป็นมุมมองของฉัน AOP เพิ่มระดับใหม่ของความซับซ้อนของรหัสที่ฉันต้องการหลีกเลี่ยง
ออกัสโต

@ สิงหาคม Spring มีการสนับสนุน AOP ที่ยอดเยี่ยม คุณควรทดลองใช้ :)
Konstantin Yovkov
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.